在微服务架构中,获取 HTTP 请求头信息与在单体 Spring MVC 应用中类似,但也需要考虑一些微服务特有的场景,比如服务间的调用和上下文传播。
以下是在基于 Spring (特别是 Spring Boot / Spring Cloud) 的微服务中获取请求头信息的几种主要方式:
1. 在 Controller 层使用 @RequestHeader
(最常用)
这是最直接、最标准的方式,与传统 Spring MVC 完全一致。当请求到达微服务 Controller 时,可以使用 @RequestHeader
注解将特定的请求头值注入到方法参数中。
java
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Optional;
@RestController
public class HeaderApiController {
// 1. 获取单个必需的 Header
@GetMapping("/api/service-a/resource")
public ResponseEntity<String> getResource(
@RequestHeader("Authorization") String authorizationToken,
@RequestHeader("X-Request-ID") String requestId) { // 例如,用于分布式追踪的请求 ID
System.out.println("Authorization Token: " + authorizationToken);
System.out.println("Request ID: " + requestId);
// ... 处理业务逻辑 ...
return ResponseEntity.ok("Resource data from Service A, Request ID: " + requestId);
}
// 2. 获取单个可选的 Header (带默认值)
@GetMapping("/api/service-a/config")
public ResponseEntity<String> getConfig(
@RequestHeader(name = "X-Tenant-ID", defaultValue = "default_tenant") String tenantId) {
System.out.println("Processing request for Tenant ID: " + tenantId);
// ... 根据 tenantId 处理逻辑 ...
return ResponseEntity.ok("Configuration for tenant: " + tenantId);
}
// 3. 获取单个可选的 Header (可能为 null 或使用 Optional)
@GetMapping("/api/service-a/optional")
public ResponseEntity<String> getOptionalData(
@RequestHeader(name = "X-User-Preferences", required = false) String preferences,
@RequestHeader(name = "X-Device-Type", required = false) Optional<String> deviceType) {
String prefMessage = (preferences != null) ? "Preferences: " + preferences : "Preferences header missing.";
String deviceMessage = deviceType.map(dt -> "Device Type: " + dt).orElse("Device Type header missing.");
System.out.println(prefMessage);
System.out.println(deviceMessage);
return ResponseEntity.ok(prefMessage + " | " + deviceMessage);
}
// 4. 获取所有 Headers (推荐使用 HttpHeaders)
@GetMapping("/api/service-a/all-headers")
public ResponseEntity<String> getAllHeaders(@RequestHeader HttpHeaders headers) {
// HttpHeaders 提供了便捷方法访问各种标准和自定义头
String contentType = String.valueOf(headers.getContentType());
long traceId = headers.getFirst("X-B3-TraceId") != null ? Long.parseLong(headers.getFirst("X-B3-TraceId"), 16) : -1L; // 示例:处理追踪头
System.out.println("Received Headers: " + headers);
System.out.println("Content-Type: " + contentType);
System.out.println("Trace ID (if present): " + traceId);
// 不太推荐的方式:
// @RequestHeader Map<String, String> headerMap // 可能丢失多值 Header
// @RequestHeader MultiValueMap<String, String> headerMultiMap // 可以处理多值 Header
return ResponseEntity.ok("Processed request with headers. Trace ID (long): " + traceId);
}
}
2. 通过 HttpServletRequest
(方法参数注入)
虽然不如 @RequestHeader
直接,但你仍然可以像在传统 Servlet 应用中那样,将 HttpServletRequest
注入到 Controller 方法中,然后调用其 getHeader()
, getHeaders()
, getHeaderNames()
等方法。
java
import javax.servlet.http.HttpServletRequest;
// ... 其他 imports ...
@RestController
public class ServletHeaderController {
@GetMapping("/api/service-b/info")
public ResponseEntity<String> getServiceInfo(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
String customHeader = request.getHeader("X-Custom-Data");
// 获取某个 header 的所有值 (如果允许重复)
java.util.Enumeration<String> acceptHeaders = request.getHeaders("Accept");
System.out.println("User-Agent (via HttpServletRequest): " + userAgent);
System.out.println("X-Custom-Data (via HttpServletRequest): " + customHeader);
StringBuilder acceptHeaderString = new StringBuilder();
if (acceptHeaders != null) {
while(acceptHeaders.hasMoreElements()) {
acceptHeaderString.append(acceptHeaders.nextElement()).append(", ");
}
}
System.out.println("Accept Headers: " + acceptHeaderString);
// ... 业务逻辑 ...
return ResponseEntity.ok("Info from Service B. User-Agent starts with: " + (userAgent != null ? userAgent.substring(0, 10) : "N/A"));
}
}
何时使用? 当需要访问 @RequestHeader
不直接支持的功能,或者需要更底层的控制时(虽然比较少见)。
3. 在过滤器 (Filter) 或拦截器 (Interceptor) 中获取
在微服务中,很多横切关注点 (Cross-Cutting Concerns)如认证、授权、日志记录、分布式追踪上下文传播 等,在请求到达 Controller 之前处理。这时,可以在 Filter
或 Interceptor
中获取请求头。
示例 (使用 Servlet Filter):
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC; // 用于日志上下文
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; // 保证每个请求只执行一次
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component // 让 Spring 管理这个 Filter
public class LoggingAndTraceFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(LoggingAndTraceFilter.class);
private static final String TRACE_ID_HEADER = "X-Trace-ID";
private static final String TENANT_ID_HEADER = "X-Tenant-ID";
private static final String TRACE_ID_MDC_KEY = "traceId";
private static final String TENANT_ID_MDC_KEY = "tenantId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1. 从请求头获取或生成 Trace ID
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
log.debug("Generated new Trace ID: {}", traceId);
// 如果需要传播,可将生成的 ID 添加到响应头或下游请求头
}
// 2. 从请求头获取 Tenant ID
String tenantId = request.getHeader(TENANT_ID_HEADER);
if (tenantId == null) {
tenantId = "unknown"; // Or handle as an error depending on requirements
}
// 3. 将信息放入 MDC (Mapped Diagnostic Context) 以便日志自动包含这些信息
MDC.put(TRACE_ID_MDC_KEY, traceId);
MDC.put(TENANT_ID_MDC_KEY, tenantId);
log.info("Request received: {} {} for Tenant: {}", request.getMethod(), request.getRequestURI(), tenantId);
try {
// 继续处理请求链 (最终会到达 Controller)
filterChain.doFilter(request, response);
} finally {
// 4. 请求处理完毕后,清理 MDC,避免影响线程池中的下一个请求
MDC.remove(TRACE_ID_MDC_KEY);
MDC.remove(TENANT_ID_MDC_KEY);
log.info("Request completed: {} {}", request.getMethod(), request.getRequestURI());
}
}
}
场景:
- 分布式追踪 : 获取或生成追踪 ID (如
X-Request-ID
,X-B3-TraceId
),放入日志上下文 (MDC),并在调用下游服务时传递。Spring Cloud Sleuth 等库会自动处理很多这类逻辑。 - 认证/授权 : 读取
Authorization
头,验证 Token,并将用户信息放入安全上下文 (SecurityContextHolder
)。Spring Security 等框架会做这些。 - 多租户 : 读取
X-Tenant-ID
头,设置当前线程的租户上下文。 - 通用日志: 记录所有请求的某些通用头信息。
4. 在 Service 层或更深层访问 (需要传递或使用上下文)
如果业务逻辑层 (Service Layer) 或其他非 Controller 组件需要访问请求头信息,不推荐 直接依赖 HttpServletRequest
或 RequestContextHolder
(因为它会耦合业务逻辑与 Web 层)。
推荐的方式是:
-
参数传递 (首选) : 在 Controller 层使用
@RequestHeader
获取所需头信息,然后将其作为参数传递给 Service 方法。这是最清晰、最解耦、最易于测试的方式。java// Controller @GetMapping("/api/service-c/process") public ResponseEntity<String> processData( @RequestHeader("X-User-ID") String userId, @RequestHeader("X-Correlation-ID") String correlationId) { String result = myService.processUserData(userId, correlationId); // 传递给 Service return ResponseEntity.ok(result); } // Service @Service public class MyService { public String processUserData(String userId, String correlationId) { // 使用 userId 和 correlationId 进行处理 System.out.println("Service processing for User: " + userId + ", Correlation: " + correlationId); // ... return "Processed data for user " + userId; } }
-
使用线程本地上下文 (ThreadLocal) : 对于像追踪 ID、租户 ID、用户 ID 这样需要在整个请求处理链中都可用的信息,可以在 Filter/Interceptor 中将其读取并存入自定义的
ThreadLocal
变量或使用现成的上下文机制(如 Spring Security 的SecurityContextHolder
、日志的 MDC、或自定义的TenantContextHolder
)。然后在 Service 层或其他地方从该上下文中读取。java// 在 Filter/Interceptor 中设置 (如上面的 LoggingAndTraceFilter 示例设置 MDC) // 在 Service 层获取 @Service public class AnotherService { private static final Logger log = LoggerFactory.getLogger(AnotherService.class); public void doSomething() { String traceId = MDC.get("traceId"); // 从 MDC 获取 String tenantId = MDC.get("tenantId"); // 或者从 SecurityContextHolder 获取用户信息 // Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // String username = (authentication != null) ? authentication.getName() : "N/A"; // 使用这些上下文信息 log.info("Doing something within service context. Trace ID: {}, Tenant ID: {}", traceId, tenantId); // ... } }
总结与建议
- Controller 层 : 优先使用
@RequestHeader
。清晰、简洁,是 Spring 推荐的方式。使用HttpHeaders
参数可以获取所有头信息。 - Filter/Interceptor : 用于处理横切关注点,如日志、认证、追踪、多租户等,获取相关头信息,并可以设置请求/线程上下文(如 MDC, SecurityContext, 自定义 ThreadLocal)。
- Service 层及以下 : 优先通过方法参数传递 从 Controller 获取的头信息。如果信息是全局上下文(如追踪 ID、用户 ID、租户 ID),则通过 Filter/Interceptor 设置并在需要时从线程本地上下文 (MDC, SecurityContextHolder, 自定义 ContextHolder)读取。避免直接依赖 Web 层 API。
- 框架利用: 充分利用 Spring Cloud Sleuth (用于追踪头传播)、Spring Security (用于认证头处理) 等框架,它们能自动处理很多常见的头信息获取和上下文管理工作。
根据具体需求选择最合适的方式。在微服务中,清晰的处理和传递请求头对于保证服务的正确性、可观测性和安全性至关重要。