From 394a97d7e047a4675f76502ca1689e108a42eced Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 8 Feb 2026 17:54:02 +0100 Subject: [PATCH] Add setPackagesToScan to LocalEntityManagerFactoryBean (and make setDataSource work) Includes fix for consistent PersistenceException in case of no unit found for name. Includes proper tests for Local(Container)EntityManagerFactoryBean with scan setup. Closes gh-36270 Closes gh-36271 --- ...ocalContainerEntityManagerFactoryBean.java | 3 - .../jpa/LocalEntityManagerFactoryBean.java | 78 +++++++++++++++++-- .../springframework/orm/jpa/domain/Car.java | 1 + .../orm/jpa/domain/Employee.java | 1 + .../orm/jpa/domain/EmployeeCategory.java | 2 + .../jpa/domain/EmployeeCategoryConverter.java | 3 + .../orm/jpa/domain/EmployeeKind.java | 2 + .../orm/jpa/domain/EmployeeKindConverter.java | 3 + .../orm/jpa/domain/EmployeeLocation.java | 1 + .../jpa/domain/EmployeeLocationConverter.java | 1 + ...ityManagerFactoryScanIntegrationTests.java | 30 +++++++ ...yManagerFactorySimpleIntegrationTests.java | 45 +++++++++++ ...ityManagerFactoryScanIntegrationTests.java | 30 +++++++ ...yManagerFactorySimpleIntegrationTests.java | 45 +++++++++++ ...ypesBeanRegistrationAotProcessorTests.java | 3 +- .../PersistenceManagedTypesScannerTests.java | 14 +++- .../eclipselink/eclipselink-manager-scan.xml | 29 +++++++ .../eclipselink-manager-simple.xml | 29 +++++++ .../jpa/eclipselink/eclipselink-manager.xml | 10 +-- .../jpa/hibernate/hibernate-manager-scan.xml | 42 ++++++++++ .../hibernate/hibernate-manager-simple.xml | 42 ++++++++++ 21 files changed, 393 insertions(+), 21 deletions(-) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactoryScanIntegrationTests.java create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactorySimpleIntegrationTests.java create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryScanIntegrationTests.java create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactorySimpleIntegrationTests.java create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-scan.xml create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-simple.xml create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-scan.xml create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-simple.xml diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index 94fa087dead..7cc91d73b12 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -86,14 +86,11 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @author Rod Johnson * @since 2.0 - * @see #setPersistenceXmlLocation * @see #setJpaProperties * @see #setJpaVendorAdapter * @see #setLoadTimeWeaver * @see #setDataSource - * @see EntityManagerFactoryInfo * @see LocalEntityManagerFactoryBean - * @see org.springframework.orm.jpa.support.SharedEntityManagerBean * @see jakarta.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory */ @SuppressWarnings("serial") 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 f6e8386dd3e..0880b12ac1e 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,9 @@ package org.springframework.orm.jpa; +import java.util.LinkedHashSet; +import java.util.Set; + import javax.sql.DataSource; import jakarta.persistence.EntityManagerFactory; @@ -25,8 +28,16 @@ import jakarta.persistence.PersistenceException; import jakarta.persistence.spi.PersistenceProvider; import org.jspecify.annotations.Nullable; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypesScanner; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link org.springframework.beans.factory.FactoryBean} that creates a JPA @@ -54,18 +65,26 @@ import org.springframework.util.Assert; * @since 2.0 * @see #setJpaProperties * @see #setJpaVendorAdapter - * @see JpaTransactionManager#setEntityManagerFactory + * @see #setPersistenceConfiguration + * @see #setDataSource * @see LocalContainerEntityManagerFactoryBean - * @see org.springframework.jndi.JndiObjectFactoryBean - * @see org.springframework.orm.jpa.support.SharedEntityManagerBean * @see jakarta.persistence.Persistence#createEntityManagerFactory * @see jakarta.persistence.spi.PersistenceProvider#createEntityManagerFactory */ @SuppressWarnings("serial") -public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean { +public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean implements ResourceLoaderAware { + + private static final String NON_JTA_DATASOURCE_PROPERTY = "jakarta.persistence.nonJtaDataSource"; + + private static final String PACKAGE_INFO_SUFFIX = ".package-info"; + private @Nullable PersistenceConfiguration configuration; + private String @Nullable [] packagesToScan; + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + /** * Create a {@code LocalEntityManagerFactoryBean}. @@ -151,6 +170,27 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB super.setPersistenceUnitName(persistenceUnitName); } + /** + * 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} + * markers in them. In case of Spring-based scanning, no {@code persistence.xml} + * is necessary; all you need to do is to specify base packages to search here. + *

Default is none. Specify packages to search for autodetection of your entity + * classes in the classpath. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + *

