HTTP 请求头一般都是由客户端设置,服务端获取,用以处理业务。但是偶尔也有一些需求,需要服务端对一些请求,添加自定义的请求头。例如:统一为所有请求添加一个名为 X-Requested-Id
的请求头,用于跟踪不同请求。
Servlet 中的请求对象 jakarta.servlet.http.HttpServletRequest
并未提供设置 Header 的方法。
但是,但是通过一些方法,可以实现。本教程将会教你如何在 Spring Boot 应用中添加自定义请求头到 HttpServletRequest
。
思路 {#思路}
既然可以从 HttpServletRequest
获取到 Header,那它一定是把 Header 存储在了某个地方。我们只要找到 HttpServletRequest
存储 Header 的容器,就可以对其进行添加、编辑、删除操作。
不同 HttpServletRequest
的实现中,对 Header 的存储方式不一定一样。目前,在 Spring Boot 中最流行的 Servlet 实现就是 Tomcat 和 Undertow,所以下文将会针对这两个服务器进行讲解。
示例应用 {#示例应用}
创建一个 Spring Boot 应用。我们要在 Filter
中为所有请求添加一个 X-Requested-Id
请求头,并在 Controller 中获取并返回。
Controller 如下:
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
@RestController @RequestMapping("/demo") public class DemoController { @GetMapping public String getHeaders (HttpServletRequest request) {
// 获取 X-Requested-Id Header 值 String requestedId = request.getHeader("X-Requested-Id");
return requestedId;
}
}
在 Spring Boot 应用中,我们可以通过在 Handler 方法中声明
HttpServletRequest
对象,调用其getHeader()
方法来获取客户端的请求头。String requestedId = request.getHeader("X-Requested-Id",);
也可以在 Handler 方法中使用
@RequestHeader
注解参数来声明要获取的请求头:@RequestHeader(name = "X-Requested-Id", required = false) String requestedId
Tomcat {#tomcat}
Tomcat 对 Servlet 的实现稍微有点复杂,使用了观察者模式。
-
首先是
org.apache.catalina.connector.RequestFacade
实现。 -
RequestFacade
中定义了一个org.apache.catalina.connector.Request request
的字段。protected org.apache.catalina.connector.Request request
-
org.apache.catalina.connector.Request
中又定义了一个org.apache.coyote.Request coyoteRequest
字段。protected org.apache.coyote.Request coyoteRequest;
-
org.apache.coyote.Request
对象有一个org.apache.tomcat.util.http.MimeHeaders
字段,它便是封装了客户端 Header 的容器。并且提供了获取它的public
方法。// ... private final MimeHeaders headers = new MimeHeaders(); // ... public MimeHeaders getMimeHeaders() { return headers; }
那么,我们可以先通过反射从 RequestFacade
中获取到 request
对象,再使用反射从 request
对象中获取到 coyoteRequest
对象。最后调用它的 getMimeHeaders()
方法来获取到封装了请求头的 MimeHeaders
对象。
Filter 实现如下:
import java.io.IOException; import java.lang.reflect.Field; import java.util.UUID;
import org.apache.tomcat.util.http.MimeHeaders; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils;
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
@WebFilter(urlPatterns = "/*") // 拦截所有请求 @Component public class TomcatRequesteIdHeaderFilter extends HttpFilter{
/** * */ private static final long serialVersionUID = -287194674100570345L;
@Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
// 使用反射获取 RequestFacade 中的 org.apache.catalina.connector.Request 对象 Field connectorRequestField = ReflectionUtils.findField(req.getClass(), "request", org.apache.catalina.connector.Request.class); connectorRequestField.setAccessible(true); // 允许反射访问 org.apache.catalina.connector.Request connectorRequest = (org.apache.catalina.connector.Request) ReflectionUtils.getField(connectorRequestField, req);
// 使用反射获取 org.apache.catalina.connector.Request 中的 org.apache.coyote.Request 对象 Field coyoteRequestField = ReflectionUtils.findField(org.apache.catalina.connector.Request.class, "coyoteRequest", org.apache.coyote.Request.class); coyoteRequestField.setAccessible(true); // 允许反射访问 org.apache.coyote.Request coyoteRequest = (org.apache.coyote.Request) ReflectionUtils.getField(coyoteRequestField, connectorRequest);
// 从 coyoteRequest 对象获取到 MimeHeaders MimeHeaders headers = coyoteRequest.getMimeHeaders();
// 添加新的 Header headers.addValue("X-Requested-Id").setString(UUID.randomUUID().toString());
// 继续执行调用链 super.doFilter(connectorRequest, res, chain);
}
}
我们在此 Filter 中为所有请求,都添加了一个 X-Requested-Id
头,它是值是一个 UUID。
测试,启动服务后使用 curl 发起请求:
$ curl localhost:8080/demo
ee3382ba-6567-4261-bb0a-91ddfbdab119
如你所见,Controller 返回了 UUID 值,表示我们成功在 HttpServletRequest
中添加了 X-Requested-Id
头。
Undertow {#undertow}
Spring Boot 默认使用 Tomcat 作为嵌入式服务器,如果你想替换为 Undertow 可以参考 这篇文章。
Undertow 的实现就简单太多了,首先是 io.undertow.servlet.spec.HttpServletRequestImpl
实现。它提供了一个 public
方法可以获取到 io.undertow.server.HttpServerExchange
对象。
io.undertow.server.HttpServerExchange
又有一个 public
方法可以获取到 io.undertow.util.HeaderMap
对象,这就是封装了请求头的 Map 对象。
简而言之, Undertow 不需要反射,支持链式调用,甚至只要一行代码就可以获取到 HeaderMap
对象,如下:
import java.io.IOException; import java.util.UUID;
import org.springframework.stereotype.Component;
import io.undertow.servlet.spec.HttpServletRequestImpl; import io.undertow.util.HeaderMap; import io.undertow.util.HttpString; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
@WebFilter(urlPatterns = "/*") @Component public class UndertowRequesteIdHeaderFilter extends HttpFilter{
/** * */ private static final long serialVersionUID = -374523395760767552L;
@Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
// 强制转换为 HttpServletRequestImpl HttpServletRequestImpl request = (HttpServletRequestImpl) req; // 获取到 HeaderMap HeaderMap headerMap = request.getExchange().getRequestHeaders();
// 添加 Header headerMap.add(HttpString.tryFromString("X-Requested-Id"), UUID.randomUUID().toString());
// 继续执行调用链 super.doFilter(req, res, chain);
}
}
和 Tomcat
示例一样,也是添加了一个值是 UUID 的 X-Requested-Id
头到请求中。
启动服务,使用 CURL 进行测试:
$ curl localhost:8080/demo
02ceed83-f33a-4387-8e41-e89e9916e63d
同样没问题。