Spring Boot Jersey应用线程安全问题排查求助
这个问题我之前也碰到过,核心原因其实是你对Spring和Jersey的Bean生命周期以及线程安全理解有个小误区,咱们一步步拆解:
问题根源:单例Controller共享请求级上下文
首先,Spring的@Component默认是单例模式,你用@Component标注的DocumentController会被Spring容器创建成一个全局唯一的实例,所有请求都会复用这个实例。
而你在Controller里把@Context ContainerRequestContext声明为成员变量,这就踩了线程安全的大坑:ContainerRequestContext是请求级别的对象,每个请求都有自己的上下文,但单例Controller的成员变量会被所有请求线程共享。高负载下,多个请求的上下文会互相覆盖,导致你拿到的User对象要么是其他请求的,要么因为上下文被覆盖成null而出现NPE。
你在Controller的getUserFromContext里做了断言没失败,可能是因为断言在单个线程测试时没问题,但高并发下线程切换快,断言执行时上下文还没被覆盖,到了Repository层时上下文已经被其他请求替换了,所以Repository里会出问题。
修复步骤:避免在单例Bean中持有请求级成员变量
最直接的修复是把ContainerRequestContext从成员变量改成方法参数注入,确保每个请求都拿到自己的上下文:
修改DocumentController代码
@Component @Path("/documents") public class DocumentController { @Autowired private DocumentRepository documentRepository; // 把ContainerRequestContext作为方法参数传入 private User getUserFromContext(@Context ContainerRequestContext context) { User user; try { user = (User) context.getProperty(User.PROPERTY_NAME); assert user != null; } catch (NullPointerException e) { throw new IllegalStateException("User in context is null!"); } catch (ClassCastException castE) { throw new IllegalStateException("Object in context is not User"); } return user; } @PUT @Path("invoices") @Consumes("application/json;charset=utf-8") @Produces("application/json;charset=utf-8") // 在业务方法中注入当前请求的ContainerRequestContext public Response saveInvoices(ArrayList<Invoice> arr, @Context ContainerRequestContext context) throws InternalErrorException { return arr != null ? Response.ok().entity(documentRepository.saveInvoices(getUserFromContext(context), arr)).build() : Response.notModified().build(); } }
检查所有Controller
别忘了检查其他所有Controller(比如UserController、ClientController等),只要是用@Component或@Path标注的单例Bean,都不能把ContainerRequestContext、HttpServletRequest这类请求级对象作为成员变量,必须用方法参数注入。
进阶优化:直接注入User对象(更简洁安全)
为了避免每次手动从上下文拿User,你可以封装一个Jersey的InjectableProvider,让框架直接把User注入到Controller方法中,减少出错概率:
1. 创建UserContextProvider
@Provider public class UserContextProvider implements InjectableProvider<Context, Type> { @Override public Injectable<User> getInjectable(ComponentContext cc, Context context, Type type) { // 只处理User类型的注入 if (type.equals(User.class)) { return new Injectable<User>() { // 这里用@Context注入当前请求的上下文 @Context private ContainerRequestContext requestContext; @Override public User getValue() { User user = (User) requestContext.getProperty(User.PROPERTY_NAME); if (user == null) { throw new IllegalStateException("User not authenticated or missing in context"); } return user; } }; } return null; } @Override public ComponentScope getScope() { // 声明为请求级 scope,确保每个请求拿到自己的User return ComponentScope.PerRequest; } }
2. 在SpringConfig中注册Provider
@SpringBootConfiguration @ApplicationPath("/MyServer") public class SpringConfig extends ResourceConfig { public SpringConfig() { register(AuthorizationRequestFilter.class); register(UserContextProvider.class); // 新增注册这个Provider register(UserController.class); register(ClientController.class); register(DocumentController.class); register(MetadataController.class); register(ProductController.class); register(InternalErrorMapper.class); } }
3. 在Controller中直接注入User
现在你的Controller方法可以简化成这样,不用再手动处理上下文:
@PUT @Path("invoices") @Consumes("application/json;charset=utf-8") @Produces("application/json;charset=utf-8") public Response saveInvoices(ArrayList<Invoice> arr, @Context User user) throws InternalErrorException { return arr != null ? Response.ok().entity(documentRepository.saveInvoices(user, arr)).build() : Response.notModified().build(); }
额外验证:确保UserRepository的线程安全
虽然核心问题是Controller的上下文共享,但也建议确认UserRepository.get(auth, imei)方法是线程安全的——比如JDBC连接是否是请求级的(Spring的JdbcTemplate默认是线程安全的,因为每次请求都会获取新的连接),如果是自己手动管理连接,要确保每个请求用独立的连接,避免线程安全问题。
内容的提问来源于stack exchange,提问作者Ngor

