You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot Jersey应用线程安全问题排查求助

分析与解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:43:01