JSF场景下使用非线程安全EntityManager实现同步懒加载的问题咨询
现有方案的核心隐患
- 只同步
em.find方法无法覆盖所有EntityManager操作场景:你实体类的addresses、company属性都是懒加载,渲染页面时EL表达式读取这些属性会自动触发EntityManager的加载操作,这部分操作没有加同步,依然存在多线程并发调用的风险。 - 同一会话的并发请求必然触发线程安全问题:JSF单次请求的生命周期(含渲染阶段)默认是单线程执行,单次请求内的多个属性读取不会并行,但如果用户同一会话打开多个标签页同时发起请求,多个线程会同时访问
SessionScopedBean中持有的同一个EntityManager实例,极易出现数据错乱、ConcurrentModificationException、连接泄漏等问题。 - 长生命周期EntityManager存在额外问题:将EntityManager放在会话级作用域会导致一级缓存持续膨胀占用内存,未提交的事务还可能引发数据库锁等待,同时如果实体被其他请求修改,缓存中的旧数据会导致页面展示异常。
可行解决方案
针对你Java SE + Tomcat、无Spring、手动事务的环境,推荐以下两种方案,优先级从高到低:
方案1:EntityManager请求级生命周期管理(推荐)
EntityManager的设计定位就是请求/事务级的短生命周期对象,不要放在会话级作用域共享:
- 全局初始化唯一的
EntityManagerFactory,应用启动时创建,关闭时销毁。 - 新增请求过滤器统一管理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(); } }
- 在
web.xml中配置该过滤器,优先级高于JSF的FacesServlet。 - 将
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
相关产品推荐
相关产品推荐

