跨服务传递ThreadLocal解决方案

   通常情况下,我们会将登录用户的相关信息,存放到threadLocal当中,以便于我们在代码中获取用户信息,但是threadLocal的数据只存在于当前请求线程中,对于分布式微服务场景,如何将threaLocal中的数据,进行跨服务传递,需要我们思考解决。

核心需要解决的两个问题是:

1.如何将当前服务的threadLocal数据传递给下游服务?

2.下游服务如何拿到上游服务的threadLocal信息并塞到当前线程的threadLocal当中?

  对于第一个问题,我们可以借助http Header,将threadLocal信息封装到header当中,传递给被调用的服务,微服务场景下,利用feign的拦截器,可以简化这种封装逻辑。自定义CustomFeignInterceptor 实现 RequestInterceptor,重写apply方法,将当前线程的threadLocal信息,封装到feign请求模板的header当中,需要注意的是,这种传递threadLocal的逻辑,是需要定义为全局逻辑,还是指定某个feignClient逻辑,如果定义为全局拦截逻辑,需要配合@Configuration,将CustomFeignInterceptor Bean注入到父容器中,参考代码如下:

@Slf4j
public class CustomFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        buildHead(requestTemplate);
        String headers = JSON.toJSONString(HttpContext.getHeaders());
        if (!"{}".equals(headers)) {
            requestTemplate.header(FocoConstants.HTTP_CONTEXT, headers);
        }
    }

    private void buildHead(RequestTemplate requestTemplate) {
        //传递loginUser参数
        PageParam pageParam = ThreadPagingUtil.get();
        String pageValue = null;
        if (pageParam != null) {
            pageValue = JSON.toJSONString(pageParam);
        }
        String loginContext = LoginContextHolder.get(true);
        if(!"{}".equals(loginContext)){
            requestTemplate
                    .header(LoginContextConstant.LOGIN_CONTEXT, loginContext);
        }
        FocoContextManager.setHeader((header,focoContext)->{
            if(!"{}".equals(focoContext)){
                requestTemplate.header(header,focoContext);
            }
        });
        requestTemplate
                .header(FocoConstants.ORIGINAL, FocoConstants.FEIGN_ORIGINAL)
                .header(FocoConstants.PAGE, pageValue);
    }
}

 

@Slf4j
@Configuration
public class FeignAutoConfiguration {
    /**
     * 自定义Feign请求拦截器
     */
    @Bean
    public CustomFeignInterceptor loginContextFeignInterceptor() {
        return new CustomFeignInterceptor();
    }
    /**
     * 自定义Feign 错误解码器
     */
    @Bean
    @ConditionalOnProperty(prefix = "feign.hystrix", name = FocoConstants.ENABLED,havingValue = "false",matchIfMissing = true)
    public FeignErrorDecoder feignErrorDecoder() {
        return new FeignErrorDecoder();
    }

    @Bean
    FeignExceptionHandler feignExceptionHandler(){
        return new FeignExceptionHandler();
    }
}

  对于第二个问题,可以自定义spring拦截器,获取请求头中的threadLocal信息,塞入当前线程的threadLocal中,即可完成跨服务的threadLocal传递,参考代码如下:

@Slf4j
public class LoginContextInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (request != null) {
            String loginContextStr = request.getHeader(LoginContextConstant.LOGIN_CONTEXT);
            if (StrUtil.isNotBlank(loginContextStr)) {
                LoginContextHolder.set(loginContextStr);
            }
            FocoContextManager.setLocal((focoContextHeader)-> request.getHeader(focoContextHeader));
            String headerMap = request.getHeader(FocoConstants.HTTP_CONTEXT);
            if(StrUtil.isNotBlank(headerMap)){
                HttpContext.setHeaders(JSON.parseObject(headerMap, Map.class));
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        LoginContextHolder.remove();
        HttpContext.cleanHeaders();
        FocoContextManager.remove();
    }

}

   需要注意的是,在请求结束后,需要remove线程中threadLocal数据,因为请求线程被tomcat回收后,不一定会立即销毁,如果不在请求结束后主动remove 线程中的threadLocal信息,可能会影响后续请求,拿到脏数据。

   总结,利用请求头传递threadLocal在实际生产中非常实用,除此之外,还有一些特殊场景,也可能需要考虑threadLocal的传递,例如MQ消息,发送接收时,可以将threadLocal信息放在消息头中,进行传递。类似这种解决方案基本是整个公司项目通用,建议将上述处理逻辑封装成starter,通过spirng的SPI机制,注入到目标服务

  

posted on 2023-03-02 19:44  Simpleeee  阅读(951)  评论(0编辑  收藏  举报