diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java index a392b3ddb2e..59e572acd1b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -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 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)); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateExceptionTranslator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateExceptionTranslator.java index 2e4fc0bdd05..9b1b3a02c3b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateExceptionTranslator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateExceptionTranslator.java @@ -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()); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringBeanContainer.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringBeanContainer.java index 80b70a53ed6..009252d7fae 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringBeanContainer.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringBeanContainer.java @@ -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) bean; } + // @Override - on Hibernate 8.0 + public ContainedBean getBootstrapSafeBean( + Class 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); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java index dc1051f5cf5..f0c59c46854 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java @@ -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 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());