微服务中实现基于 Feign + Hystrix 的调用上下文透传 Starter 实践


一、解决的核心问题

微服务架构下,服务 A 调用服务 B 时面临两个关键问题:

  1. 认证信息丢失:前端请求带的 JWT Token 不会自动传递到下游服务
  2. 请求上下文丢失:Hystrix 线程池隔离模式下,子线程无法访问主线程的 ThreadLocal(RequestAttributes、链路追踪 ID 等)

本 Starter 通过 Feign 拦截器 + Hystrix 自定义并发策略,实现上下文在微服务间的自动透传。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、涉及的技术知识点

2.1 Feign 请求拦截器

知识点说明
feign.RequestInterceptorFeign 提供的扩展点,在每次请求发送前执行
RequestTemplateFeign 请求模板,可动态添加 Header、Query 参数等
拦截器链多个 Interceptor 按注册顺序依次执行

2.2 Hystrix 线程池隔离与上下文传递

知识点说明
Hystrix 线程隔离Feign 调用默认在 Hystrix 管理的独立线程池中执行,与主线程隔离
HystrixConcurrencyStrategyHystrix 提供的并发策略扩展点,可自定义线程创建和任务包装行为
wrapCallable()在任务提交到线程池前包装 Callable,实现上下文搬运
HystrixPluginsHystrix 全局插件注册中心,支持注册自定义策略
ThreadLocal 跨线程主线程捕获上下文 → 包装到 Callable → 子线程执行时恢复

2.3 Spring Web 请求上下文

知识点说明
RequestContextHolderSpring 提供的请求上下文持有者,底层是 ThreadLocal
RequestAttributes封装了 HttpServletRequest 的属性信息
RequestContextListener确保非 DispatcherServlet 管理的请求也能使用 RequestContext

2.4 Spring Boot 自动配置

知识点说明
spring.factoriesSPI 机制声明自动配置入口
@Configuration + @Bean声明式注册组件
@Value注入配置项(如项目名称)
@PostConstructBean 初始化后执行 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 封装步骤

  1. 识别通用需求 — 多个微服务都需要透传 Token 和链路 ID
  2. 定义扩展点 — Feign 拦截器处理 Header 注入,Hystrix 策略处理线程切换
  3. 实现核心逻辑 — 主线程捕获上下文,子线程恢复上下文
  4. 自动配置 — spring.factories 声明,引入即生效
  5. 打包发布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快速定位是哪个服务、哪个实例发起的调用

更多推荐