数据传递问题
0、前言
最近在开发的一个操作信息记录的接口,遇到了两个数据传递的问题,特此记录。
问题产生:该接口是一个异步接口且调用方和被调用方不在同一个服务内,在进行数据库保存操作时,出现了通用字段(创建人,修改人)未被正确赋值的问题。
初步排查,该问题的产生点有两个,1:服务间调用使用 OpenFeign,而 OpenFeign 在进行远程调用时会发起一个新的 http 请求,导致请求头中的信息丢失,2:异步接口是通过线程池的方式进行,而子线程通常情况不会持有父线程的上下文内容。
注:本文须进行传递的数据为 Cookie,进一步讲为存储在 LocalThread 的用户信息,该信息由 Cookie 解析而来。
1、OpenFeign 数据传递问题
处理这个问题相对简单,只需在新的请求中加入请求头信息即可。
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Cookie tokenCookie = CookieUtils.getCookie(ACCESS_TOKEN);
if (tokenCookie != null) {
String cookie = String.format("%s=%s", ACCESS_TOKEN, tokenCookie.getValue());
requestTemplate.header("Cookie", cookie);
}
}
};
}
2、父子线程数据传递问题
该问题比较棘手,笔者尝试过许多种方式,最终决定使用 Alibaba 的 TransmittableThreadLocal 来解决问题,即将存储用户信息的 LocalThread 换为 TransmittableThreadLocal 。
// 前
private static final ThreadLocal<UserInfo> SYS_USER_THREAD_LOCAL = new ThreadLocal<>();
// 后
private static final TransmittableThreadLocal<UserInfo> SYS_USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
以下篇幅则记录笔者的尝试方案。
大前提:笔者不希望存储用户信息的工具类直接对外提供 set 方法来设置用户信息。
第一种思路:类似于解决第一类问题,直接将 Cookie 信息传递到子线程,该尝试使用了线程池的任务装饰器(PS:线程池提供了 setTaskDecorator 方法,允许用户重新设置装饰器)。
该方法存在一个致命问题,即 RequestAttributes 会在父线程返回时清空,而此方法又是浅拷贝,子线程获取到信息又会重新丢失,并且 RequestAttributes 并没有实现序列化接口(Serializable)和可克隆接口(Cloneable),这又大大增加了重写深拷贝方法的复杂度,因而不使用。
/**
* 上下文拷贝装饰器
*/
static class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 在此处获取你需要的线程上下文信息
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
// 在新的线程中设置线程上下文信息
try {
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
第二种思路,即将不能线程共享的 LocalThread 换成可以"线程共享"的 TransmittableThreadLocal,该方法成功解决了此问题(PS:TransmittableThreadLocal 是 Alibaba 开源的一个 Java 库,它是对 Java 的 InheritableThreadLocal 类的增强版本,主要用于在使用线程池等会复用线程的场景下,传递线程本地变量)。
补充:可能会有学者想到使用 InheritableThreadLocal 实现上下文信息在父子线程的传递,但该选择有个问题,即这里使用的是线程池,线程池会复用线程,而 InheritableThreadLocal 实现上下文传递的触发点在于父线程创建子线程时(PS:上下文会通过继承的方式,从父线程传递到子线程)。