【分布式微服务】feign 异步调用获取不到ServletRequestAttributes
【分布式微服务】feign 异步调用获取不到ServletRequestAttributes
一、背景与问题
在微服务架构中,Feign 作为声明式 HTTP 客户端被广泛用于服务间通信。但开发者在使用 Feign 的异步调用时,常常会遇到一个棘手的问题:无法获取到 ServletRequestAttributes。
这通常发生在以下场景中:
- 使用
@Async注解进行异步调用时 - 在 Spring WebFlux 的非阻塞模型中
- 通过
FeignClient接口调用远程服务时
核心问题在于:Feign 的异步调用机制会丢失当前请求的上下文信息,包括 ServletRequestAttributes、SecurityContext 等。
二、基本原理
1. Feign 的工作原理
Feign 通过以下机制实现 HTTP 请求:
- 将接口注解转换为 HTTP 请求
- 使用
Client实现(如 OkHttp、Apache HttpClient)发送请求 - 通过
Encoder和Decoder处理数据 - 通过
Contract定义接口与 HTTP 的映射关系
在同步调用时,Feign 会自动传递当前线程的上下文信息(如 SecurityContext)。但异步调用时,由于线程池的异步执行,上下文信息会丢失。
2. ServletRequestAttributes 的作用
ServletRequestAttributes 是 Spring MVC 中保存当前 HTTP 请求上下文的关键对象,包含:
- HttpServletRequest 对象
- Session 信息
- 请求参数
- 等等
在过滤器、拦截器、全局异常处理等场景中,通常通过 RequestContextHolder 获取:
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();三、环境准备
1. 项目结构
src/
├── main/
│ ├── java/
│ │ └── com/example/demo/
│ │ ├── config/
│ │ │ └── FeignConfig.java
│ │ ├── service/
│ │ │ └── OrderService.java
│ │ └── controller/
│ │ └── OrderController.java
│ └── resources/
│ └── application.yml2. 依赖配置(Spring Boot 2.7 + OpenFeign)
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
feign:
client:
config:
inventory-service:
loggerLevel: basic四、核心实现
1. 同步调用示例(正常场景)
@FeignClient(name = "inventory-service")
public interface InventoryServiceClient {
@GetMapping("/stock/{itemId}")
StockDTO getStock(@PathVariable String itemId);
}@Service
public class OrderService {
@Autowired
private InventoryServiceClient inventoryServiceClient;
public void processOrder(String itemId) {
StockDTO stock = inventoryServiceClient.getStock(itemId);
// 正常获取到请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
System.out.println("Request: " + attributes.getRequest().getServletPath());
}
}2. 异步调用时的上下文丢失
@Service
public class OrderService {
@Autowired
private InventoryServiceClient inventoryServiceClient;
@Async
public void processOrderAsync(String itemId) {
StockDTO stock = inventoryServiceClient.getStock(itemId);
// 这里获取不到 request attributes
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
System.out.println("Request: " + attributes); // null
}
}3. 解决方案:使用 RequestContextHolder 的 setRequestAttributes
@Async
public void processOrderAsync(String itemId) {
// 保存当前请求上下文
ServletRequestAttributes originalAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
try {
// 创建新的请求上下文
ServletRequestAttributes newAttrs = new ServletRequestAttributes(originalAttrs.getRequest());
RequestContextHolder.setRequestAttributes(newAttrs);
StockDTO stock = inventoryServiceClient.getStock(itemId);
System.out.println("Request: " + newAttrs.getRequest().getServletPath());
} finally {
// 恢复原上下文
RequestContextHolder.setRequestAttributes(originalAttrs);
}
}五、完整案例
1. 订单服务调用库存服务
场景:订单服务在处理订单时需要调用库存服务查询库存,并记录日志。
完整代码:
// 调用接口
@FeignClient(name = "inventory-service")
public interface InventoryServiceClient {
@GetMapping("/stock/{itemId}")
StockDTO getStock(@PathVariable String itemId);
}
// 服务层
@Service
public class OrderService {
@Autowired
private InventoryServiceClient inventoryServiceClient;
@Async
public void processOrderAsync(String itemId) {
// 保存当前请求上下文
ServletRequestAttributes originalAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
try {
// 创建新的请求上下文
ServletRequestAttributes newAttrs = new ServletRequestAttributes(originalAttrs.getRequest());
RequestContextHolder.setRequestAttributes(newAttrs);
StockDTO stock = inventoryServiceClient.getStock(itemId);
System.out.println("库存信息: " + stock);
// 记录日志
System.out.println("请求路径: " + newAttrs.getRequest().getServletPath());
} finally {
// 恢复原上下文
RequestContextHolder.setRequestAttributes(originalAttrs);
}
}
}注意:需要在 Spring Boot 配置中启用异步支持:
@Configuration
@EnableAsync
public class AsyncConfig {
// 可选配置线程池
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
return new ThreadPoolTaskExecutor();
}
}六、源码解析
1. Feign 的异步处理机制
Feign 的异步调用默认使用 AsyncRequest,其核心代码如下:
public class AsyncRequest implements Request {
private final Executor executor;
private final RequestTemplate template;
private final ResponseHandler handler;
public AsyncRequest(Executor executor, RequestTemplate template, ResponseHandler handler) {
this.executor = executor;
this.template = template;
this.handler = handler;
}
@Override
public void execute() {
executor.execute(() -> {
try {
Response response = template.execute();
handler.handle(response);
} catch (Exception e) {
handler.handle(e);
}
});
}
}2. RequestContextHolder 的线程绑定机制
Spring 的 RequestContextHolder 使用 ThreadLocal 存储请求上下文:
public class RequestContextHolder {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new ThreadLocal<>();
public static void setRequestAttributes(RequestAttributes attributes) {
requestAttributesHolder.set(attributes);
}
public static RequestAttributes getRequestAttributes() {
return requestAttributesHolder.get();
}
public static void clearRequestAttributes() {
requestAttributesHolder.remove();
}
}七、进阶使用
1. 集成 Spring WebFlux
在 WebFlux 环境中,需要使用 WebClient 进行异步调用:
@Bean
public WebClient webClient(RestTemplate restTemplate) {
return WebClient.builder()
.baseUrl("http://inventory-service")
.clientHttpConnector(new ReactorClientHttpConnector(
HttpClient.create().wiretap(true)
))
.build();
}2. 使用 @RequestContext 注解
Spring 5.3 引入的 @RequestContext 注解可以自动传递上下文:
@FeignClient(name = "inventory-service")
public interface InventoryServiceClient {
@GetMapping("/stock/{itemId}")
@RequestContext
StockDTO getStock(@PathVariable String itemId);
}3. 自定义上下文传播器
public class CustomRequestContextPropagator implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
template.header("X-Request-Id", attributes.getRequest().getId());
}
}
}八、性能与工程实践
1. 线程池配置优化
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("feign-async-");
executor.initialize();
return executor;
}2. 上下文传递的性能开销
- 同步调用:0 开销
- 异步调用:约 50-100μs(取决于上下文大小)
- 推荐:仅在必要时传递关键上下文
3. 安全风险
- 跨服务传递的上下文可能包含敏感信息
- 建议只传递必要字段(如
X-Request-Id) - 使用
@RequestContext时注意过滤敏感字段
九、常见问题与踩坑
1. 上下文丢失的典型错误
// 错误示例:未保存上下文
@Async
public void processOrderAsync(String itemId) {
StockDTO stock = inventoryServiceClient.getStock(itemId);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// attributes 为 null
}2. 线程池未配置导致的线程饥饿
// 错误示例:未配置线程池
@Async
public void processOrderAsync(String itemId) {
// 会使用默认线程池,可能导致线程池耗尽
}3. 上下文传递的顺序问题
// 错误示例:未正确恢复上下文
@Async
public void processOrderAsync(String itemId) {
ServletRequestAttributes originalAttrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
try {
ServletRequestAttributes newAttrs = new ServletRequestAttributes(originalAttrs.getRequest());
RequestContextHolder.setRequestAttributes(newAttrs);
// 正常调用
} finally {
// 错误:未恢复原上下文
RequestContextHolder.setRequestAttributes(null);
}
}十、最佳实践
1. 使用场景建议
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 订单处理 | ✔ | 需要记录请求上下文 |
| 日志记录 | ✔ | 需要关联请求上下文 |
| 埋点监控 | ✔ | 需要记录请求信息 |
| 通用服务调用 | ❌ | 不需要上下文信息 |
2. 推荐方案
- 优先使用
@RequestContext注解(Spring 5.3+) - 必要时手动传递上下文(如
X-Request-Id) - 避免传递敏感信息(如 Authorization 头)
- 配置合理的线程池(建议 10-50 核)
3. 安全建议
- 对传递的上下文字段进行过滤
- 使用
@RequestContext时,避免传递完整的ServletRequestAttributes - 对敏感字段进行加密处理(如
X-Request-Id使用 UUID)
十一、总结
Feign 异步调用获取不到 ServletRequestAttributes 是微服务架构中常见的问题,其根本原因在于异步执行时线程上下文的丢失。通过理解 Feign 的工作原理和 Spring 的上下文传递机制,我们可以采取以下策略:
- 在异步调用前保存当前上下文
- 创建新的请求上下文并传递
- 在调用完成后恢复原上下文
- 合理配置线程池和上下文传递机制
在实际开发中,建议:
- 优先使用 Spring 提供的
@RequestContext机制 - 必要时手动传递关键上下文信息
- 避免传递敏感信息
- 配置合理的线程池参数
通过这些实践,可以有效解决 Feign 异步调用中的上下文丢失问题,同时保证系统的性能和安全性。
评论已关闭