作者:鱼仔
博客首页: codeease.top
公众号:Java鱼仔
Bug回顾
前段时间组内遇到了一个现象十分奇怪的Bug,有人反馈自己的账号里竟然出现了其他人的数据,并且过了一段时间又看到了另外一个人的数据,定位到问题之后才发现,这是一起因为ThreadLocal而导致的Bug。
Bug模拟
代码逻辑
导致这起Bug的代码我做了精简,首先有个登陆认证的拦截器,在这个拦截器中,会将登陆人的信息存在一个ThreadLocal中,并且在存之前会先清空当前线程的数据。
在使用的时候,直接通过ThreadLocal就可以拿到用户信息,没有任何毛病。对应的代码如下
java
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author by: 神秘的鱼仔
* @ClassName: UserInterceptor
* @Description: 用户信息过滤器
* @Date: 2024/1/29 14:12
*/
@Service
public class UserInterceptor implements HandlerInterceptor {
public static ThreadLocal<String> userAccountThreadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
NoLogin noLoginAnnotation = handlerMethod.getMethod().getAnnotation(NoLogin.class);
if (noLoginAnnotation != null) {
// 不需要进行登录验证,直接通过
return true;
}
// 从请求中获取用户账号
String userAccount = request.getHeader("User-Account");
// 将用户账号存储到 ThreadLocal 中,先清空,再设置值
userAccountThreadLocal.remove();
userAccountThreadLocal.set(userAccount);
return true;
}
return true;
}
在上面的代码中,还有一个NoLogin的注解,这个注解的目的是:如果一个接口加了这个注解,就不需要认证
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogin {
}
最后通过一个拦截器,将上面定义的UserInterceptor注册进去
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author by: 神秘的鱼仔
* @ClassName: WebConfig
* @Description: 拦截器
* @Date: 2024/1/29 14:14
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor);
}
}
通过上面的配置之后,每一个接口在执行过程中都会进入到拦截器中,接着写一个简单的接口。
java
/**
*
* @author by: 神秘的鱼仔
* @ClassName: TestController
* @Description: 测试Controller
* @Date: 2023/6/2 10:49
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/testThreadLocal")
public String testThreadLocal(){
String account = UserInterceptor.userAccountThreadLocal.get();
return account;
}
}
在Postman中进行调用,并在Header信息中设置一下User-Account,当接口被调用时,可以看到用户信息被返回了出来,符合需求。
Bug产生
这个时候同事B也写了一个接口,这个接口不需要登陆认证,所以加了NoLogin注解,在拦截器里就不会存用户信息进去。在原来的预期中,因为没有用户信息,所以ThreadLocal拿不到值,就不会进入到获取用户信息的方法内。
java
@GetMapping("/testThreadLocal2")
@NoLogin
public String testThreadLocal2(){
return UserInterceptor.userAccountThreadLocal.get();
}
然后他在本地自己测试了几遍,确实拿不到用户的账号,于是就上线了,接着一个坑就被藏起来了,大家看出来这段代码中的问题了吗?
调用这个接口确实不会输出账号信息,但是如果我先多调用几次另外一个需要认证的接口,然后再多次调用这个接口,竟然也打印出了用户信息。
Bug分析
上面问题的罪魁祸首就是对ThreadLocal的不熟悉。我们来看看原因,ThreadLocal中存储了当前线程中的值,我们在调用SpringBoot的接口时是用tomcat的线程池去接收请求的。既然是线程池,就存在线程复用的情况。
现在假设调用localThreadTest1时,是a线程去处理,这个时候a线程的ThreadLocal中已经存在了用户信息,接着如果a线程去处理localThreadTest2的请求,因为这个接口是免登陆,虽然并没有在ThreadLocal中set值,但是也因为存在了之前留下来的数据导致了用户信息的错乱。

这个问题的解决也很简单,在使用完ThreadLocal之后一定要清空数据,然后就在拦截器中加了一个处理完成后的操作去清空ThreadLocal,而不是只在创建前去清空。
java
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author by: 神秘的鱼仔
* @ClassName: UserInterceptor
* @Description: 用户信息过滤器
* @Date: 2024/1/29 14:12
*/
@Service
public class UserInterceptor implements HandlerInterceptor {
public static ThreadLocal<String> userAccountThreadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
NoLogin noLoginAnnotation = handlerMethod.getMethod().getAnnotation(NoLogin.class);
if (noLoginAnnotation != null) {
// 不需要进行登录验证,直接通过
return true;
}
// 从请求中获取用户账号
String userAccount = request.getHeader("User-Account");
// 将用户账号存储到 ThreadLocal 中,先清空,再设置值
userAccountThreadLocal.remove();
userAccountThreadLocal.set(userAccount);
return true;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
userAccountThreadLocal.remove();
}
}