网关、服务 userId 透传
微服务里一个请求要经过网关 → A 服务 → A 调 B 服务。userId 怎么从头传到尾?为什么 Feign 调用时 Header 是空的?怎么补上?
先搞清楚角色关系
任何一个微服务请求都经过这几步:
#mermaid-svg-53jPA8cVj5pY0sIB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-53jPA8cVj5pY0sIB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-53jPA8cVj5pY0sIB .error-icon{fill:#552222;}#mermaid-svg-53jPA8cVj5pY0sIB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-53jPA8cVj5pY0sIB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .marker.cross{stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-53jPA8cVj5pY0sIB p{margin:0;}#mermaid-svg-53jPA8cVj5pY0sIB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label text{fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label span{color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label span p{background-color:transparent;}#mermaid-svg-53jPA8cVj5pY0sIB .label text,#mermaid-svg-53jPA8cVj5pY0sIB span{fill:#333;color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .node rect,#mermaid-svg-53jPA8cVj5pY0sIB .node circle,#mermaid-svg-53jPA8cVj5pY0sIB .node ellipse,#mermaid-svg-53jPA8cVj5pY0sIB .node polygon,#mermaid-svg-53jPA8cVj5pY0sIB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .rough-node .label text,#mermaid-svg-53jPA8cVj5pY0sIB .node .label text,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label,#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label{text-anchor:middle;}#mermaid-svg-53jPA8cVj5pY0sIB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .rough-node .label,#mermaid-svg-53jPA8cVj5pY0sIB .node .label,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label,#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label{text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .node.clickable{cursor:pointer;}#mermaid-svg-53jPA8cVj5pY0sIB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .arrowheadPath{fill:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-53jPA8cVj5pY0sIB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-53jPA8cVj5pY0sIB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster text{fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster span{color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-53jPA8cVj5pY0sIB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB rect.text{fill:none;stroke-width:0;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape p,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label rect,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-53jPA8cVj5pY0sIB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-53jPA8cVj5pY0sIB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 A
Header: userId = 10001
Feign 调用
新请求 B
Header 空的
客户端
Gateway(网关)
鉴权通过后,把 userId 塞进 Header
转发给下游
服务 A
Filter 从 Header 读 userId → 写入 TTL
内部 Controller → Service → DAO 随时可拿
⚠️ Feign 发起全新 HTTP 请求
这是新的 TCP 连接,Header 是空的!
网关只给 A 加了 Header
这个新请求没人给它加
服务 B
Filter 从 Header 读 userId
空的 → TTL 也是空的 → 链路断了
Feign 调用是通过 HTTP 协议向另一个服务发起新的请求,这个请求跟网关转发过来的那个请求没有关系------它是一条新的 TCP 连接,Header 是空的。而 GatewayFilter 只负责给经由它转发的请求加 Header。
三个组件各管一段,组成一条接力链:
| 组件 | 位置 | 干了什么 | 没有它会怎样 |
|---|---|---|---|
| Gateway Filter | 网关 | 鉴权通过后,把 userId 塞进 Header,转发给下游 | 请求进到服务 A 时 Header 里没有 userId,谁都不知道是谁 |
| Context Filter | 每个服务内部 | 请求进来时从 Header 读 userId → 存入 TTL | Controller/Service/DAO 到处都得传 userId 参数 |
| Feign Interceptor | 服务 A 调 B 时 | 从 TTL 读 userId → 塞进 Feign 新请求的 Header | 服务 B 的 Header 是空的,链路中断 |
需要引入的依赖
三个组件涉及四个依赖,各管各的:
| 依赖 | 用途 | 哪个组件在用 |
|---|---|---|
spring-cloud-starter-gateway |
Gateway Filter | 网关模块 |
spring-boot-starter-web |
OncePerRequestFilter、FilterRegistrationBean |
Context Filter(每个服务) |
feign-core |
RequestInterceptor 接口 |
Feign Interceptor(每个服务) |
transmittable-thread-local |
TTL,跨线程池传递 userId | Context(每个服务) |
网关模块的 pom.xml:
xml
<!-- Gateway 依赖(WebFlux 环境,不要同时引入 spring-boot-starter-web) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
每个业务服务的 pom.xml:
xml
<!-- Servlet 环境,提供 Filter 接口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Feign 核心包,提供 RequestInterceptor 接口 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<!-- 阿里 TTL:替代普通 ThreadLocal,线程池里也能传递 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>
注意:
spring-cloud-starter-gateway基于 WebFlux,和spring-boot-starter-web(基于 Servlet)不能放在同一个模块里,否则启动报错。网关模块只引 Gateway,业务模块只引 Web。
实际项目里可以把 Context Filter + TTL 容器 + Feign 拦截器抽成一个公共 starter(只要是请求到 A 服务这里我就用 Context Filter 取 Header,存入 TTL,这样 A 服务内部使用没问题,调用 B 服务时把 id 存入新的请求的 Heaer,下一个服务继续读取,存入 TTL...),业务模块只需要引这一个:
xml
<!-- 你自己的公共 starter -->
<dependency>
<groupId>com.your-company</groupId>
<artifactId>biz-context-starter</artifactId>
</dependency>
<!-- 另外还要引入 OpenFeign 的 starter 和你要调用的服务的 Feign 接口包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.your-company</groupId>
<artifactId>user-service-api</artifactId>
</dependency>
其中 biz-context-starter 内部把 feign-core 和 transmittable-thread-local 打包进去了,业务模块不用再单独声明。
第一段:Gateway Filter ------ 让所有下游服务知道是谁
网关是整个系统的唯一入口。不管前端请求最终被路由到哪个服务,都要先过网关。所以在网关里把 userId 放进 Header,是最合理的位置。
java
@Component
@Order(-99)
public class AddUserId2HeaderFilter implements GlobalFilter {
private static final String HEADER_USER_ID = "userId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从鉴权框架获取当前登录的 userId(这里以 Sa-Token 为例)
Long userId = getLoginUserId();
if (userId == null) {
return chain.filter(exchange);
}
// 把 userId 塞进 Header,所有下游服务都能读到
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.headers(headers -> {
headers.remove(HEADER_USER_ID); // 防止前端伪造
headers.add(HEADER_USER_ID, String.valueOf(userId));
}))
.build();
return chain.filter(newExchange);
}
}
三个注意点:
headers.remove(HEADER_USER_ID)------前端可能在请求里自己塞了一个假的 userId,先删再写,防止伪造exchange.mutate()------Gateway 的请求对象不可变,只能创建新的 Exchange 往下传- 不管鉴权框架用的是 Sa-Token、Spring Security 还是自研的,思路都一样:鉴权通过后拿到 userId → 塞 Header
第二段:Context Filter ------ 让服务内部随时随地拿到 userId
请求进了某个服务,Header 里已经有网关放进去的 userId 了。但如果 Controller 每次从 HttpServletRequest 取、Service 再从参数传、DAO 再从参数传------太蠢,而且跟 HTTP 耦合。
在 Filter 里把 Header 的值取出来,存进 TTL。后续整个请求链路(Controller → Service → DAO)直接从 TTL 拿,就像拿一个全局变量。
java
// Filter:请求进来读 Header 写 TTL,请求结束清 TTL
@Slf4j
public class UserContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String userId = request.getHeader("userId");
if (StringUtils.isBlank(userId)) {
chain.doFilter(request, response);
return;
}
// 写入 TTL
UserContext.setUserId(userId);
try {
chain.doFilter(request, response);
} finally {
UserContext.remove(); // 请求结束必须清,否则内存泄漏 + 数据串了
}
}
}
java
// TTL 容器------业务代码唯一需要接触的类
public class UserContext {
// 为什么是 TTL 而不是普通 ThreadLocal?见后文
private static final ThreadLocal<Map<String, Object>> CONTEXT
= TransmittableThreadLocal.withInitial(HashMap::new);
public static void setUserId(Object value) {
CONTEXT.get().put("userId", value);
}
public static Long getUserId() {
Object value = CONTEXT.get().get("userId");
if (value == null) { return null; }
return Long.valueOf(value.toString());
}
public static void remove() {
CONTEXT.remove();
}
}
这个 Filter 用 Spring Boot 自动配置注册,打成 starter,业务模块引入依赖即可:
java
@AutoConfiguration
public class UserContextAutoConfiguration {
@Bean
public FilterRegistrationBean<UserContextFilter> userContextFilter() {
return new FilterRegistrationBean<>(new UserContextFilter());
}
}
到此为止,同一个服务内部 的 userId 传递已经解决了。业务代码任何地方直接 UserContext.getUserId()。
第三段:Feign Interceptor ------ 跨服务调用时把 userId 塞进新请求
一个服务的 Controller → Service → DAO 链路里,如果需要调另一个服务,通常是发一个 Feign 请求。
Feign 发的是一个全新的 HTTP 请求。新请求的 Header 是空的。 下游服务的 Context Filter 从 Header 读到 null → TTL 也是空的。
服务 B Feign(新 HTTP 请求) 服务 A 服务 B Feign(新 HTTP 请求) 服务 A #mermaid-svg-8OGA6uz88Q4nNBmk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8OGA6uz88Q4nNBmk .error-icon{fill:#552222;}#mermaid-svg-8OGA6uz88Q4nNBmk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8OGA6uz88Q4nNBmk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8OGA6uz88Q4nNBmk .marker.cross{stroke:#333333;}#mermaid-svg-8OGA6uz88Q4nNBmk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8OGA6uz88Q4nNBmk p{margin:0;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OGA6uz88Q4nNBmk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .sequenceNumber{fill:white;}#mermaid-svg-8OGA6uz88Q4nNBmk #sequencenumber{fill:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageText{fill:#333;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk .labelText,#mermaid-svg-8OGA6uz88Q4nNBmk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .loopText,#mermaid-svg-8OGA6uz88Q4nNBmk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OGA6uz88Q4nNBmk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8OGA6uz88Q4nNBmk .noteText,#mermaid-svg-8OGA6uz88Q4nNBmk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .actorPopupMenu{position:absolute;}#mermaid-svg-8OGA6uz88Q4nNBmk .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-man circle,#mermaid-svg-8OGA6uz88Q4nNBmk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8OGA6uz88Q4nNBmk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TTL 里有 userId=10001 ✅ 这是个新请求Header 是空的TTL 传不过去 Filter 读 Header → nullTTL 也是空的💥 不知道是谁在请求 userClient.getById(10001)GET /user/10001Header: (空)
所以需要一个 Feign 拦截器:在 Feign 发出请求之前,自己从当前线程的 TTL 里取出 userId,塞进去。
java
@Slf4j
public class UserContextFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUserId();
if (userId != null) {
// 把这个 userId 塞进 Feign 请求的 Header
requestTemplate.header("userId", String.valueOf(userId));
}
}
}
自动配置------全局生效:
java
@AutoConfiguration
public class FeignContextAutoConfiguration {
@Bean
public UserContextFeignInterceptor userContextFeignInterceptor() {
return new UserContextFeignInterceptor();
}
}
加上之后:
服务 B Feign(新 HTTP 请求) 服务 A 服务 B Feign(新 HTTP 请求) 服务 A #mermaid-svg-bG3Jgzt073nyaAMU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bG3Jgzt073nyaAMU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bG3Jgzt073nyaAMU .error-icon{fill:#552222;}#mermaid-svg-bG3Jgzt073nyaAMU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bG3Jgzt073nyaAMU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bG3Jgzt073nyaAMU .marker.cross{stroke:#333333;}#mermaid-svg-bG3Jgzt073nyaAMU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bG3Jgzt073nyaAMU p{margin:0;}#mermaid-svg-bG3Jgzt073nyaAMU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bG3Jgzt073nyaAMU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bG3Jgzt073nyaAMU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .sequenceNumber{fill:white;}#mermaid-svg-bG3Jgzt073nyaAMU #sequencenumber{fill:#333;}#mermaid-svg-bG3Jgzt073nyaAMU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .messageText{fill:#333;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU .labelText,#mermaid-svg-bG3Jgzt073nyaAMU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .loopText,#mermaid-svg-bG3Jgzt073nyaAMU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bG3Jgzt073nyaAMU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bG3Jgzt073nyaAMU .noteText,#mermaid-svg-bG3Jgzt073nyaAMU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .actorPopupMenu{position:absolute;}#mermaid-svg-bG3Jgzt073nyaAMU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bG3Jgzt073nyaAMU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU .actor-man circle,#mermaid-svg-bG3Jgzt073nyaAMU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bG3Jgzt073nyaAMU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TTL 里有 userId=10001 ✅ 拦截器从 TTL 读完写进 Header: userId=10001 Filter 读 Header → 写 TTL ✅后续链路都能拿到 userId userClient.getById(10001)GET /user/10001Header: userId=10001
完整链路串起来
服务 B 服务 A Gateway 客户端 服务 B 服务 A Gateway 客户端 #mermaid-svg-rwCjIvA1SEEHs0bh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rwCjIvA1SEEHs0bh .error-icon{fill:#552222;}#mermaid-svg-rwCjIvA1SEEHs0bh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rwCjIvA1SEEHs0bh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rwCjIvA1SEEHs0bh .marker.cross{stroke:#333333;}#mermaid-svg-rwCjIvA1SEEHs0bh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rwCjIvA1SEEHs0bh p{margin:0;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rwCjIvA1SEEHs0bh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .sequenceNumber{fill:white;}#mermaid-svg-rwCjIvA1SEEHs0bh #sequencenumber{fill:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageText{fill:#333;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh .labelText,#mermaid-svg-rwCjIvA1SEEHs0bh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .loopText,#mermaid-svg-rwCjIvA1SEEHs0bh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rwCjIvA1SEEHs0bh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-rwCjIvA1SEEHs0bh .noteText,#mermaid-svg-rwCjIvA1SEEHs0bh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .actorPopupMenu{position:absolute;}#mermaid-svg-rwCjIvA1SEEHs0bh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-man circle,#mermaid-svg-rwCjIvA1SEEHs0bh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-rwCjIvA1SEEHs0bh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① Gateway Filter鉴权 → userId=10001塞进 Header ② Context FilterHeader 读 userId → 写 TTL ③ 需要调服务 B ② Context FilterHeader 读 userId → 写 TTL 请求 + Authorization: Bearer xxx转发(Header: userId=10001)Controller → ServiceUserContext.getUserId() → 10001 ✅FeignRequestInterceptorTTL 读 userId → 写新请求 HeaderPOST /user/findByPhoneHeader: userId=10001Controller → ServiceUserContext.getUserId() → 10001 ✅响应响应响应
三段接力,各解决一个问题:
#mermaid-svg-pgVECdyzr77I50uE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pgVECdyzr77I50uE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pgVECdyzr77I50uE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pgVECdyzr77I50uE .error-icon{fill:#552222;}#mermaid-svg-pgVECdyzr77I50uE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pgVECdyzr77I50uE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE .marker.cross{stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pgVECdyzr77I50uE p{margin:0;}#mermaid-svg-pgVECdyzr77I50uE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label text{fill:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label span{color:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label span p{background-color:transparent;}#mermaid-svg-pgVECdyzr77I50uE .label text,#mermaid-svg-pgVECdyzr77I50uE span{fill:#333;color:#333;}#mermaid-svg-pgVECdyzr77I50uE .node rect,#mermaid-svg-pgVECdyzr77I50uE .node circle,#mermaid-svg-pgVECdyzr77I50uE .node ellipse,#mermaid-svg-pgVECdyzr77I50uE .node polygon,#mermaid-svg-pgVECdyzr77I50uE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .rough-node .label text,#mermaid-svg-pgVECdyzr77I50uE .node .label text,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label,#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label{text-anchor:middle;}#mermaid-svg-pgVECdyzr77I50uE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .rough-node .label,#mermaid-svg-pgVECdyzr77I50uE .node .label,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label,#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label{text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .node.clickable{cursor:pointer;}#mermaid-svg-pgVECdyzr77I50uE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE .arrowheadPath{fill:#333333;}#mermaid-svg-pgVECdyzr77I50uE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pgVECdyzr77I50uE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pgVECdyzr77I50uE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .cluster text{fill:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster span{color:#333;}#mermaid-svg-pgVECdyzr77I50uE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pgVECdyzr77I50uE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pgVECdyzr77I50uE rect.text{fill:none;stroke-width:0;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape,#mermaid-svg-pgVECdyzr77I50uE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape p,#mermaid-svg-pgVECdyzr77I50uE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label rect,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pgVECdyzr77I50uE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pgVECdyzr77I50uE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ Feign Interceptor:掏口袋盖章
服务 A 要调服务 B
Feign 发新请求,Header 是空的
从 TTL 掏出 userId
盖在新请求的 Header 上
下游服务又能读到
② Context Filter:进口袋
请求进入服务
从 Header 取出 userId
放进 TTL(内存口袋)
Controller/Service/DAO
随时能拿
① Gateway Filter:盖章
鉴权通过
Header 盖章:userId=10001
下游服务都能读到
口袋和章的比喻:
Gateway Filter:
所有人从公网进来,网关验明正身,在 Header 上盖个章"这是 10001"
Context Filter:
服务收到请求,从 Header 取下这个章,放进 TTL(服务内部的"口袋")
整个请求链路谁要谁从口袋拿,不用每次都从 Header 掏
Feign Interceptor:
服务调另一个服务,发的是新请求,Header 是白纸
从口袋掏出章,盖在新请求的 Header 上
→ 下游服务又能从 Header 取到 → 进口袋 → 循环继续
为什么用 TTL 而不是普通 ThreadLocal
服务内部如果用了线程池做异步任务,普通 ThreadLocal 在线程池里会丢值:
java
// 主线程 set 了
UserContext.setUserId(10001L);
// 线程池线程没被 set 过 → null
executor.submit(() -> {
Long id = UserContext.getUserId(); // null ❌
});
TransmittableThreadLocal(阿里开源)在每次提交任务时自动把值从父线程拷贝到线程池线程:
java
// 方式一:包装每个任务
executor.submit(TtlRunnable.get(() -> {
Long id = UserContext.getUserId(); // 10001 ✅
}));
// 方式二:包装线程池,一劳永逸
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);
如果项目里没用线程池,普通 ThreadLocal 也够。但微服务里很难保证永远不用------随便接个异步日志、消息队列、CompletableFuture,线程池就出现了。所以直接用 TTL,省得后面排查半天。
附:工程结构建议
把三个组件------Filter、TTL 容器、Feign 拦截器------抽成一个公共 starter。所有业务模块引入即可,不用在每个服务里重复写。
your-company-starters/
└── biz-context-starter/ ← 公共 starter
├── config/
│ ├── UserContextAutoConfiguration.java → 注册 Context Filter
│ └── FeignContextAutoConfiguration.java → 注册 Feign 拦截器
├── filter/
│ └── UserContextFilter.java → Header → TTL
├── context/
│ └── UserContext.java → TTL 容器
└── interceptor/
└── UserContextFeignInterceptor.java → TTL → Feign Header
业务模块只需要:
xml
<dependency>
<groupId>com.your-company</groupId>
<artifactId>biz-context-starter</artifactId>
</dependency>
java
@SpringBootApplication
@EnableFeignClients(basePackages = "com.your-company")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
启动类加 @EnableFeignClients 扫描所有 Feign 接口所在的包。