I am currently working on retrofitting Spring-based declarative transactions to a legacy application. The application is deployed on Tomcat and uses JPA/Hibernate to access a PostgreSQL database, and a homegrown web framework (so switching e.g. to Spring Boot is at this time not an option).
My problem is that after changing all DAOs to use an injected EntityManager, everything works when there is only a single user, but with multiple users, I get exceptions that indicate concurrency problems:
Caused by: java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1424)
at org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.releaseResources(ResourceRegistryStandardImpl.java:328)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.afterTransaction(AbstractLogicalConnectionImplementor.java:60)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.afterTransaction(LogicalConnectionManagedImpl.java:167)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.afterCompletion(LogicalConnectionManagedImpl.java:293)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:95)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:282)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
and
Caused by: java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) ~[?:?]
at java.util.ArrayList$Itr.next(ArrayList.java:967) ~[?:?]
at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1054) ~[?:?]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:602) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[?:?]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
and
Caused by: org.hibernate.AssertionFailure: possible non thread safe access to session
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:215) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[?:?]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1394) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
This is the superclass all DAOs inherit from to get their EntityManager
instance:
public abstract class AbstractDao {
protected EntityManager entityManager;
protected AbstractDao() {
}
@PersistenceContext(type = PersistenceContextType.EXTENDED)
@Scope("request") // just an experiment, should not be necessary AFAIK
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}
This is the persistence configuration:
@Configuration
@EnableAsync
@EnableTransactionManagement
public class PersistenceConfiguration {
@Bean
public EntityManagerFactory getEntityManagerFactory() {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("my.package");
return entityManagerFactory;
}
@Bean
public PlatformTransactionManager initTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(getEntityManagerFactory());
transactionManager.setJpaDialect(new HibernateJpaDialect());
return transactionManager;
}
}
Here is the persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1">
<persistence-unit name="de.lexcom.agroparts.persistence" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<non-jta-data-source>java:comp/env/jdbc/myapp</non-jta-data-source>
<shared-cache-mode>NONE</shared-cache-mode>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="show_sql" value="false"/>
<property name="hibernate.show_sql" value="false" />
<property name="hibernate.format_sql" value="false" />
<property name="hibernate.cache.use_second_level_cache" value="false"/>
<property name="tomee.jpa.factory.lazy" value="true"/>
</properties>
</persistence-unit>
</persistence>
And the configuration in web.xml
seems to be exactly what is needed to support the request scope, according to the Spring documentation - I have tried configuring either a RequestContextListener
or a RequestContextFilter
, and can confirm via debugger that they are called, but neither seems to have an effect.
My understanding, according to this article is that the @PersistenceConfig
annotation should
provide a proxy which forwards calls to a request-scoped entity manager instance that is set up by the RequestContextListener
or RequestContextFilter
.
For some reason, that doesn't seem to be working and instead every request goes to the same entity manager (or at least the same Hibernate session).
Or is the problem the transaction-type="RESOURCE_LOCAL"
in the persistence.xml
?
According to this Stack Overflow answer
it sounds like I have to use transaction-type="JTA"
to use @PersistenceContext
and have multiple EntityManager
instances,
but that seems to be copied verbatim from the TomEE documentation
(a JEE container), and it is contradicted this article
which says that "by default, Spring applications use RESOURCE_LOCAL transactions".
What am I missing?