大家好,我是飘渺。
在上一篇文章中,我们解决了网关层认证后向后端服务传递用户信息的问题。今天我们来解决另外一个问题:如何在 OpenFeign 中传递 Token,并且保证多线程情况下也能适用。
这是DDD&微服务系列文章的第34篇,欢迎持续关注!
为了方便演示,首先定义一个接口,在接口中通过 Feign 调用其他服务:
@Operation(summary = "用户测试接口") @GetMapping("/api/pd/customer/info") public String info() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign调用方获取当前登录用户:" + currentUser); //通过feign调用远程服务 String info = experimentClient.info(); log.info("远程获取用户:" + info); return currentUser; }
然后在远程接口中通过上文定义的UserContextHolder对象获取用户信息:
@GetMapping("/api/pd/experiment/info") public String userInfo() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign被调用方获取userToken : {} ",currentUser); return currentUser == null ? "" : currentUser; }
图片
通过调用结果可知,当使用OpenFeign调用远程服务时,接口是无法获取到用户 ID 的。
在使用OpenFeign请求其他服务接口时,默认不携带header信息,这样就导致无法携带登录用户信息。常规情况下,我们只需要在使用 OpenFeign 调用时先从 Header 获取 Token 信息,放入新请求即可,在项目中可以定义一个OpenFeign的拦截器来实现此功能,代码如下所示:
public class FeignRequestConfiguration { @Bean public RequestInterceptor requestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; // 当主线程的请求执行完毕后,Servlet容器会被销毁当前的Servlet,因此在这里需要做判空 if (attributes != null) { HttpServletRequest request = attributes.getRequest(); // 获取userId 并传递 userId String userId = request.getHeader(CommonConstant.X_CLIENT_TOKEN); if (StringUtils.hasText(userId)) { template.header(CommonConstant.X_CLIENT_TOKEN, userId); } } } }; } }
经过上述配置以后再次调用即可在 Feign 接口中也获取到用户ID,如下图所示:
图片
上面是单线程的情况,假如我们在当前线程中又开启了子线程去进行 Feign 调用,那么是无法从 RequestContextHolder 获取到 Header 的。测试代码如下:
public String info() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign调用方获取当前登录用户:" + currentUser); CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor); String info = ""; try{ info = infoFuture.get(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } log.info("远程获取用户:" + info); return currentUser; }
在上述代码中,通过 CompletableFuture 开启异步线程去调用 experimentClient ,可以发现此时无法获取到用户信息,效果如下所示:
图片
出现上述问题的原因是,RequestContextHolder.getRequestAttributes() 方法里面使用的一个 ThreadLocal,默认不是线程共享的,源码如下:
public static RequestAttributes getRequestAttributes() { RequestAttributes attributes = requestAttributesHolder.get(); if (attributes == null) { attributes = inheritableRequestAttributesHolder.get(); } return attributes; }
所以主线程调用子线程时,无法获取到主线程请求里面的 RequestAttributes。
原因已经清楚了,继续观察 RequestContextHolder.getRequestAttributes() 方法源码,注意到如果当前线程拿不到 RequestAttributes ,它会从 inheritableRequestAttributesHolder 里面拿,再仔细观察发现源码设置 RequestAttributes 到 ThreadLocal 的时候有这样一个重载方法。
/** * 给当前线程绑定属性 * @param inheritable 是否要将属性暴露给子线程 */public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) { ......}
这看起来符合我们的要求,只需要在主线程调用其他线程前将 RequestAttributes 对象设置为子线程共享,就能把 Header 等信息传递下去。
所以,在异步调用 Feign 接口时添加如下代码即可:
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor);......
再次执行发现,是可以获取到 userId 的。
这里使用CompletableFuture异步调用时需要使用自定义线程池,而不能使用默认线程池ForkJoinPool,这是为什么呢?
虽然可以在异步调用时设置 RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); 可以实现请求头透传,但是每次调用都需要加上这一句,实现上还略显麻烦。
并且我们知道了获取不到请求头的原因是子线程无法获取主线程的 header 属性,那么我们只需要定义一个数据结构,使用 InheritableThreadLocal 在内存中保存一份 header 属性即可。在上篇文章中通过网关进行 UserID 透传时我们是使用 ThreadLocal 保存数据,现在只需要将其换成 InheritableThreadLocal,同时在 RequestInterceptor#apply() 方法中不再通过请求头获取而是直接从 InheritableThreadLocal 中获取数据。
实现过程如下:
1、重命名并修改数据结构:
首先,将 UserContextHolder 重命名为 RequestHeaderHolder,同时使用 InheritableThreadLocal 替换 ThreadLocal,以便子线程也能获取数据。
public class RequestHeaderHolder { private final ThreadLocal<Map<String,String>> REQUEST_HEADER_HOLDER; //使用InheritableThreadLocal,使得共享变量可被子线程继承 private RequestHeaderHolder() { this.REQUEST_HEADER_HOLDER = new InheritableThreadLocal<>() { @Override protected Map<String, String> initialValue() { return new HashMap<>(); } }; } public String getCurrentUser(){ return this.REQUEST_HEADER_HOLDER.get().get(CommonConstant.X_CLIENT_TOKEN); } ......}
2、修改请求拦截器:
将请求拦截器 UserTokenInterceptor 重命名为 RequestHeaderInterceptor,并将请求头放入 RequestHeaderHolder 中。
@Slf4jpublic class RequestHeaderInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { Enumeration<String> headerNames = request.getHeaderNames(); RequestHeaderHolder requestHeaderHolder = RequestHeaderHolder.getInstance(); //重新设置请求头 while (headerNames.hasMoreElements()){ String key = headerNames.nextElement(); requestHeaderHolder.set(key,request.getHeader(key)); } return true; } ......}
3、修改 Feign 配置类:在 FeignRequestConfiguration 中不再从 RequestContextHolder 获取数据,而是从 RequestHeaderHolder 获取数据。
@Slf4jpublic class FeignRequestConfiguration { @Bean public RequestInterceptor requestInterceptor(){ return template -> { Map<String, String> headerMap = RequestHeaderHolder.getInstance().get(); if(headerMap != null){ headerMap.forEach((key, value) -> { template.header(key, value); }); } }; }}
通过上面的改造,不管是同步调用还是子线程异步调用都可以直接通过RequestHeaderHolder.getInstance().getCurrentUser();获取用户信息,并且调用方无须做任何改动。
本文链接:http://www.28at.com/showinfo-26-79599-0.htmlSpringCloud微服务中Feign如何传递用户Token,并保证多线程环境也可适用?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 你的Css选择器可视化备忘录