The use of this setter switches this {@code LocalEntityManagerFactoryBean} + * to a {@link #getPersistenceConfiguration() PersistenceConfiguration}, with no + * {@code persistence.xml} reading or provider-driven scanning happening anymore. + * Further JPA settings can be applied on the local {@link PersistenceConfiguration} + * via {@link #getPersistenceConfiguration()}. + * @since 7.0.4 + * @see LocalContainerEntityManagerFactoryBean#setPackagesToScan + * @see org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean#setPackagesToScan + */ + public void setPackagesToScan(String... packagesToScan) { + this.packagesToScan = packagesToScan; + } + /** * Specify the JDBC DataSource that the JPA persistence provider is supposed * to use for accessing the database. This is an alternative to keeping the @@ -165,9 +205,11 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB public void setDataSource(@Nullable DataSource dataSource) { if (dataSource != null) { getJpaPropertyMap().put(PersistenceConfiguration.JDBC_DATASOURCE, dataSource); + getJpaPropertyMap().put(NON_JTA_DATASOURCE_PROPERTY, dataSource); } else { getJpaPropertyMap().remove(PersistenceConfiguration.JDBC_DATASOURCE); + getJpaPropertyMap().remove(NON_JTA_DATASOURCE_PROPERTY); } } @@ -182,6 +224,11 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB return (DataSource) getJpaPropertyMap().get(PersistenceConfiguration.JDBC_DATASOURCE); } + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + /** * Initialize the EntityManagerFactory for the given configuration. @@ -193,6 +240,25 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB logger.debug("Building JPA EntityManagerFactory for persistence unit '" + getPersistenceUnitName() + "'"); } + if (this.packagesToScan != null) { + 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 + 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)); + } + } + if (this.configuration != null) { this.configuration.properties(getJpaPropertyMap()); } @@ -204,8 +270,8 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB provider.createEntityManagerFactory(this.configuration) : provider.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap())); if (emf == null) { - throw new IllegalStateException( - "PersistenceProvider [" + provider + "] did not return an EntityManagerFactory for name '" + + throw new PersistenceException( + "PersistenceProvider [" + provider + "] could not find persistence unit for name '" + getPersistenceUnitName() + "'"); } return emf; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Car.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Car.java index 144e55fdfc4..7b9f3b59e2e 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Car.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Car.java @@ -47,4 +47,5 @@ public class Car { String getModel() { return model; } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Employee.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Employee.java index 4459399f50d..cdf456c0098 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Employee.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Employee.java @@ -86,4 +86,5 @@ public class Employee { @PreRemove public void preRemove() { } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategory.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategory.java index 0b878f9058a..55054fa286a 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategory.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategory.java @@ -17,6 +17,7 @@ package org.springframework.orm.jpa.domain; public class EmployeeCategory { + private String name; public String getName() { @@ -26,4 +27,5 @@ public class EmployeeCategory { public void setName(String name) { this.name = name; } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategoryConverter.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategoryConverter.java index 3cccaee608c..2b6656dbf3c 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategoryConverter.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/EmployeeCategoryConverter.java @@ -17,7 +17,9 @@ package org.springframework.orm.jpa.domain; import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +@Converter public class EmployeeCategoryConverter implements AttributeConverter { @Override @@ -37,4 +39,5 @@ public class EmployeeCategoryConverter implements AttributeConverter { @Override @@ -37,4 +39,5 @@ public class EmployeeKindConverter implements AttributeConverter candidates = List.of(Person.class.getName(), DriversLicense.class.getName()); PersistenceManagedTypes managedTypes = new PersistenceManagedTypesScanner( - RESOURCE_LOADER, candidates::contains).scan("org.springframework.orm.jpa.domain"); + resourceLoader, candidates::contains).scan("org.springframework.orm.jpa.domain"); assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder( Person.class.getName(), DriversLicense.class.getName()); assertThat(managedTypes.getManagedPackages()).isEmpty(); diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-scan.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-scan.xml new file mode 100644 index 00000000000..e8c02504b87 --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-scan.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + false + + + + + + + + + diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-simple.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-simple.xml new file mode 100644 index 00000000000..95cfc29af24 --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager-simple.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + false + + + + + + + + + diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager.xml index e2ef60dd32c..c535edd6d09 100644 --- a/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager.xml +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/eclipselink/eclipselink-manager.xml @@ -1,14 +1,10 @@ - - - + - diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-scan.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-scan.xml new file mode 100644 index 00000000000..ba8ebdf8fc4 --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-scan.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + org.springframework.orm.jpa.hibernate.SpringSessionContext + org.hibernate.cache.HashtableCacheProvider + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-simple.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-simple.xml new file mode 100644 index 00000000000..92e63b375a1 --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-simple.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + org.springframework.orm.jpa.hibernate.SpringSessionContext + org.hibernate.cache.HashtableCacheProvider + + + + + + + + + + + + + + + + + + + + +