diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index 53aa1ee9d4c..0af8c25de6a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -69,6 +69,7 @@ import org.springframework.util.CollectionUtils; * a Spring application context. As of 7.0, it additionally exposes a shared * {@link jakarta.persistence.EntityManager} instance through {@link SmartFactoryBean}, * making {@code EntityManager} available for dependency injection as well. + * As of 7.0.4, it also exposes a JPA 4.0 {@code EntityAgent} if available. * *

Encapsulates the common functionality between the different JPA bootstrap * contracts: standalone as well as container. Note that as of 7.0, the JPA 3.2 @@ -99,6 +100,10 @@ public abstract class AbstractEntityManagerFactoryBean implements BeanNameAware, InitializingBean, SmartInitializingSingleton, DisposableBean, EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable { + /** JPA 4.0 EntityAgent class, if available. */ + private static final @Nullable Class ENTITY_AGENT_CLASS = EntityManagerFactoryUtils.getEntityAgentClass(); + + /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); @@ -112,6 +117,8 @@ public abstract class AbstractEntityManagerFactoryBean implements private @Nullable Class entityManagerInterface; + private @Nullable Class entityAgentInterface = ENTITY_AGENT_CLASS; + private @Nullable JpaDialect jpaDialect; private @Nullable JpaVendorAdapter jpaVendorAdapter; @@ -138,6 +145,9 @@ public abstract class AbstractEntityManagerFactoryBean implements /** Exposed client-level shared EntityManager proxy. */ private @Nullable EntityManager sharedEntityManager; + /** Exposed client-level shared EntityAgent proxy. */ + private @Nullable Object sharedEntityAgent; + /** * Set the PersistenceProvider implementation class to use for creating the @@ -383,6 +393,11 @@ public abstract class AbstractEntityManagerFactoryBean implements this.entityManagerInterface = EntityManager.class; } } + if (this.entityAgentInterface != null) { + Class vendorInterface = jpaVendorAdapter.getEntityAgentInterface(); + this.entityAgentInterface = (vendorInterface != null && + ClassUtils.isVisible(vendorInterface, this.beanClassLoader) ? vendorInterface : null); + } if (this.jpaDialect == null) { this.jpaDialect = jpaVendorAdapter.getJpaDialect(); } @@ -401,7 +416,12 @@ public abstract class AbstractEntityManagerFactoryBean implements // application-managed EntityManager proxy that automatically joins // existing transactions. this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory); + this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory); + if (this.entityAgentInterface != null) { + this.sharedEntityAgent = SharedEntityManagerCreator.createSharedEntityAgent( + this.entityManagerFactory, null, this.entityAgentInterface); + } } @Override @@ -645,6 +665,9 @@ public abstract class AbstractEntityManagerFactoryBean implements if (EntityManager.class.isAssignableFrom(type)) { return (type.isInstance(this.sharedEntityManager) ? type.cast(this.sharedEntityManager) : null); } + if (ENTITY_AGENT_CLASS != null && ENTITY_AGENT_CLASS.isAssignableFrom(type)) { + return (type.isInstance(this.sharedEntityAgent) ? type.cast(this.sharedEntityAgent) : null); + } return SmartFactoryBean.super.getObject(type); } @@ -653,6 +676,9 @@ public abstract class AbstractEntityManagerFactoryBean implements if (EntityManager.class.isAssignableFrom(type)) { return type.isInstance(this.sharedEntityManager); } + if (ENTITY_AGENT_CLASS != null && ENTITY_AGENT_CLASS.isAssignableFrom(type)) { + return type.isInstance(this.sharedEntityAgent); + } return SmartFactoryBean.super.supportsType(type); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java index 6037f5e93ef..43d1f5e8d1c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java @@ -16,6 +16,7 @@ package org.springframework.orm.jpa; +import java.lang.reflect.Method; import java.util.Map; import jakarta.persistence.EntityExistsException; @@ -52,7 +53,9 @@ import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -76,6 +79,12 @@ public abstract class EntityManagerFactoryUtils { public static final int ENTITY_MANAGER_SYNCHRONIZATION_ORDER = DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; + private static final @Nullable Method CREATE_ENTITY_AGENT_METHOD = + ClassUtils.getMethodIfAvailable(EntityManagerFactory.class, "createEntityAgent"); + + private static final @Nullable Method CREATE_ENTITY_AGENT_WITH_PROPERTIES_METHOD = + ClassUtils.getMethodIfAvailable(EntityManagerFactory.class, "createEntityAgent", Map.class); + private static final Log logger = LogFactory.getLog(EntityManagerFactoryUtils.class); @@ -195,9 +204,8 @@ public abstract class EntityManagerFactoryUtils { Assert.notNull(emf, "No EntityManagerFactory specified"); - EntityManagerHolder emHolder = - (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf); - if (emHolder != null) { + EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf); + if (emHolder != null && emHolder.hasEntityManager()) { if (synchronizedWithTransaction) { if (!emHolder.isSynchronizedWithTransaction()) { if (TransactionSynchronizationManager.isActualTransactionActive()) { @@ -232,6 +240,7 @@ public abstract class EntityManagerFactoryUtils { // with no synchronized EntityManager having been requested by application code before. // Unbind in order to register a new unsynchronized EntityManager instead. TransactionSynchronizationManager.unbindResource(emf); + emHolder = null; } else { // Either a previously bound unsynchronized EntityManager, or the application @@ -263,21 +272,30 @@ public abstract class EntityManagerFactoryUtils { } try { - // Use same EntityManager for further JPA operations within the transaction. - // Thread-bound object will get removed by synchronization at transaction completion. - emHolder = new EntityManagerHolder(em); - if (synchronizedWithTransaction) { + if (emHolder != null) { + emHolder.setEntityManager(em); Object transactionData = prepareTransaction(em, emf); TransactionSynchronizationManager.registerSynchronization( - new TransactionalEntityManagerSynchronization(emHolder, emf, transactionData, true)); + new TransactionalEntityManagerSynchronization(emHolder, emf, transactionData, false)); emHolder.setSynchronizedWithTransaction(true); } else { - // Unsynchronized - just scope it for the transaction, as demanded by the JPA 2.1 spec... - TransactionSynchronizationManager.registerSynchronization( - new TransactionScopedEntityManagerSynchronization(emHolder, emf)); + // Use same EntityManager for further JPA operations within the transaction. + // Thread-bound object will get removed by synchronization at transaction completion. + emHolder = new EntityManagerHolder(em); + if (synchronizedWithTransaction) { + Object transactionData = prepareTransaction(em, emf); + TransactionSynchronizationManager.registerSynchronization( + new TransactionalEntityManagerSynchronization(emHolder, emf, transactionData, true)); + emHolder.setSynchronizedWithTransaction(true); + } + else { + // Unsynchronized - just scope it for the transaction, as demanded by the JPA 2.1 spec... + TransactionSynchronizationManager.registerSynchronization( + new TransactionScopedEntityManagerSynchronization(emHolder, emf)); + } + TransactionSynchronizationManager.bindResource(emf, emHolder); } - TransactionSynchronizationManager.bindResource(emf, emHolder); } catch (RuntimeException ex) { // Unexpected exception from external delegation call -> close EntityManager and rethrow. @@ -288,6 +306,79 @@ public abstract class EntityManagerFactoryUtils { return em; } + /** + * Obtain a JPA EntityAgent from the given factory. Binds a corresponding + * EntityAgent to the thread when running in a Spring-managed transaction. + * @param emf the EntityManagerFactory to create the EntityAgent with + * @param properties the properties to be passed into the {@code createEntityAgent} + * call (may be {@code null}) + * @return the EntityAgent, or {@code null} if none found + * @throws PersistenceException if the EntityManager couldn't be created + * @since 7.0.4 + */ + static @Nullable Object doGetTransactionalEntityAgent(EntityManagerFactory emf, @Nullable Map properties) + throws PersistenceException { + + Assert.notNull(emf, "No EntityManagerFactory specified"); + + EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf); + if (emHolder != null) { + if (emHolder.hasEntityAgent()) { + return emHolder.getEntityAgent(); + } + } + else if (!TransactionSynchronizationManager.isSynchronizationActive()) { + // Indicate that we can't obtain a transactional EntityManager. + return null; + } + + // Create a new EntityManager for use within the current transaction. + logger.debug("Opening JPA EntityAgent"); + Object entityAgent = createEntityAgent(emf, properties); + + if (emHolder != null) { + emHolder.setEntityAgent(entityAgent); + } + else { + emHolder = new EntityManagerHolder(entityAgent); + TransactionSynchronizationManager.registerSynchronization( + new TransactionScopedEntityManagerSynchronization(emHolder, emf)); + TransactionSynchronizationManager.bindResource(emf, emHolder); + } + emHolder.setSynchronizedWithTransaction(true); + + return entityAgent; + } + + /** + * Create a new JPA EntityAgent via reflectively detected JPA 4.0 API. + * @param emf the EntityManagerFactory to create the EntityAgent with + * @param properties the properties to be passed into the {@code createEntityAgent} + * call (may be {@code null}) + * @since 7.0.4 + */ + static Object createEntityAgent(EntityManagerFactory emf, @Nullable Map properties) { + if (CREATE_ENTITY_AGENT_METHOD == null || CREATE_ENTITY_AGENT_WITH_PROPERTIES_METHOD == null) { + throw new IllegalStateException("JPA 4.0 createEntityAgent API not available"); + } + Object entityAgent = (!CollectionUtils.isEmpty(properties) ? + ReflectionUtils.invokeMethod(CREATE_ENTITY_AGENT_WITH_PROPERTIES_METHOD, emf, properties) : + ReflectionUtils.invokeMethod(CREATE_ENTITY_AGENT_METHOD, emf)); + if (entityAgent == null) { + throw new IllegalStateException("JPA 4.0 createEntityAgent API returned null"); + } + return entityAgent; + } + + /** + * Determine the {@code jakarta.persistence.EntityAgent} class. + * @return the {@code EntityAgent} class, or {@code null} if not available + * @since 7.0.4 + */ + static @Nullable Class getEntityAgentClass() { + return (CREATE_ENTITY_AGENT_METHOD != null ? CREATE_ENTITY_AGENT_METHOD.getReturnType() : null); + } + /** * Prepare a transaction on the given EntityManager, if possible. * @param em the EntityManager to prepare @@ -424,6 +515,23 @@ public abstract class EntityManagerFactoryUtils { } } + /** + * Close the given JPA EntityHandler (EntityManager or EntityAgent), + * catching and logging any cleanup exceptions thrown. + * @param entityHandler the JPA EntityManager/EntityAgent to close + * @since 7.0.4 + */ + static void closeEntityHandler(@Nullable Object entityHandler) { + if (entityHandler instanceof AutoCloseable closeable) { + try { + closeable.close(); + } + catch (Throwable ex) { + logger.error("Failed to close JPA EntityHandler", ex); + } + } + } + /** * Callback for resource cleanup at the end of a non-JPA transaction @@ -488,7 +596,7 @@ public abstract class EntityManagerFactoryUtils { @Override protected void releaseResource(EntityManagerHolder resourceHolder, EntityManagerFactory resourceKey) { - closeEntityManager(resourceHolder.getEntityManager()); + resourceHolder.closeAll(); } @Override @@ -523,7 +631,7 @@ public abstract class EntityManagerFactoryUtils { @Override protected void releaseResource(EntityManagerHolder resourceHolder, EntityManagerFactory resourceKey) { - closeEntityManager(resourceHolder.getEntityManager()); + resourceHolder.closeAll(); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java index 17ebebade89..fdbc4e41576 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java @@ -39,6 +39,8 @@ public class EntityManagerHolder extends ResourceHolderSupport { protected @Nullable EntityManager entityManager; + protected @Nullable Object entityAgent; + private boolean transactionActive; private @Nullable SavepointManager savepointManager; @@ -48,12 +50,37 @@ public class EntityManagerHolder extends ResourceHolderSupport { this.entityManager = entityManager; } + EntityManagerHolder(@Nullable Object entityAgent) { + this.entityAgent = entityAgent; + } + + + void setEntityManager(EntityManager entityManager) { + this.entityManager = entityManager; + } public EntityManager getEntityManager() { Assert.state(this.entityManager != null, "No EntityManager available"); return this.entityManager; } + void setEntityAgent(Object entityAgent) { + this.entityAgent = entityAgent; + } + + Object getEntityAgent() { + Assert.state(this.entityAgent != null, "No EntityAgent available"); + return this.entityAgent; + } + + boolean hasEntityManager() { + return (this.entityManager != null); + } + + boolean hasEntityAgent() { + return (this.entityAgent != null); + } + protected void setTransactionActive(boolean transactionActive) { this.transactionActive = transactionActive; } @@ -79,7 +106,10 @@ public class EntityManagerHolder extends ResourceHolderSupport { } protected void closeAll() { - EntityManagerFactoryUtils.closeEntityManager(this.entityManager); + EntityManagerFactoryUtils.closeEntityHandler(this.entityManager); + EntityManagerFactoryUtils.closeEntityHandler(this.entityAgent); + this.entityManager = null; + this.entityAgent = null; } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java index bc511542fdc..b2bb24ba070 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java @@ -124,6 +124,20 @@ public interface JpaVendorAdapter { return EntityManager.class; } + /** + * Return the vendor-specific JPA 4.0 EntityAgent interface + * that this provider's EntityAgents will implement. + *

If the provider does not offer any EntityAgent extensions, + * the adapter should simply return the standard + * {@link jakarta.persistence.EntityAgent} class here. + * @return the EntityAgent class, or {@code null} if not supported + * (on JPA 3.2) + * @since 7.0.4 + */ + default @Nullable Class getEntityAgentInterface() { + return EntityManagerFactoryUtils.getEntityAgentClass(); + } + /** * Optional callback for post-processing the native EntityManagerFactory * before active use. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 303a2409513..4b8a024a592 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -177,31 +177,42 @@ public abstract class SharedEntityManagerCreator { ifcs, new SharedEntityManagerInvocationHandler(emf, properties, synchronizedWithTransaction)); } - /** - * Invocation handler that delegates all calls to the current - * transactional EntityManager, if any; else, it will fall back - * to a newly created EntityManager per operation. + * Create a transactional EntityAgent proxy for the given EntityManagerFactory. + * @param emf the EntityManagerFactory to obtain EntityAgentss from as needed + * @param properties the properties to be passed into the + * {@code createEntityAgent} call (may be {@code null}) + * @param ifc the interfaces to be implemented by the EntityAgent + * @return a shareable transactional EntityAgent proxy + * @since 7.0.4 */ - @SuppressWarnings("serial") - private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable { - - private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class); + static Object createSharedEntityAgent(EntityManagerFactory emf, @Nullable Map properties, Class ifc) { + ClassLoader cl = null; + if (emf instanceof EntityManagerFactoryInfo emfInfo) { + cl = emfInfo.getBeanClassLoader(); + } + return Proxy.newProxyInstance( + (cl != null ? cl : SharedEntityManagerCreator.class.getClassLoader()), + new Class[] {ifc}, new SharedEntityAgentInvocationHandler(emf, properties)); + } - private final EntityManagerFactory targetFactory; - private final @Nullable Map properties; + /** + * Base class for EntityManager and EntityAgent invocation handlers. + * @since 7.0.4 + */ + @SuppressWarnings("serial") + private abstract static class SharedEntityHandlerInvocationHandler implements InvocationHandler, Serializable { - private final boolean synchronizedWithTransaction; + protected final EntityManagerFactory targetFactory; - private transient volatile @Nullable ClassLoader proxyClassLoader; + protected final @Nullable Map properties; - public SharedEntityManagerInvocationHandler( - EntityManagerFactory target, @Nullable Map properties, boolean synchronizedWithTransaction) { + protected transient volatile @Nullable ClassLoader proxyClassLoader; + public SharedEntityHandlerInvocationHandler(EntityManagerFactory target, @Nullable Map properties) { this.targetFactory = target; this.properties = properties; - this.synchronizedWithTransaction = synchronizedWithTransaction; initProxyClassLoader(); } @@ -214,6 +225,65 @@ public abstract class SharedEntityManagerCreator { } } + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + // Initialize transient fields. + initProxyClassLoader(); + } + + protected Object invokeMethod(Method method, @Nullable Object[] args, Object target, boolean newTarget) + throws Throwable { + + boolean close = newTarget; + try { + Object result = method.invoke(target, args); + if (result instanceof Query query) { + if (newTarget) { + Class[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key -> + ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader)); + result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs, + new DeferredQueryInvocationHandler(query, target)); + close = false; + } + else { + EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory); + } + } + return result; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + finally { + if (close) { + EntityManagerFactoryUtils.closeEntityHandler(target); + } + } + } + } + + + /** + * Invocation handler that delegates all calls to the current + * transactional EntityManager, if any; else, it will fall back + * to a newly created EntityManager per operation. + */ + @SuppressWarnings("serial") + private static class SharedEntityManagerInvocationHandler extends SharedEntityHandlerInvocationHandler + implements Serializable { + + private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class); + + private final boolean synchronizedWithTransaction; + + public SharedEntityManagerInvocationHandler( + EntityManagerFactory target, @Nullable Map properties, boolean synchronizedWithTransaction) { + + super(target, properties); + this.synchronizedWithTransaction = synchronizedWithTransaction; + } + @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { // Invocation on EntityManager interface coming in... @@ -303,47 +373,120 @@ public abstract class SharedEntityManagerCreator { } // Regular EntityManager operations. - boolean isNewEm = false; + boolean newTarget = false; if (target == null) { logger.debug("Creating new EntityManager for shared EntityManager invocation"); target = (!CollectionUtils.isEmpty(this.properties) ? this.targetFactory.createEntityManager(this.properties) : this.targetFactory.createEntityManager()); - isNewEm = true; + newTarget = true; } // Invoke method on current EntityManager. - try { - Object result = method.invoke(target, args); - if (result instanceof Query query) { - if (isNewEm) { - Class[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key -> - ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader)); - result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs, - new DeferredQueryInvocationHandler(query, target)); - isNewEm = false; + return invokeMethod(method, args, target, newTarget); + } + } + + + /** + * Invocation handler that delegates all calls to the current + * transactional EntityAgent, if any; else, it will fall back + * to a newly created EntityAgent per operation. + * @since 7.0.4 + */ + @SuppressWarnings("serial") + private static class SharedEntityAgentInvocationHandler extends SharedEntityHandlerInvocationHandler + implements Serializable { + + private static final Log logger = LogFactory.getLog(SharedEntityAgentInvocationHandler.class); + + private transient volatile @Nullable ClassLoader proxyClassLoader; + + public SharedEntityAgentInvocationHandler(EntityManagerFactory target, @Nullable Map properties) { + super(target, properties); + } + + @Override + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on EntityAgent interface coming in... + + switch (method.getName()) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + case "hashCode" -> { + // Use hashCode of EntityAgent proxy. + return hashCode(); + } + case "toString" -> { + // Deliver toString without touching a target EntityAgent. + return "Shared EntityAgent proxy for target factory [" + this.targetFactory + "]"; + } + case "getEntityManagerFactory" -> { + // JPA 2.0: return EntityManagerFactory without creating an EntityAgent. + return this.targetFactory; + } + case "getCriteriaBuilder", "getMetamodel" -> { + // JPA 2.0: return EntityManagerFactory's CriteriaBuilder/Metamodel (avoid creation of EntityAgent) + try { + return EntityManagerFactory.class.getMethod(method.getName()).invoke(this.targetFactory); } - else { - EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory); + catch (InvocationTargetException ex) { + throw ex.getTargetException(); } } - return result; - } - catch (InvocationTargetException ex) { - throw ex.getTargetException(); + case "unwrap" -> { + // JPA 2.0: handle unwrap method - could be a proxy match. + Class targetClass = (Class) args[0]; + if (targetClass != null && targetClass.isInstance(proxy)) { + return proxy; + } + } + case "isOpen" -> { + // Handle isOpen method: always return true. + return true; + } + case "close" -> { + // Handle close method: suppress, not valid. + return null; + } + case "getTransaction" -> { + throw new IllegalStateException( + "Not allowed to create transaction on shared EntityAgent - " + + "use Spring transactions or EJB CMT instead"); + } } - finally { - if (isNewEm) { - EntityManagerFactoryUtils.closeEntityManager(target); + + // Determine current EntityAgent: either the transactional one + // managed by the factory or a temporary one for the given invocation. + Object target = EntityManagerFactoryUtils.doGetTransactionalEntityAgent( + this.targetFactory, this.properties); + + switch (method.getName()) { + case "unwrap" -> { + Class targetClass = (Class) args[0]; + if (targetClass == null) { + return (target != null ? target : proxy); + } + // We need a transactional target now. + if (target == null) { + throw new IllegalStateException("No transactional EntityAgent available"); + } } + // Still perform unwrap call on target EntityAgent. } - } - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - // Rely on default serialization, just initialize state after deserialization. - ois.defaultReadObject(); - // Initialize transient fields. - initProxyClassLoader(); + // Regular EntityAgent operations. + boolean newTarget = false; + if (target == null) { + logger.debug("Creating new EntityAgent for shared EntityAgent invocation"); + target = EntityManagerFactoryUtils.createEntityAgent(this.targetFactory, this.properties); + newTarget = true; + } + + // Invoke method on current EntityAgent. + return invokeMethod(method, args, target, newTarget); } } @@ -359,13 +502,13 @@ public abstract class SharedEntityManagerCreator { private final Query target; - private @Nullable EntityManager entityManager; + private @Nullable Object entityHandler; private @Nullable Map<@Nullable Object, @Nullable Object> outputParameters; - public DeferredQueryInvocationHandler(Query target, EntityManager entityManager) { + public DeferredQueryInvocationHandler(Query target, Object entityHandler) { this.target = target; - this.entityManager = entityManager; + this.entityHandler = entityHandler; } @Override @@ -395,7 +538,7 @@ public abstract class SharedEntityManagerCreator { } } case "getOutputParameterValue" -> { - if (this.entityManager == null) { + if (this.entityHandler == null) { Object key = args[0]; if (this.outputParameters == null || !this.outputParameters.containsKey(key)) { throw new IllegalArgumentException("OUT/INOUT parameter not available: " + key); @@ -426,7 +569,7 @@ public abstract class SharedEntityManagerCreator { } finally { if (queryTerminatingMethods.contains(method.getName())) { - // Actual execution of the query: close the EntityManager right + // Actual execution of the query: close the EntityHandler right // afterwards, since that was the only reason we kept it open. if (this.outputParameters != null && this.target instanceof StoredProcedureQuery storedProc) { for (Map.Entry entry : this.outputParameters.entrySet()) { @@ -444,8 +587,8 @@ public abstract class SharedEntityManagerCreator { } } } - EntityManagerFactoryUtils.closeEntityManager(this.entityManager); - this.entityManager = null; + EntityManagerFactoryUtils.closeEntityHandler(this.entityHandler); + this.entityHandler = null; } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java index ec958bdcd97..29c14516ccc 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java @@ -39,8 +39,6 @@ import org.springframework.util.Assert; */ public class SessionHolder extends EntityManagerHolder { - private @Nullable StatelessSession statelessSession; - private @Nullable Transaction transaction; private @Nullable FlushMode previousFlushMode; @@ -52,7 +50,7 @@ public class SessionHolder extends EntityManagerHolder { SessionHolder(StatelessSession session) { super(null); - this.statelessSession = session; + setStatelessSession(session); } @@ -69,16 +67,16 @@ public class SessionHolder extends EntityManagerHolder { } void setStatelessSession(StatelessSession statelessSession) { - this.statelessSession = statelessSession; + this.entityAgent = statelessSession; } StatelessSession getStatelessSession() { - Assert.state(this.statelessSession != null, "No StatelessSession available"); - return this.statelessSession; + Assert.state(this.entityAgent != null, "No StatelessSession available"); + return (StatelessSession) this.entityAgent; } boolean hasStatelessSession() { - return (this.statelessSession != null); + return (this.entityAgent != null); } void setTransaction(@Nullable Transaction transaction) { @@ -109,9 +107,6 @@ public class SessionHolder extends EntityManagerHolder { @Override protected void closeAll() { super.closeAll(); - if (this.statelessSession != null && this.statelessSession.isOpen()) { - this.statelessSession.close(); - } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index 6e051d62ca3..b4e31f3d2f7 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -26,6 +26,7 @@ import jakarta.persistence.spi.PersistenceUnitInfo; import jakarta.transaction.TransactionManager; import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.H2Dialect; @@ -78,10 +79,6 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter implemen private final PersistenceProvider persistenceProvider; - private final Class entityManagerFactoryInterface; - - private final Class entityManagerInterface; - private @Nullable Object jtaTransactionManager; private @Nullable BeanFactory beanFactory; @@ -89,8 +86,6 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter implemen public HibernateJpaVendorAdapter() { this.persistenceProvider = new SpringHibernateJpaPersistenceProvider(); - this.entityManagerFactoryInterface = SessionFactory.class; - this.entityManagerInterface = Session.class; } @@ -271,12 +266,17 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter implemen @Override public Class getEntityManagerFactoryInterface() { - return this.entityManagerFactoryInterface; + return SessionFactory.class; } @Override public Class getEntityManagerInterface() { - return this.entityManagerInterface; + return Session.class; + } + + @Override + public Class getEntityAgentInterface() { + return StatelessSession.class; } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java index a6f8918bea8..3280e3f427d 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java @@ -108,7 +108,6 @@ class SharedEntityManagerCreatorTests { Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); given(targetEm.createQuery("x")).willReturn(targetQuery); - given(targetEm.isOpen()).willReturn(true); given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -129,7 +128,6 @@ class SharedEntityManagerCreatorTests { Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); given(targetEm.createQuery("x")).willReturn(targetQuery); - given(targetEm.isOpen()).willReturn(true); given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -150,7 +148,6 @@ class SharedEntityManagerCreatorTests { Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); given(targetEm.createQuery("x")).willReturn(targetQuery); - given(targetEm.isOpen()).willReturn(true); given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -171,7 +168,6 @@ class SharedEntityManagerCreatorTests { Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); given(targetEm.createQuery("x")).willReturn(targetQuery); - given(targetEm.isOpen()).willReturn(true); given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -194,7 +190,6 @@ class SharedEntityManagerCreatorTests { given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); willReturn("y").given(targetQuery).getOutputParameterValue(0); willReturn("z").given(targetQuery).getOutputParameterValue(2); - given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); StoredProcedureQuery spq = em.createStoredProcedureQuery("x"); @@ -225,7 +220,6 @@ class SharedEntityManagerCreatorTests { given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); willReturn("y").given(targetQuery).getOutputParameterValue("a"); willReturn("z").given(targetQuery).getOutputParameterValue("c"); - given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); StoredProcedureQuery spq = em.createStoredProcedureQuery("x"); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java index ca9ff977fb4..2570d3982e7 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java @@ -59,7 +59,6 @@ class PersistenceContextTransactionTests { void setup() { given(factory.createEntityManager()).willReturn(manager); given(manager.getTransaction()).willReturn(tx); - given(manager.isOpen()).willReturn(true); @SuppressWarnings("serial") PersistenceAnnotationBeanPostProcessor pabpp = new PersistenceAnnotationBeanPostProcessor() { @@ -99,8 +98,6 @@ class PersistenceContextTransactionTests { @Test void testTransactionCommitWithSharedEntityManagerAndPropagationSupports() { - given(manager.isOpen()).willReturn(true); - tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); tt.execute(status -> { diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java index cc01c0d7998..a76a8cfc789 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java @@ -42,7 +42,6 @@ class SharedEntityManagerFactoryTests { Object o = new Object(); EntityManager mockEm = mock(); - given(mockEm.isOpen()).willReturn(true); EntityManagerFactory mockEmf = mock(); given(mockEmf.createEntityManager()).willReturn(mockEm); @@ -58,8 +57,7 @@ class SharedEntityManagerFactoryTests { assertThat(proxyFactoryBean.getObject()).isSameAs(proxy); assertThat(proxy.contains(o)).isFalse(); - boolean condition = proxy instanceof EntityManagerProxy; - assertThat(condition).isTrue(); + assertThat(proxy).isInstanceOf(EntityManagerProxy.class); EntityManagerProxy emProxy = (EntityManagerProxy) proxy; assertThatIllegalStateException().as("outside of transaction").isThrownBy( emProxy::getTargetEntityManager);