SpringBoot中SimpleThreadScope无法在Filter与RestController间传递对象的问题
这个问题我之前也遇到过,核心原因其实和Spring的作用域Bean代理机制、Filter与Controller的执行上下文关系有关,下面给你拆解清楚:
核心原因分析
1. 你可能没通过Spring容器管理线程作用域Bean的存储
如果在Filter里直接new了线程作用域Bean(比如LanguageContext context = new LanguageContext()),然后自己存到自定义的ThreadLocal里,那@RestController里通过@Autowired拿到的是Spring代理的Bean——这个代理是从Spring管理的SimpleThreadScope内部ThreadLocal中取实例的,和你自己维护的ThreadLocal完全是两个独立的存储,自然拿不到Filter里设置的值。
2. 线程作用域Bean没配置代理模式
Spring的@RestController默认是单例的,如果你给线程作用域Bean只加了@Scope("thread"),没加proxyMode = ScopedProxyMode.TARGET_CLASS,那么Spring在初始化单例Controller的时候,就会提前创建一个线程作用域Bean的实例(此时是Tomcat启动线程的实例),之后所有请求进来,Controller都会复用这个旧实例,而Filter里设置的是当前请求线程的新实例,自然无法传递。
3. (次要)线程复用的潜在问题
SimpleThreadScope是基于ThreadLocal实现的,而Tomcat这类Web容器会复用线程池中的线程。如果请求结束后没手动清理ThreadLocal中的数据,下一个请求复用线程时可能拿到残留数据,但这不是你遇到的“创建新实例”的直接原因。
解决方案
步骤1:正确注册SimpleThreadScope并配置Bean
首先要把SimpleThreadScope注册到Spring容器,同时给线程作用域Bean加上代理模式:
@Configuration public class ThreadScopeConfig { @Bean public static CustomScopeConfigurer threadScopeConfigurer() { CustomScopeConfigurer configurer = new CustomScopeConfigurer(); configurer.addScope("thread", new SimpleThreadScope()); return configurer; } } @Component @Scope(value = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS) public class LanguageContext { private String language; // getter和setter方法 }
步骤2:在Filter中通过Spring上下文获取Bean并设置值
GenericFilterBean可以直接获取Spring的Web上下文,通过上下文拿到线程作用域Bean再设置语言信息:
public class LanguageFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String lang = httpRequest.getHeader("Accept-Language"); // 从Spring上下文获取线程作用域Bean,而不是自己new LanguageContext languageContext = getWebApplicationContext().getBean(LanguageContext.class); languageContext.setLanguage(lang); try { chain.doFilter(request, response); } finally { // 手动清理,避免线程复用导致数据残留 languageContext.setLanguage(null); } } }
步骤3:在Controller和Service中直接注入使用
此时@RestController和@Service里的@Autowired就能拿到当前线程的正确实例了:
@RestController @RequestMapping("/demo") public class DemoController { @Autowired private LanguageContext languageContext; @GetMapping public String getLang() { return "Controller获取的语言:" + languageContext.getLanguage(); } } @Service public class DemoService { @Autowired private LanguageContext languageContext; public void printLang() { System.out.println("Service获取的语言:" + languageContext.getLanguage()); } }
额外建议:用RequestScope替代SimpleThreadScope
如果你的需求是每个请求独立的语言上下文,其实Spring自带的RequestScope更合适:它是基于请求绑定的,不管线程是否复用,每个请求都会有独立的实例,而且Spring会在请求结束后自动清理数据,不需要手动处理线程残留问题。
只需要把LanguageContext的作用域改成请求作用域即可:
@Component @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public class LanguageContext { // ... 代码不变 }
内容的提问来源于stack exchange,提问作者Anh Nguyen tuan

