微服务中使用阿里开源的TTL,优雅的实现身份信息的线程间复用二
简单应用
在 Spring Security 往往需要存储用户登录的详细信息,这样在业务方法中能够随时获取用户的信息。
在前面的Spring Cloud Gateway整合OAuth2.0实现统一认证鉴权 文章中笔者是将用户信息直接存储在Request中,这样每次请求都能获取到对应的信息。
其实Request中的信息存储也是通过ThreadLocal完成的,在异步执行的时候还是需要重新转存,这样一来代码就变得复杂。
那么了解了TransmittableThreadLocal 之后,完全可以使用这个存储用户的登录信息,实现如下:
/**
* @author 公众号:码猿技术专栏
* @url: www.java-family.cn
* @description 使用TransmittableThreadLocal存储用户身份信息LoginVal
*/
public class SecurityContextHolder {
//使用TTL存储身份信息
private static final TransmittableThreadLocal<LoginVal> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void set(LoginVal loginVal){
THREAD_LOCAL.set(loginVal);
}
public static LoginVal get(){
return THREAD_LOCAL.get();
}
public static void remove(){
THREAD_LOCAL.remove();
}
}
由于mvc中的一次请求对应一个线程,因此只需要在拦截器中的设置和移除TransmittableThreadLocal中的信息,代码如下:
/**
* @author 公众号:码猿技术专栏
* @url: www.java-family.cn
* @description 拦截器,在preHandle中解析请求头的中的token信息,将其放入SecurityContextHolder中
* 在afterCompletion方法中移除对应的ThreadLocal中信息
* 确保每个请求的用户信息独立
*/
@Component
public class AuthInterceptor implements AsyncHandlerInterceptor {
/**
* 在执行controller方法之前将请求头中的token信息解析出来,放入SecurityContextHolder中(TransmittableThreadLocal)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod))
return true;
//获取请求头中的加密的用户信息
String token = request.getHeader(OAuthConstant.TOKEN_NAME);
if (StrUtil.isBlank(token))
return true;
//解密
String json = Base64.decodeStr(token);
//将json解析成LoginVal
LoginVal loginVal = TokenUtils.parseJsonToLoginVal(json);
//封装数据到ThreadLocal中
SecurityContextHolder.set(loginVal);
return true;
}
/**
* 在视图渲染之后执行,意味着一次请求结束,清除TTL中的身份信息
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){
SecurityContextHolder.remove();
}
}
原理
从定义来看,TransimittableThreadLocal继承于InheritableThreadLocal,并实现TtlCopier接口,它里面只有一个copy方法。所以主要是对InheritableThreadLocal的扩展。
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T>
在TransimittableThreadLocal中添加holder属性。这个属性的作用就是被标记为具备线程传递资格的对象都会被添加到这个对象中。
要标记一个类,比较容易想到的方式,就是给这个类新增一个Type字段,还有一个方法就是将具备这种类型的的对象都添加到一个静态全局集合中。之后使用时,这个集合里的所有值都具备这个标记。
// 1. holder本身是一个InheritableThreadLocal对象
// 2. 这个holder对象的value是WeakHashMap<TransmittableThreadLocal<Object>, ?>
// 2.1 WeekHashMap的value总是null,且不可能被使用。
// 2.2 WeekHasshMap支持value=null
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
}
/**
* 重写了childValue方法,实现上直接将父线程的属性作为子线程的本地变量对象。
*/
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
}
};
应用代码是通过TtlExecutors工具类对线程池对象进行包装。工具类只是简单的判断,输入的线程池是否已经被包装过、非空校验等,然后返回包装类ExecutorServiceTtlWrapper。根据不同的线程池类型,有不同和的包装类。
@Nullable
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService);
}
进入包装类ExecutorServiceTtlWrapper。可以注意到不论是通过ExecutorServiceTtlWrapper#submit方法或者是ExecutorTtlWrapper#execute方法,都会将线程对象包装成TtlCallable或者TtlRunnable,用于在真正执行run方法前做一些业务逻辑。
/**
* 在ExecutorServiceTtlWrapper实现submit方法
*/
@NonNull
@Override
public <T> Future<T> submit(@NonNull Callable<T> task) {
return executorService.submit(TtlCallable.get(task));
}
/**
* 在ExecutorTtlWrapper实现execute方法
*/
@Override
public void execute(@NonNull Runnable command) {
executor.execute(TtlRunnable.get(command));
}
文章转自公众号:码猿技术专栏