Add runtime compatibility with JPA 4.0 M7 and Hibernate ORM 8.0

Closes gh-37315
This commit is contained in:
Juergen Hoeller
2026-10-05 17:20:28 +02:00
parent bda69f0ce2
commit ee1aac605b
4 changed files with 65 additions and 22 deletions
@@ -16,6 +16,7 @@
package org.springframework.orm.jpa;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -38,6 +39,7 @@ import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypesScanner;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link org.springframework.beans.factory.FactoryBean} that creates a JPA
@@ -76,6 +78,9 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
private static final String NON_JTA_DATASOURCE_PROPERTY = "jakarta.persistence.nonJtaDataSource";
private static final @Nullable Method MANAGED_PACKAGE_DESCRIPTOR_METHOD =
ClassUtils.getMethodIfAvailable(PersistenceConfiguration.class, "managedPackageDescriptor", String.class);
private @Nullable PersistenceConfiguration configuration;
private String @Nullable [] packagesToScan;
@@ -242,18 +247,29 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
}
if (this.packagesToScan != null) {
PersistenceConfiguration config = getPersistenceConfiguration();
PersistenceManagedTypesScanner scanner = new PersistenceManagedTypesScanner(this.resourcePatternResolver);
PersistenceManagedTypes result = scanner.scan(this.packagesToScan);
// Expose managed class names from scan result (on JPA 4.0+, this includes
// everything meta-annotated with @Discoverable, even package-info classes)
Set<String> classNameSet = new LinkedHashSet<>(result.getManagedClassNames());
// Expose managed packages as package-info class names if not included already
// (accepted by PersistenceConfiguration on Hibernate as well as EclipseLink)
for (String managedPackage : result.getManagedPackages()) {
classNameSet.add(managedPackage + ClassUtils.PACKAGE_INFO_SUFFIX);
if (MANAGED_PACKAGE_DESCRIPTOR_METHOD != null) { // on JPA 4.0
for (String managedPackage : result.getManagedPackages()) {
ReflectionUtils.invokeMethod(MANAGED_PACKAGE_DESCRIPTOR_METHOD, config, managedPackage);
classNameSet.remove(managedPackage + ClassUtils.PACKAGE_INFO_SUFFIX);
}
}
else {
for (String managedPackage : result.getManagedPackages()) {
classNameSet.add(managedPackage + ClassUtils.PACKAGE_INFO_SUFFIX);
}
}
// Expose pre-resolved Class references to PersistenceConfiguration.
PersistenceConfiguration config = getPersistenceConfiguration();
ClassLoader classLoader = this.resourcePatternResolver.getClassLoader();
for (String className : classNameSet) {
config.managedClass(ClassUtils.resolveClassName(className, classLoader));
@@ -34,8 +34,6 @@ import org.hibernate.StaleStateException;
import org.hibernate.TransientObjectException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.WrongClassException;
import org.hibernate.dialect.lock.OptimisticEntityLockException;
import org.hibernate.dialect.lock.PessimisticEntityLockException;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.DataException;
import org.hibernate.exception.JDBCConnectionException;
@@ -206,10 +204,10 @@ public class HibernateExceptionTranslator implements PersistenceExceptionTransla
if (exToCheck instanceof StaleStateException) {
return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex);
}
if (exToCheck instanceof OptimisticEntityLockException) {
if (exToCheck.getClass().getSimpleName().equals("OptimisticEntityLockException")) {
return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex);
}
if (exToCheck instanceof PessimisticEntityLockException) {
if (exToCheck.getClass().getSimpleName().equals("PessimisticEntityLockException")) {
if (ex.getCause() instanceof LockAcquisitionException) {
return new CannotAcquireLockException(ex.getMessage(), ex.getCause());
}
@@ -16,6 +16,7 @@
package org.springframework.orm.jpa.hibernate;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
@@ -24,6 +25,7 @@ import org.apache.commons.logging.LogFactory;
import org.hibernate.resource.beans.container.spi.BeanContainer;
import org.hibernate.resource.beans.container.spi.ContainedBean;
import org.hibernate.resource.beans.spi.BeanInstanceProducer;
import org.hibernate.resource.beans.spi.ManagedBean;
import org.hibernate.type.spi.TypeBootstrapContext;
import org.jspecify.annotations.Nullable;
@@ -133,6 +135,22 @@ public final class SpringBeanContainer implements BeanContainer {
return (SpringContainedBean<B>) bean;
}
// @Override - on Hibernate 8.0
public <B> ContainedBean<B> getBootstrapSafeBean(
Class<B> beanType, LifecycleOptions lifecycleOptions, BeanInstanceProducer fallbackProducer) {
// Fallback implementation for Hibernate 8.0 runtime compatibility
return getBean(beanType, lifecycleOptions, fallbackProducer);
}
// @Override - on Hibernate 8.0
public void releaseBean(ManagedBean<?> bean) {
if (bean instanceof SpringContainedBean<?> contained) {
this.beanCache.values().removeAll(Collections.singleton(contained));
contained.destroyIfNecessary();
}
}
@Override
public void stop() {
this.beanCache.values().forEach(SpringContainedBean::destroyIfNecessary);
@@ -207,9 +225,7 @@ public final class SpringBeanContainer implements BeanContainer {
else {
// No bean found by name -> construct by type using createBean
return new SpringContainedBean<>(
beanType,
this.beanFactory.createBean(beanType),
this.beanFactory::destroyBean);
beanType, this.beanFactory.createBean(beanType), this.beanFactory::destroyBean);
}
}
else {
@@ -272,6 +288,16 @@ public final class SpringBeanContainer implements BeanContainer {
return this.beanClass;
}
// @Override - on Hibernate 8.0
public void initialize() {
getBeanInstance();
}
// @Override - on Hibernate 8.0
public void release() {
destroyIfNecessary();
}
public void destroyIfNecessary() {
if (this.destructionCallback != null) {
this.destructionCallback.accept(this.beanInstance);
@@ -21,6 +21,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jakarta.persistence.FetchType;
@@ -262,21 +263,23 @@ public class SpringPersistenceUnitInfo extends MutablePersistenceUnitInfo {
// Fast path for SmartPersistenceUnitInfo JTA check
return (getTransactionType() == PersistenceUnitTransactionType.JTA);
}
else if (method.getName().equals("getAllClassNames")) {
// JPA 4.0 letting the container perform the scanning
if (excludeUnlistedClasses()) { // typically coming from Spring default persistence unit
List<String> mergedClassesAndPackages =
new ArrayList<>(getManagedClassNames().size() + getManagedPackages().size());
mergedClassesAndPackages.addAll(getManagedClassNames());
for (String managedPackage : getManagedPackages()) {
mergedClassesAndPackages.add(managedPackage + ClassUtils.PACKAGE_INFO_SUFFIX);
}
return mergedClassesAndPackages;
}
if (method.getName().startsWith("getAll") && !excludeUnlistedClasses()) {
throw new UnsupportedOperationException(
"JPA 4.0 getAllClassNames only supported with Spring-configured packagesToScan or " +
"JPA 4.0 getAll* methods only supported with Spring-configured packagesToScan or " +
"with completely listed managed classes plus exclude-unlisted-classes=true");
}
switch (method.getName()) {
case "getAllClassNames" -> {
return getManagedClassNames();
}
case "getAllPackageDescriptors", "getManagedPackageDescriptors" -> {
return getManagedPackages();
}
case "getAllModuleDescriptors", "getManagedModuleDescriptors" -> {
return Collections.emptyList();
}
}
// Regular methods to be delegated to SpringPersistenceUnitInfo
Method targetMethod = SpringPersistenceUnitInfo.class.getMethod(method.getName(), method.getParameterTypes());