微服务中实现基于 Feign + Hystrix 的调用上下文透传 Starter 实践
·
微服务中实现基于 Feign + Hystrix 的调用上下文透传 Starter 实践
一、解决的核心问题
微服务架构下,服务 A 调用服务 B 时面临两个关键问题:
- 认证信息丢失:前端请求带的 JWT Token 不会自动传递到下游服务
- 请求上下文丢失:Hystrix 线程池隔离模式下,子线程无法访问主线程的
ThreadLocal(RequestAttributes、链路追踪 ID 等)
本 Starter 通过 Feign 拦截器 + Hystrix 自定义并发策略,实现上下文在微服务间的自动透传。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、涉及的技术知识点
2.1 Feign 请求拦截器
| 知识点 | 说明 |
|---|---|
feign.RequestInterceptor | Feign 提供的扩展点,在每次请求发送前执行 |
RequestTemplate | Feign 请求模板,可动态添加 Header、Query 参数等 |
| 拦截器链 | 多个 Interceptor 按注册顺序依次执行 |
2.2 Hystrix 线程池隔离与上下文传递
| 知识点 | 说明 |
|---|---|
| Hystrix 线程隔离 | Feign 调用默认在 Hystrix 管理的独立线程池中执行,与主线程隔离 |
HystrixConcurrencyStrategy | Hystrix 提供的并发策略扩展点,可自定义线程创建和任务包装行为 |
wrapCallable() | 在任务提交到线程池前包装 Callable,实现上下文搬运 |
HystrixPlugins | Hystrix 全局插件注册中心,支持注册自定义策略 |
| ThreadLocal 跨线程 | 主线程捕获上下文 → 包装到 Callable → 子线程执行时恢复 |
2.3 Spring Web 请求上下文
| 知识点 | 说明 |
|---|---|
RequestContextHolder | Spring 提供的请求上下文持有者,底层是 ThreadLocal |
RequestAttributes | 封装了 HttpServletRequest 的属性信息 |
RequestContextListener | 确保非 DispatcherServlet 管理的请求也能使用 RequestContext |
2.4 Spring Boot 自动配置
| 知识点 | 说明 |
|---|---|
spring.factories | SPI 机制声明自动配置入口 |
@Configuration + @Bean | 声明式注册组件 |
@Value | 注入配置项(如项目名称) |
@PostConstruct | Bean 初始化后执行 Hystrix 插件注册 |
2.5 设计模式
| 模式 | 应用场景 |
|---|---|
| 装饰器模式 | WrappedCallable 包装原始 Callable,增加上下文恢复能力 |
| 委托模式 | 自定义并发策略委托给已有策略处理非核心方法 |
| 拦截器模式 | Feign RequestInterceptor 在调用链中透明注入逻辑 |
| 模板方法 | HystrixConcurrencyStrategy 定义模板,子类覆写 wrapCallable |
三、封装 Starter 的流程
3.1 项目结构
example-feign-context-starter/
├── pom.xml
└── src/main/
├── java/com/example/feign/
│ ├── FeignContextAutoConfiguration.java // 自动配置入口
│ ├── FeignContextInterceptor.java // Feign请求拦截器
│ ├── HystrixContextConfiguration.java // Hystrix上下文配置
│ └── HystrixContextConcurrencyStrategy.java // 自定义并发策略
└── resources/META-INF/
└── spring.factories // 自动配置声明
3.2 封装步骤
- 识别通用需求 — 多个微服务都需要透传 Token 和链路 ID
- 定义扩展点 — Feign 拦截器处理 Header 注入,Hystrix 策略处理线程切换
- 实现核心逻辑 — 主线程捕获上下文,子线程恢复上下文
- 自动配置 — spring.factories 声明,引入即生效
- 打包发布 —
mvn deploy到私有 Maven 仓库
3.3 引入方使用
<dependency>
<groupId>com.example</groupId>
<artifactId>example-feign-context-starter</artifactId>
<version>1.0.0</version>
</dependency>
引入后无需任何额外配置或注解,所有 Feign 调用自动携带 Token、RequestId、调用来源信息。
四、通用示例代码
4.1 spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.feign.FeignContextAutoConfiguration
4.2 FeignContextAutoConfiguration(自动配置入口)
package com.example.feign;
import feign.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextListener;
/**
* Feign上下文透传自动配置.
* 引入依赖后自动生效,无需额外注解.
*/
@Configuration
public class FeignContextAutoConfiguration {
@Value("${spring.application.name:unknown}")
private String applicationName;
/**
* Feign日志级别设为FULL,记录完整请求响应.
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
/**
* Feign请求拦截器,自动透传认证和链路信息.
*/
@Bean
public FeignContextInterceptor feignContextInterceptor() {
return new FeignContextInterceptor(applicationName);
}
/**
* 确保RequestContext在非DispatcherServlet场景下可用.
*/
@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
4.3 FeignContextInterceptor(Feign 请求拦截器)
package com.example.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.net.URLDecoder;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Feign请求拦截器.
* 每次Feign调用前自动注入以下Header:
* 1. Authorization - JWT Token透传
* 2. X-Request-Id - 链路追踪ID
* 3. X-Referer - 调用来源服务名
* 4. X-Referer-Host - 调用来源主机名
*/
public class FeignContextInterceptor implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(FeignContextInterceptor.class);
private static final String HEADER_AUTHORIZATION = "Authorization";
private static final String HEADER_REQUEST_ID = "X-Request-Id";
private static final String HEADER_REFERER = "X-Referer";
private static final String HEADER_REFERER_HOST = "X-Referer-Host";
private static final String ATTR_REQUEST_ID = "requestId";
private final String applicationName;
public FeignContextInterceptor(String applicationName) {
this.applicationName = applicationName;
}
@Override
public void apply(RequestTemplate template) {
// 1. 透传认证Token
String token = getAuthorizationToken();
if (StringUtils.isNotBlank(token)) {
template.header(HEADER_AUTHORIZATION, token);
log.debug("透传认证Token到Feign请求");
}
// 2. 透传链路追踪ID
String requestId = getRequestId();
template.header(HEADER_REQUEST_ID, requestId);
// 3. 设置调用来源服务名(如果下游请求中没有)
Collection<String> refererValues = template.headers().get(HEADER_REFERER);
if (CollectionUtils.isEmpty(refererValues)) {
template.header(HEADER_REFERER, applicationName);
}
// 4. 设置调用来源主机名
Collection<String> hostValues = template.headers().get(HEADER_REFERER_HOST);
if (CollectionUtils.isEmpty(hostValues)) {
template.header(HEADER_REFERER_HOST, getHostName());
}
}
private String getAuthorizationToken() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
ServletRequestAttributes servletAttributes = (ServletRequestAttributes) attributes;
HttpServletRequest request = servletAttributes.getRequest();
String token = request.getHeader(HEADER_AUTHORIZATION);
if (StringUtils.isNotBlank(token)) {
try {
return URLDecoder.decode(token, "UTF-8");
} catch (Exception e) {
log.error("Token URL解码失败", e);
}
}
return null;
}
private String getRequestId() {
String requestId = null;
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes != null) {
ServletRequestAttributes servletAttributes = (ServletRequestAttributes) attributes;
HttpServletRequest request = servletAttributes.getRequest();
Object attr = request.getAttribute(ATTR_REQUEST_ID);
if (attr != null) {
requestId = attr.toString();
}
}
// 兜底生成新的requestId
return Optional.ofNullable(requestId)
.filter(StringUtils::isNotBlank)
.orElseGet(() -> UUID.randomUUID().toString().replace("-", ""));
}
private String getHostName() {
try {
return java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
return "unknown";
}
}
}
4.4 HystrixContextConfiguration(Hystrix 上下文配置)
package com.example.feign;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
/**
* Hystrix上下文传递配置.
* 注册自定义并发策略,解决Hystrix线程隔离导致RequestContext丢失的问题.
*/
@Configuration
public class HystrixContextConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
/**
* 初始化时注册自定义并发策略.
* 需要先保存再重置再注册,因为Hystrix只允许注册一次.
*/
@PostConstruct
public void init() {
// 保存现有的Hystrix插件配置
HystrixEventNotifier eventNotifier =
HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher =
HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
// 重置后重新注册(Hystrix限制只能注册一次)
HystrixPlugins.reset();
// 注册自定义并发策略
HystrixPlugins.getInstance().registerConcurrencyStrategy(
new HystrixContextConcurrencyStrategy(existingConcurrencyStrategy));
// 恢复其他插件注册
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
4.5 HystrixContextConcurrencyStrategy(自定义并发策略)
package com.example.feign;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
* 自定义Hystrix并发策略.
* 核心作用:在主线程和Hystrix线程池子线程之间传递RequestContext.
*
* 原理:
* 1. wrapCallable() 在主线程中被调用,此时捕获当前线程的上下文
* 2. 返回包装后的Callable,在子线程执行时恢复上下文
* 3. 执行完毕后清理子线程的上下文,避免线程池复用时上下文污染
*/
public class HystrixContextConcurrencyStrategy extends HystrixConcurrencyStrategy {
private final HystrixConcurrencyStrategy delegate;
public HystrixContextConcurrencyStrategy(HystrixConcurrencyStrategy delegate) {
this.delegate = delegate;
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
// 在主线程中捕获当前请求上下文
RequestAttributes requestAttributes =
RequestContextHolder.getRequestAttributes();
String requestId = getRequestIdFromContext();
// 返回包装后的Callable
return new ContextWrappedCallable<>(callable, requestAttributes, requestId);
}
// === 以下方法委托给已有策略或父类 ===
@Override
public ThreadPoolExecutor getThreadPool(
HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return delegate != null
? delegate.getThreadPool(threadPoolKey, threadPoolProperties)
: super.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return delegate != null
? delegate.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(
HystrixRequestVariableLifecycle<T> rv) {
return delegate != null
? delegate.getRequestVariable(rv)
: super.getRequestVariable(rv);
}
private String getRequestIdFromContext() {
try {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs != null) {
Object requestId = attrs.getAttribute("requestId",
RequestAttributes.SCOPE_REQUEST);
return requestId != null ? requestId.toString() : null;
}
} catch (Exception ignored) {
}
return null;
}
/**
* 包装后的Callable,在子线程执行时恢复主线程的上下文.
*/
private static class ContextWrappedCallable<T> implements Callable<T> {
private final Callable<T> delegate;
private final RequestAttributes requestAttributes;
private final String requestId;
ContextWrappedCallable(Callable<T> delegate,
RequestAttributes requestAttributes,
String requestId) {
this.delegate = delegate;
this.requestAttributes = requestAttributes;
this.requestId = requestId;
}
@Override
public T call() throws Exception {
try {
// 恢复上下文到当前子线程
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
// 执行实际的Feign调用
return delegate.call();
} finally {
// 清理子线程上下文,防止线程池复用时污染
RequestContextHolder.resetRequestAttributes();
}
}
}
}
4.6 pom.xml(Starter 侧)
<project>
<groupId>com.example</groupId>
<artifactId>example-feign-context-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!-- Feign 核心(provided,由引入方提供) -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Web(获取RequestContext) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hystrix(provided,由引入方提供) -->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<scope>provided</scope>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
五、引入方使用
5.1 添加依赖
<dependency>
<groupId>com.example</groupId>
<artifactId>example-feign-context-starter</artifactId>
<version>1.0.0</version>
</dependency>
5.2 效果验证
无需任何配置,Feign 调用自动携带:
Request Headers:
Authorization: Bearer eyJhbGciOiJSUzI1NiI...
X-Request-Id: a3f8b2c1d4e5f6a7b8c9d0e1
X-Referer: order-service
X-Referer-Host: order-service-pod-abc123
5.3 下游服务接收
@RestController
public class StockController {
@GetMapping("/api/stock/query")
public Result query(HttpServletRequest request) {
// 自动从Header中获取,无需手动传递
String token = request.getHeader("Authorization"); // 上游的JWT
String requestId = request.getHeader("X-Request-Id"); // 链路追踪ID
String referer = request.getHeader("X-Referer"); // 谁调用的我
// ...
}
}
六、关键设计总结
| 设计要点 | 实现方式 | 收益 |
|---|---|---|
| 零侵入 | spring.factories 自动装配 | 引入依赖即生效,业务代码无感知 |
| Token 透传 | Feign RequestInterceptor | 微服务间认证自动传递 |
| 链路追踪 | RequestId Header 注入 | 全链路日志关联排查 |
| 线程安全 | WrappedCallable + finally 清理 | 线程池复用不会上下文污染 |
| 兼容性 | 委托模式处理已有策略 | 不影响其他 Hystrix 插件 |
| 依赖最小化 | 全部 provided scope | 不引入额外传递性依赖 |
| 调用溯源 | Referer + Hostname | 快速定位是哪个服务、哪个实例发起的调用 |
更多推荐
所有评论(0)