Runtime compatibility with JPA 4.0 M4

Closes gh-36784
This commit is contained in:
Juergen Hoeller
2026-05-29 13:28:46 +02:00
parent 0b77c3df45
commit 175c1f9437
13 changed files with 75 additions and 54 deletions
@@ -509,6 +509,7 @@ public abstract class AbstractEntityManagerFactoryBean implements
* Delegate an incoming invocation from the proxy, dispatching to EntityManagerFactoryInfo
* or the native EntityManagerFactory accordingly.
*/
@SuppressWarnings("NullAway") // for query.unwrap(null)
Object invokeProxyMethod(Method method, Object @Nullable [] args) throws Throwable {
if (method.getDeclaringClass().isAssignableFrom(EntityManagerFactoryInfo.class)) {
return method.invoke(this, args);
@@ -517,9 +518,8 @@ public abstract class AbstractEntityManagerFactoryBean implements
args[0] == SynchronizationType.SYNCHRONIZED) {
// JPA 2.1's createEntityManager(SynchronizationType, Map)
// Redirect to plain createEntityManager and add synchronization semantics through Spring proxy
EntityManager rawEntityManager = (args.length > 1 ?
getNativeEntityManagerFactory().createEntityManager((Map<?, ?>) args[1]) :
getNativeEntityManagerFactory().createEntityManager());
EntityManager rawEntityManager = EntityManagerFactoryUtils.createEntityManager(
getNativeEntityManagerFactory(), (args.length > 1 ? (Map<?, ?>) args[1] : null));
postProcessEntityManager(rawEntityManager);
return ExtendedEntityManagerCreator.createApplicationManagedEntityManager(rawEntityManager, this, true);
}
@@ -532,6 +532,7 @@ public abstract class AbstractEntityManagerFactoryBean implements
// Assumably a Spring-generated proxy from SharedEntityManagerCreator:
// since we're passing it back to the native EntityManagerFactory,
// let's unwrap it to the original Query object from the provider.
// Note that null is not an officially supported argument in JPA.
try {
args[i] = query.unwrap(null);
}
@@ -605,9 +606,8 @@ public abstract class AbstractEntityManagerFactoryBean implements
@Override
public EntityManager createNativeEntityManager(@Nullable Map<?, ?> properties) {
EntityManager rawEntityManager = (!CollectionUtils.isEmpty(properties) ?
getNativeEntityManagerFactory().createEntityManager(properties) :
getNativeEntityManagerFactory().createEntityManager());
EntityManager rawEntityManager = EntityManagerFactoryUtils.createEntityManager(
getNativeEntityManagerFactory(), properties);
postProcessEntityManager(rawEntityManager);
return rawEntityManager;
}
@@ -162,7 +162,7 @@ public abstract class EntityManagerFactoryAccessor implements BeanFactoryAware {
protected EntityManager createEntityManager() throws IllegalStateException {
EntityManagerFactory emf = obtainEntityManagerFactory();
Map<String, Object> properties = getJpaPropertyMap();
return (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager());
return EntityManagerFactoryUtils.createEntityManager(emf, properties);
}
/**
@@ -79,10 +79,10 @@ 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 boolean CREATE_ENTITY_MANAGER_WITHOUT_ARGUMENTS_AVAILABLE =
ClassUtils.hasMethod(EntityManagerFactory.class, "createEntityManager");
private static final @Nullable Method CREATE_ENTITY_AGENT_WITH_PROPERTIES_METHOD =
private static final @Nullable Method CREATE_ENTITY_AGENT_METHOD =
ClassUtils.getMethodIfAvailable(EntityManagerFactory.class, "createEntityAgent", Map.class);
private static final Log logger = LogFactory.getLog(EntityManagerFactoryUtils.class);
@@ -268,7 +268,7 @@ public abstract class EntityManagerFactoryUtils {
}
}
if (em == null) {
em = (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager());
em = createEntityManager(emf, properties);
}
try {
@@ -358,6 +358,23 @@ public abstract class EntityManagerFactoryUtils {
return entityAgent;
}
/**
* Create a new JPA EntityManager via reflectively detected JPA 3.2/4.0 API.
* @param emf the EntityManagerFactory to create the EntityManager with
* @param properties the properties to be passed into the {@code createEntityManager}
* call (may be {@code null})
* @since 7.0.8
*/
public static EntityManager createEntityManager(EntityManagerFactory emf, @Nullable Map<?, ?> properties) {
if (CollectionUtils.isEmpty(properties)) {
properties = null;
}
if (properties == null && CREATE_ENTITY_MANAGER_WITHOUT_ARGUMENTS_AVAILABLE) {
return emf.createEntityManager(); // on JPA 3.2
}
return emf.createEntityManager(properties); // on JPA 4.0 even for empty properties
}
/**
* Create a new JPA EntityAgent via reflectively detected JPA 4.0 API.
* @param emf the EntityManagerFactory to create the EntityAgent with
@@ -366,12 +383,13 @@ public abstract class EntityManagerFactoryUtils {
* @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) {
if (CREATE_ENTITY_AGENT_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 (CollectionUtils.isEmpty(properties)) {
properties = null;
}
Object entityAgent = ReflectionUtils.invokeMethod(CREATE_ENTITY_AGENT_METHOD, emf, properties);
if (entityAgent == null) {
throw new IllegalStateException("JPA 4.0 createEntityAgent API returned null");
}
@@ -31,6 +31,7 @@ import org.springframework.util.ClassUtils;
* {@link AbstractEntityManagerFactoryBean} and {@link SharedEntityManagerCreator} are registered.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
* @since 6.0
*/
class EntityManagerRuntimeHints implements RuntimeHintsRegistrar {
@@ -45,6 +46,12 @@ class EntityManagerRuntimeHints implements RuntimeHintsRegistrar {
// As of Hibernate 7.1
private static final String SQM_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.sqm.internal.SqmQueryImpl";
// As of Hibernate 8.0
private static final String SELECTION_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.internal.SelectionQueryImpl";
// As of Hibernate 8.0
private static final String MUTATION_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.internal.MutationQueryImpl";
private static final String NATIVE_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.sql.internal.NativeQueryImpl";
@@ -76,6 +83,18 @@ class EntityManagerRuntimeHints implements RuntimeHintsRegistrar {
}
catch (ClassNotFoundException ignored) {
}
try {
Class<?> clazz = ClassUtils.forName(SELECTION_QUERY_IMPL_CLASS_NAME, classLoader);
hints.proxies().registerJdkProxy(ClassUtils.getAllInterfacesForClass(clazz, classLoader));
}
catch (ClassNotFoundException ignored) {
}
try {
Class<?> clazz = ClassUtils.forName(MUTATION_QUERY_IMPL_CLASS_NAME, classLoader);
hints.proxies().registerJdkProxy(ClassUtils.getAllInterfacesForClass(clazz, classLoader));
}
catch (ClassNotFoundException ignored) {
}
try {
Class<?> clazz = ClassUtils.forName(NATIVE_QUERY_IMPL_CLASS_NAME, classLoader);
hints.proxies().registerJdkProxy(ClassUtils.getAllInterfacesForClass(clazz, classLoader));
@@ -42,7 +42,6 @@ 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.ConcurrentReferenceHashMap;
/**
@@ -174,8 +173,7 @@ public abstract class ExtendedEntityManagerCreator {
return createProxy(rawEntityManager, emfInfo, true, synchronizedWithTransaction);
}
else {
EntityManager rawEntityManager = (!CollectionUtils.isEmpty(properties) ?
emf.createEntityManager(properties) : emf.createEntityManager());
EntityManager rawEntityManager = EntityManagerFactoryUtils.createEntityManager(emf, properties);
return createProxy(rawEntityManager, null, null, null, null, true, synchronizedWithTransaction);
}
}
@@ -475,8 +475,7 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager
em = emfInfo.createNativeEntityManager(properties);
}
else {
em = (!CollectionUtils.isEmpty(properties) ?
emf.createEntityManager(properties) : emf.createEntityManager());
em = EntityManagerFactoryUtils.createEntityManager(emf, properties);
}
if (this.entityManagerInitializer != null) {
this.entityManagerInitializer.accept(em);
@@ -143,9 +143,7 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
*/
public PersistenceConfiguration getPersistenceConfiguration() {
if (this.configuration == null) {
String name = getPersistenceUnitName();
Assert.state(name != null, "No persistenceUnitName set");
this.configuration = new PersistenceConfiguration(name);
this.configuration = new PersistenceConfiguration(obtainPersistenceUnitName());
}
return this.configuration;
}
@@ -167,6 +165,12 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
super.setPersistenceUnitName(persistenceUnitName);
}
private String obtainPersistenceUnitName() {
String name = getPersistenceUnitName();
Assert.state(name != null, "No persistenceUnitName set");
return name;
}
/**
* Set whether to use Spring-based scanning for entity classes in the classpath
* instead of using JPA's standard scanning of jar files with {@code persistence.xml}
@@ -265,7 +269,7 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
// Create EntityManagerFactory directly through PersistenceProvider.
EntityManagerFactory emf = (this.configuration != null ?
provider.createEntityManagerFactory(this.configuration) :
provider.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap()));
provider.createEntityManagerFactory(obtainPersistenceUnitName(), getJpaPropertyMap()));
if (emf == null) {
throw new PersistenceException(
"PersistenceProvider [" + provider + "] could not find persistence unit for name '" +
@@ -277,7 +281,7 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
// Let JPA perform its standard PersistenceProvider autodetection.
return (this.configuration != null ?
Persistence.createEntityManagerFactory(this.configuration) :
Persistence.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap()));
Persistence.createEntityManagerFactory(obtainPersistenceUnitName(), getJpaPropertyMap()));
}
}
@@ -39,7 +39,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
@@ -376,9 +375,7 @@ public abstract class SharedEntityManagerCreator {
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());
target = EntityManagerFactoryUtils.createEntityManager(this.targetFactory, this.properties);
newTarget = true;
}
@@ -146,7 +146,11 @@ public final class PersistenceManagedTypesScanner {
try {
MetadataReader reader = factory.getMetadataReader(resource);
String className = reader.getClassMetadata().getClassName();
if (matchesEntityTypeFilter(reader, factory) && this.managedClassNameFilter.matches(className)) {
if (className.endsWith(ClassUtils.PACKAGE_INFO_SUFFIX)) {
scanResult.managedPackages.add(className.substring(0,
className.length() - ClassUtils.PACKAGE_INFO_SUFFIX.length()));
}
else if (matchesEntityTypeFilter(reader, factory) && this.managedClassNameFilter.matches(className)) {
scanResult.managedClassNames.add(className);
if (scanResult.persistenceUnitRootUrl == null) {
URL url = resource.getURL();
@@ -155,10 +159,6 @@ public final class PersistenceManagedTypesScanner {
}
}
}
if (className.endsWith(ClassUtils.PACKAGE_INFO_SUFFIX)) {
scanResult.managedPackages.add(className.substring(0,
className.length() - ClassUtils.PACKAGE_INFO_SUFFIX.length()));
}
}
catch (FileNotFoundException ex) {
// Ignore non-readable resource
@@ -240,7 +240,7 @@ public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
* @see jakarta.persistence.EntityManagerFactory#createEntityManager()
*/
protected EntityManager createEntityManager(EntityManagerFactory emf) {
return emf.createEntityManager();
return EntityManagerFactoryUtils.createEntityManager(emf, null);
}
private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManager, String key) {
@@ -19,11 +19,6 @@ package org.springframework.orm.jpa;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.CommonQueryContract;
import org.hibernate.query.SelectionQuery;
import org.hibernate.query.hql.spi.SqmQueryImplementor;
import org.hibernate.query.spi.DomainQueryExecutionContext;
import org.hibernate.query.sqm.spi.InterpretationsKeySource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -47,8 +42,7 @@ class EntityManagerRuntimeHintsTests {
@BeforeEach
void setup() {
AotServices.factories().load(RuntimeHintsRegistrar.class)
.forEach(registrar -> registrar.registerHints(this.hints,
ClassUtils.getDefaultClassLoader()));
.forEach(registrar -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
}
@Test
@@ -69,13 +63,4 @@ class EntityManagerRuntimeHintsTests {
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(EntityManagerFactory.class, "getMetamodel")).accepts(this.hints);
}
@Test
void sqmQueryHints() {
assertThat(RuntimeHintsPredicates.proxies().forInterfaces(
SqmQueryImplementor.class,
InterpretationsKeySource.class,
DomainQueryExecutionContext.class,
SelectionQuery.class,
CommonQueryContract.class)).accepts(this.hints);
}
}
@@ -76,19 +76,20 @@ class OpenEntityManagerInViewTests {
@BeforeEach
void setUp() {
void setup() {
given(factory.createEntityManager()).willReturn(manager);
this.request.setAsyncSupported(true);
}
@AfterEach
void tearDown() {
void cleanup() {
assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse();
assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse();
}
@Test
void openEntityManagerInViewInterceptor() {
OpenEntityManagerInViewInterceptor interceptor = new OpenEntityManagerInViewInterceptor();
@@ -74,7 +74,7 @@ class PersistenceContextTransactionTests {
}
@AfterEach
void clear() {
void cleanup() {
assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty();
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse();