ThreadLocal有时挺好用的,但是在某些情况下需要将父线程中的本地变量传递给子线程时,这个就不行了。
那么TransmittableThreadLocal就是为了解决这个问题的,但是,如果在某些异步的场景中,特别是异步线程是下载文件等耗费时长比较长的场景中,父线程结束了,那么在父线程清掉了本地变量的情况下(如果不清,可能会引起内存泄露),子线程中还需要用到父线程带过来的本地变量(比如说用户信息等),怎么办?
这里有个解决的办法。
1.首先写一个Controller类
这里主要就是模拟主线程,从CommonUtils(CommonUtils中的内容后面有)中塞入的可传递的本地线程变量中获取请求头中的empNo,然后将整个线程变量对象传递给Service中的方法,由于Service中的处理方法是异步的,就是传递给异步子线程。
package com.zte.csc;
import com.zte.csc.base.util.CommonUtils;
import com.zte.springbootframe.common.model.ServiceData;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* TransmittableThreadLocal线程变量传递问题
* @author: xiaohuihui
* @date: 2024/12/22
*/
@RestController("testTTL")
@RequestMapping("/testTTLService")
@Slf4j
public class TestTTL {
@Autowired
private TestTTLService testTTLService;
@ApiOperation("测试TTL-Get方式")
@RequestMapping(value = "/testTTL", method = RequestMethod.GET, produces = "application/json")
public ServiceData queryTechnicalAdviceList() throws Exception {
ServiceData sd = new ServiceData();
try {
String empNo = CommonUtils.getEmpNo();
log.info("主线程中获取的empNo:{}", empNo);
// 模拟异步长时间处理,传入TTL变量
testTTLService.asyncProcess(CommonUtils.getSysGlobalConstVo());
log.info("主线程处理结束");
} catch (Exception e) {
log.error("异常:{}", e.getMessage(), e);
}
sd.setCode(ServiceData.RetCode.Success);
sd.setBo("返回成功");
return sd;
}
}
2.然后写Service方法
Service中的方法为异步方式,入参就是父线程传递过来的全局线程变量SysGlobalConstVo。
这里主要验证几个事情:
- 子线程中如果模拟长时间操作,休眠线程10s,发现子线程中获取不到全局线程变量的值了(因为主线程已经回收了)
- 传递过来的全局线程变量的对象,如果在子线程中又重新放入全局线程变量,那么在子线程中后面的操作中也是可以获取到这个全局线程变量的值的
- 子线程中就算再加子线程操作,在下面的子线程中同样能拿到这个子线程塞进去的全局线程变量的值
这里有个注意的点:
子线程在重新塞了全局线程变量后,最后要remove,防止内存泄露。
package com.zte.csc;
import com.google.common.collect.Lists;
import com.zte.csc.base.util.CommonUtils;
import com.zte.springbootframe.common.consts.SysGlobalConstVo;
import com.zte.springbootframe.common.exception.BusiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* TransmittableThreadLocal线程变量传递问题
* @author: xiaohuihui
* @date: 2024/12/22
*/
@Service
@Slf4j
public class TestTTLService {
@Async
public void asyncProcess(SysGlobalConstVo sysGlobalConstVo) throws BusiException {
// 获取父线程传递的empNo
log.info("子线程中获取到的父线程传递的empNo:{}", sysGlobalConstVo.getXEmpNo());
try {
// 模拟处理时间10s
Thread.sleep(10000);
} catch (InterruptedException e) {
log.error("异步处理线程异常:{}", e.getMessage(), e);
}
try {
// 后面的某个地方再获取empNo, 发现这里是取不到的
log.info("子线程中取到的empNo为:{}", CommonUtils.getEmpNo());
// 父线程结束后,线程变量被清空,子线程通过CommonUtils.getEmpNo()方法拿不到值了,重新塞进去
CommonUtils.setSysGlobalConstVo(sysGlobalConstVo);
// 然后其他地方就可以获取到了
log.info("重新塞值后子线程中取到的empNo为:{}", CommonUtils.getEmpNo());
// 继续模拟下面还有子线程的情况
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6);
list.parallelStream().forEach(item -> {
log.info("{}并发子线程中获取的empNo:{}", item, CommonUtils.getEmpNo());
}
);
throw new BusiException("0001" ,"特意抛出异常");
}catch (Exception e){
log.error("处理异常:{}", e.getMessage(), e);
throw e;
}finally {
// 最后还是要清空,防止内存泄露
CommonUtils.removeThreadLocal();
}
}
}
3.打印的结果
这里显示的是上面程序打印的结果,验证上面的逻辑。
2024-12-22 17:27:16.624 |-INFO [http-nio-8084-exec-5] com.zte.csc.base.interceptor.CommonHandlerInterceptorAdapter [70] -| [00292987-20241222172716-133] preHandle HTTP_HEADER_X_EMP_NO : 00292987; HTTP_HEADER_X_LANG_ID : null,URI : /zte-crm-notify-basicservice/testTTLService/testTTL
2024-12-22 17:27:16.676 |-INFO [http-nio-8084-exec-5] com.zte.csc.TestTTL [30] -| [00292987-20241222172716-133] 主线程中获取的empNo:00292987
2024-12-22 17:27:19.753 |-INFO [metricsThreadPoolTaskScheduler-1] com.zte.csc.TestTTLService [25] -| [system] 子线程中获取到的父线程传递的empNo:00292987
2024-12-22 17:27:18.746 |-INFO [http-nio-8084-exec-5] com.zte.csc.TestTTL [33] -| [00292987-20241222172716-133] 主线程处理结束
2024-12-22 17:27:19.928 |-INFO [http-nio-8084-exec-5] com.zte.csc.base.interceptor.CommonHandlerInterceptorAdapter [88] -| [00292987-20241222172716-133] afterCompletion HTTP_HEADER_X_EMP_NO : 00292987; HTTP_HEADER_X_LANG_ID : zh_CN,URI : /zte-crm-notify-basicservice/testTTLService/testTTL
2024-12-22 17:27:29.917 |-INFO [metricsThreadPoolTaskScheduler-1] com.zte.csc.TestTTLService [34] -| [system] 子线程中取到的empNo为:
2024-12-22 17:27:29.917 |-INFO [metricsThreadPoolTaskScheduler-1] com.zte.csc.TestTTLService [38] -| [system] 重新塞值后子线程中取到的empNo为:00292987
2024-12-22 17:27:29.918 |-INFO [metricsThreadPoolTaskScheduler-1] com.zte.csc.TestTTLService [42] -| [system] 4并发子线程中获取的empNo:00292987
2024-12-22 17:27:29.918 |-INFO [ForkJoinPool.commonPool-worker-7] com.zte.csc.TestTTLService [42] -| [system] 6并发子线程中获取的empNo:00292987
2024-12-22 17:27:29.918 |-INFO [ForkJoinPool.commonPool-worker-6] com.zte.csc.TestTTLService [42] -| [system] 2并发子线程中获取的empNo:00292987
2024-12-22 17:27:29.918 |-INFO [ForkJoinPool.commonPool-worker-0] com.zte.csc.TestTTLService [42] -| [system] 1并发子线程中获取的empNo:00292987
2024-12-22 17:27:29.918 |-INFO [ForkJoinPool.commonPool-worker-7] com.zte.csc.TestTTLService [42] -| [system] 5并发子线程中获取的empNo:00292987
2024-12-22 17:27:29.918 |-INFO [ForkJoinPool.commonPool-worker-6] com.zte.csc.TestTTLService [42] -| [system] 3并发子线程中获取的empNo:00292987
2024-12-22 17:27:30.957 |-ERROR [metricsThreadPoolTaskScheduler-1] com.zte.csc.TestTTLService [47] -| [system] 处理异常:特意抛出异常
com.zte.springbootframe.common.exception.BusiException: 特意抛出异常
at com.zte.csc.TestTTLService.asyncProcess(TestTTLService.java:45)
at com.zte.csc.TestTTLService$$FastClassBySpringCGLIB$$15698846.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:792)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
2024-12-22 17:27:42.074 |-ERROR [metricsThreadPoolTaskScheduler-1] org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler [39] -| [system] Unexpected exception occurred invoking async method: public void com.zte.csc.TestTTLService.asyncProcess(com.zte.springbootframe.common.consts.SysGlobalConstVo) throws com.zte.springbootframe.common.exception.BusiException
4.CommonUtils中的获取和设置本地变量的方法
这里用的就是可传递的线程变量:TransmittableThreadLocal
public class CommonUtils {
private static TransmittableThreadLocal<SysGlobalConstVo> sysGlobalInfoThreadLocal = new TransmittableThreadLocal();
private static TransmittableThreadLocal<UserInfoVO> userInfoVOThreadLocal = new TransmittableThreadLocal();
private static TransmittableThreadLocal<HttpServletRequest> httpServletRequestThreadLocal = new TransmittableThreadLocal();
private static TransmittableThreadLocal<HttpServletResponse> httpServletResponseThreadLocal = new TransmittableThreadLocal();
public CommonUtils() {
}
public static void setSysGlobalConstVo(SysGlobalConstVo sysGlobalConstVo) {
sysGlobalInfoThreadLocal.set(sysGlobalConstVo);
}
public static void setUserInfoVO(UserInfoVO userInfoVO) {
userInfoVOThreadLocal.set(userInfoVO);
}
public static void setHttpServletRequest(HttpServletRequest request) {
httpServletRequestThreadLocal.set(request);
}
public static void setHttpServletResponse(HttpServletResponse response) {
httpServletResponseThreadLocal.set(response);
}
public static void removeThreadLocal() {
sysGlobalInfoThreadLocal.remove();
userInfoVOThreadLocal.remove();
httpServletRequestThreadLocal.remove();
httpServletResponseThreadLocal.remove();
}
public static String getUserLanguage() {
return StringUtils.substring(getRequestLangId(), 0, 2);
}
public static HttpServletRequest getHttpServletRequest() {
return (HttpServletRequest)httpServletRequestThreadLocal.get();
}
public static HttpServletResponse getHttpServletResponse() {
return (HttpServletResponse)httpServletResponseThreadLocal.get();
}
public static UserInfoVO getUserInfoVO() {
return (UserInfoVO)userInfoVOThreadLocal.get();
}
public static SysGlobalConstVo getSysGlobalConstVo() {
return (SysGlobalConstVo)sysGlobalInfoThreadLocal.get();
}
public static String getUserId() {
return null == userInfoVOThreadLocal.get() ? "" : ((UserInfoVO)userInfoVOThreadLocal.get()).getGuid();
}
public static String getEmpNo() {
return null == userInfoVOThreadLocal.get() ? getRequestEmpNo() : ((UserInfoVO)userInfoVOThreadLocal.get()).getIdCardAb();
}
5.拦截器中对请求头中信息的处理及和线程变量的交互
拦截器中主要是前置处理方法和后置处理方法。
前置处理方法中,获取请求头中的请求参数,并通过CommonUtils.setSysGlobalConstVo(vo)方法设置进全局线程变量中;
后置处理方法中,主要是通过CommonUtils.removeThreadLocal()方法清除全局线程变量。
public class CommonHandlerInterceptorAdapter extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(CommonHandlerInterceptorAdapter.class);
private BiFunction<String, String, UserInfoVO> getUserFunction;
public CommonHandlerInterceptorAdapter() {
}
public void setGetUserFunction(BiFunction<String, String, UserInfoVO> getUserFunction) {
this.getUserFunction = getUserFunction;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
CommonUtils.setHttpServletRequest(request);
CommonUtils.setHttpServletResponse(response);
String xEmpNo = request.getHeader("X-Emp-No");
String xLangId = request.getHeader("X-Lang-Id");
String xAuthValue = request.getHeader("X-Auth-Value");
String xOrgId = request.getHeader("X-Org-Id");
String xOriginServiceName = request.getHeader("X-Origin-ServiceName");
String xTenantId = request.getHeader("X-Tenant-Id");
String xAppId = request.getHeader("X-App-Id");
String xTimestamp = request.getHeader("X-Timestamp");
String xEncryptedSign = request.getHeader("X-Encrypted-Sign");
String xTraceId = MyLogUtils.getOrGenerateTraceId(request);
SysGlobalConstVo vo = new SysGlobalConstVo();
vo.setXEmpNo(xEmpNo);
vo.setXLangId(xLangId);
vo.setXAuthValue(xAuthValue);
vo.setXOrgId(xOrgId);
vo.setXOriginServiceName(xOriginServiceName);
vo.setXTenantId(xTenantId);
vo.setXAppId(xAppId);
vo.setXTimestamp(xTimestamp);
vo.setXEncryptedSign(xEncryptedSign);
vo.setXTraceId(xTraceId);
CommonUtils.setSysGlobalConstVo(vo);
MDC.put("trace_id", xTraceId);
response.setHeader("X-Trace-Id", StringEscapeUtils.escapeHtml4(xTraceId));
log.info("preHandle HTTP_HEADER_X_EMP_NO : {}; HTTP_HEADER_X_LANG_ID : {},URI : {}", new Object[]{xEmpNo, xLangId, request.getRequestURI()});
if (this.getUserFunction != null && StringUtils.isNotBlank(xEmpNo)) {
String langId = StringUtils.startsWithIgnoreCase(xLangId, "en") ? "en" : "zh";
try {
UserInfoVO userInfoVO = (UserInfoVO)this.getUserFunction.apply(xEmpNo, langId);
CommonUtils.setUserInfoVO(userInfoVO);
} catch (Exception var17) {
log.error(xEmpNo + "queryUserByIdCardAb error:" + var17.getMessage());
}
}
return super.preHandle(request, response, handler);
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion HTTP_HEADER_X_EMP_NO : {}; HTTP_HEADER_X_LANG_ID : {},URI : {}", new Object[]{CommonUtils.getRequestEmpNo(), CommonUtils.getRequestLangId(), request.getRequestURI()});
CommonUtils.removeThreadLocal();
MDC.remove("trace_id");
super.afterCompletion(request, response, handler, ex);
}