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

JSF场景下使用非线程安全EntityManager实现同步懒加载的问题咨询

现有方案的核心隐患
  • 只同步em.find方法无法覆盖所有EntityManager操作场景:你实体类的addresses、company属性都是懒加载,渲染页面时EL表达式读取这些属性会自动触发EntityManager的加载操作,这部分操作没有加同步,依然存在多线程并发调用的风险。
  • 同一会话的并发请求必然触发线程安全问题:JSF单次请求的生命周期(含渲染阶段)默认是单线程执行,单次请求内的多个属性读取不会并行,但如果用户同一会话打开多个标签页同时发起请求,多个线程会同时访问SessionScoped Bean中持有的同一个EntityManager实例,极易出现数据错乱、ConcurrentModificationException、连接泄漏等问题。
  • 长生命周期EntityManager存在额外问题:将EntityManager放在会话级作用域会导致一级缓存持续膨胀占用内存,未提交的事务还可能引发数据库锁等待,同时如果实体被其他请求修改,缓存中的旧数据会导致页面展示异常。
可行解决方案

针对你Java SE + Tomcat、无Spring、手动事务的环境,推荐以下两种方案,优先级从高到低:

方案1:EntityManager请求级生命周期管理(推荐)

EntityManager的设计定位就是请求/事务级的短生命周期对象,不要放在会话级作用域共享:

  1. 全局初始化唯一的EntityManagerFactory,应用启动时创建,关闭时销毁。
  2. 新增请求过滤器统一管理EntityManager的生命周期,示例代码如下:
public class EntityManagerFilter implements Filter {
    private static EntityManagerFactory emf;

    @Override
    public void init(FilterConfig filterConfig) {
        emf = Persistence.createEntityManagerFactory("你的持久化单元名称");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        EntityManager em = emf.createEntityManager();
        try {
            // 将EntityManager存入请求属性,供业务代码获取
            request.setAttribute("currentEm", em);
            // 统一开启事务
            em.getTransaction().begin();
            chain.doFilter(request, response);
            // 请求处理完成提交事务
            em.getTransaction().commit();
        } catch (Exception e) {
            // 异常回滚事务
            if (em.getTransaction().isActive()) {
                em.getTransaction().rollback();
            }
            throw new ServletException(e);
        } finally {
            // 关闭EntityManager,释放资源
            em.close();
        }
    }

    @Override
    public void destroy() {
        emf.close();
    }
}
  1. 在web.xml中配置该过滤器,优先级高于JSF的FacesServlet。
  2. 将PersonBean改为@RequestScoped,从请求中获取EntityManager操作:
@Named
@RequestScoped
public class PersonBean {
    public Person getPerson() {
        EntityManager em = (EntityManager) FacesContext.getCurrentInstance()
                .getExternalContext().getRequestMap().get("currentEm");
        // 用FETCH JOIN一次性加载所有页面需要的关联属性,避免懒加载额外开销
        return em.createQuery("SELECT p FROM Person p LEFT JOIN FETCH p.addresses " +
                        "LEFT JOIN FETCH p.company c LEFT JOIN FETCH c.address WHERE p.id = :id", Person.class)
                .setParameter("id", 1)
                .getSingleResult();
    }
}

该方案完全避免了EntityManager的多线程共享,从根源上解决线程安全问题,同时渲染阶段EntityManager处于开启状态,不会出现懒加载异常。

方案2:会话级存储完全初始化的游离实体

如果确实需要在会话中缓存Person数据,不要持有EntityManager实例,只存储完全初始化的游离对象:

@Named
@SessionScoped
public class PersonBean implements Serializable {
    private Person cachedPerson;
    // 全局EntityManagerFactory,省略初始化逻辑
    private EntityManagerFactory emf;

    @PostConstruct
    public void initCachedPerson() {
        EntityManager em = emf.createEntityManager();
        try {
            em.getTransaction().begin();
            Person person = em.find(Person.class, 1);
            // 手动初始化所有页面需要的懒加载属性
            // 不依赖Hibernate的话,直接调用getAddresses().size()、getCompany().getAddress()也可触发加载
            Hibernate.initialize(person.getAddresses());
            Hibernate.initialize(person.getCompany());
            Hibernate.initialize(person.getCompany().getAddress());
            em.getTransaction().commit();
            this.cachedPerson = person;
        } finally {
            em.close();
        }
    }

    public Person getPerson() {
        return cachedPerson;
    }
}

该方案中会话中仅存储无状态的实体对象,完全不涉及EntityManager的共享,也不会有线程安全问题,适合数据变化频率低的场景。

内容的提问来源于stack exchange,提问作者Milo van der Zee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 09:12:02