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 a77d4f0b1a5..e499e4ca93e 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import org.springframework.instrument.classloading.LoadTimeWeaver;
import org.springframework.jdbc.datasource.lookup.SingleDataSourceLookup;
import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager;
+import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo;
@@ -157,6 +158,17 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
this.internalPersistenceUnitManager.setDefaultPersistenceUnitRootLocation(defaultPersistenceUnitRootLocation);
}
+ /**
+ * Set the {@link PersistenceManagedTypes} to use to build the list of managed types
+ * as an alternative to entity scanning.
+ * @param managedTypes the managed types
+ * @since 6.0
+ * @see DefaultPersistenceUnitManager#setManagedTypes(PersistenceManagedTypes)
+ */
+ public void setManagedTypes(PersistenceManagedTypes managedTypes) {
+ this.internalPersistenceUnitManager.setManagedTypes(managedTypes);
+ }
+
/**
* 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}
@@ -165,6 +177,8 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
*
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}).
+ *
Consider setting a {@link PersistenceManagedTypes} instead that allows the
+ * scanning logic to be optimized by AOT processing.
*
Note: There may be limitations in comparison to regular JPA scanning.
* In particular, JPA providers may pick up annotated packages for provider-specific
* annotations only when driven by {@code persistence.xml}. As of 4.1, Spring's
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java
index af39d7ea125..e4e1eaea60f 100644
--- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,23 +16,17 @@
package org.springframework.orm.jpa.persistenceunit;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
-import jakarta.persistence.Converter;
-import jakarta.persistence.Embeddable;
-import jakarta.persistence.Entity;
-import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.SharedCacheMode;
import jakarta.persistence.ValidationMode;
@@ -42,26 +36,18 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
-import org.springframework.context.index.CandidateComponentsIndex;
-import org.springframework.context.index.CandidateComponentsIndexLoader;
import org.springframework.context.weaving.LoadTimeWeaverAware;
import org.springframework.core.io.Resource;
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.core.type.classreading.CachingMetadataReaderFactory;
-import org.springframework.core.type.classreading.MetadataReader;
-import org.springframework.core.type.classreading.MetadataReaderFactory;
-import org.springframework.core.type.filter.AnnotationTypeFilter;
-import org.springframework.core.type.filter.TypeFilter;
import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver;
import org.springframework.instrument.classloading.LoadTimeWeaver;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.jdbc.datasource.lookup.MapDataSourceLookup;
import org.springframework.lang.Nullable;
-import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
@@ -73,6 +59,9 @@ import org.springframework.util.ResourceUtils;
*
Supports standard JPA scanning for {@code persistence.xml} files,
* with configurable file locations, JDBC DataSource lookup and load-time weaving.
*
+ *
Builds a persistence unit based on the state of a {@link PersistenceManagedTypes},
+ * typically built using a {@link PersistenceManagedTypesScanner}.
+ *
* The default XML file location is {@code classpath*:META-INF/persistence.xml},
* scanning for all matching files in the classpath (as defined in the JPA specification).
* DataSource names are by default interpreted as JNDI names, and no load time weaving
@@ -89,10 +78,6 @@ import org.springframework.util.ResourceUtils;
public class DefaultPersistenceUnitManager
implements PersistenceUnitManager, ResourceLoaderAware, LoadTimeWeaverAware, InitializingBean {
- private static final String CLASS_RESOURCE_PATTERN = "/**/*.class";
-
- private static final String PACKAGE_INFO_SUFFIX = ".package-info";
-
private static final String DEFAULT_ORM_XML_RESOURCE = "META-INF/orm.xml";
private static final String PERSISTENCE_XML_FILENAME = "persistence.xml";
@@ -115,17 +100,6 @@ public class DefaultPersistenceUnitManager
public static final String ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME = "default";
- private static final Set entityTypeFilters;
-
- static {
- entityTypeFilters = new LinkedHashSet<>(8);
- entityTypeFilters.add(new AnnotationTypeFilter(Entity.class, false));
- entityTypeFilters.add(new AnnotationTypeFilter(Embeddable.class, false));
- entityTypeFilters.add(new AnnotationTypeFilter(MappedSuperclass.class, false));
- entityTypeFilters.add(new AnnotationTypeFilter(Converter.class, false));
- }
-
-
protected final Log logger = LogFactory.getLog(getClass());
private String[] persistenceXmlLocations = new String[] {DEFAULT_PERSISTENCE_XML_LOCATION};
@@ -136,6 +110,9 @@ public class DefaultPersistenceUnitManager
@Nullable
private String defaultPersistenceUnitName = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME;
+ @Nullable
+ private PersistenceManagedTypes managedTypes;
+
@Nullable
private String[] packagesToScan;
@@ -164,9 +141,6 @@ public class DefaultPersistenceUnitManager
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
- @Nullable
- private CandidateComponentsIndex componentsIndex;
-
private final Set persistenceUnitInfoNames = new HashSet<>();
private final Map persistenceUnitInfos = new HashMap<>();
@@ -214,6 +188,16 @@ public class DefaultPersistenceUnitManager
this.defaultPersistenceUnitName = defaultPersistenceUnitName;
}
+ /**
+ * Set the {@link PersistenceManagedTypes} to use to build the list of managed types
+ * as an alternative to entity scanning.
+ * @param managedTypes the managed types
+ * @since 6.0
+ */
+ public void setManagedTypes(PersistenceManagedTypes managedTypes) {
+ this.managedTypes = managedTypes;
+ }
+
/**
* 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}
@@ -222,6 +206,8 @@ public class DefaultPersistenceUnitManager
* 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}).
+ *
Consider setting a {@link PersistenceManagedTypes} instead that allows the
+ * scanning logic to be optimized by AOT processing.
*
Such package scanning defines a "default persistence unit" in Spring, which
* may live next to regularly defined units originating from {@code persistence.xml}.
* Its name is determined by {@link #setDefaultPersistenceUnitName}: by default,
@@ -237,6 +223,7 @@ public class DefaultPersistenceUnitManager
* resource for the default unit if the mapping file is not co-located with a
* {@code persistence.xml} file (in which case we assume it is only meant to be
* used with the persistence units defined there, like in standard JPA).
+ * @see #setManagedTypes(PersistenceManagedTypes)
* @see #setDefaultPersistenceUnitName
* @see #setMappingResources
*/
@@ -431,7 +418,6 @@ public class DefaultPersistenceUnitManager
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
- this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader());
}
@@ -499,7 +485,7 @@ public class DefaultPersistenceUnitManager
private List readPersistenceUnitInfos() {
List infos = new ArrayList<>(1);
String defaultName = this.defaultPersistenceUnitName;
- boolean buildDefaultUnit = (this.packagesToScan != null || this.mappingResources != null);
+ boolean buildDefaultUnit = (this.managedTypes != null || this.packagesToScan != null || this.mappingResources != null);
boolean foundDefaultUnit = false;
PersistenceUnitReader reader = new PersistenceUnitReader(this.resourcePatternResolver, this.dataSourceLookup);
@@ -515,7 +501,7 @@ public class DefaultPersistenceUnitManager
if (foundDefaultUnit) {
if (logger.isWarnEnabled()) {
logger.warn("Found explicit default persistence unit with name '" + defaultName + "' in persistence.xml - " +
- "overriding local default persistence unit settings ('packagesToScan'/'mappingResources')");
+ "overriding local default persistence unit settings ('managedTypes', 'packagesToScan' or 'mappingResources')");
}
}
else {
@@ -536,10 +522,12 @@ public class DefaultPersistenceUnitManager
}
scannedUnit.setExcludeUnlistedClasses(true);
- if (this.packagesToScan != null) {
- for (String pkg : this.packagesToScan) {
- scanPackage(scannedUnit, pkg);
- }
+ if (this.managedTypes != null) {
+ applyManagedTypes(scannedUnit, this.managedTypes);
+ }
+ else if (this.packagesToScan != null) {
+ applyManagedTypes(scannedUnit, new PersistenceManagedTypesScanner(
+ this.resourcePatternResolver).scan(this.packagesToScan));
}
if (this.mappingResources != null) {
@@ -566,62 +554,13 @@ public class DefaultPersistenceUnitManager
return scannedUnit;
}
- private void scanPackage(SpringPersistenceUnitInfo scannedUnit, String pkg) {
- if (this.componentsIndex != null) {
- Set candidates = new HashSet<>();
- for (AnnotationTypeFilter filter : entityTypeFilters) {
- candidates.addAll(this.componentsIndex.getCandidateTypes(pkg, filter.getAnnotationType().getName()));
- }
- candidates.forEach(scannedUnit::addManagedClassName);
- Set managedPackages = this.componentsIndex.getCandidateTypes(pkg, "package-info");
- managedPackages.forEach(scannedUnit::addManagedPackage);
- return;
+ private void applyManagedTypes(SpringPersistenceUnitInfo scannedUnit, PersistenceManagedTypes managedTypes) {
+ managedTypes.getManagedClassNames().forEach(scannedUnit::addManagedClassName);
+ managedTypes.getManagedPackages().forEach(scannedUnit::addManagedPackage);
+ URL persistenceUnitRootUrl = managedTypes.getPersistenceUnitRootUrl();
+ if (scannedUnit.getPersistenceUnitRootUrl() == null && persistenceUnitRootUrl != null) {
+ scannedUnit.setPersistenceUnitRootUrl(persistenceUnitRootUrl);
}
-
- try {
- String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
- ClassUtils.convertClassNameToResourcePath(pkg) + CLASS_RESOURCE_PATTERN;
- Resource[] resources = this.resourcePatternResolver.getResources(pattern);
- MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
- for (Resource resource : resources) {
- try {
- MetadataReader reader = readerFactory.getMetadataReader(resource);
- String className = reader.getClassMetadata().getClassName();
- if (matchesFilter(reader, readerFactory)) {
- scannedUnit.addManagedClassName(className);
- if (scannedUnit.getPersistenceUnitRootUrl() == null) {
- URL url = resource.getURL();
- if (ResourceUtils.isJarURL(url)) {
- scannedUnit.setPersistenceUnitRootUrl(ResourceUtils.extractJarFileURL(url));
- }
- }
- }
- else if (className.endsWith(PACKAGE_INFO_SUFFIX)) {
- scannedUnit.addManagedPackage(
- className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length()));
- }
- }
- catch (FileNotFoundException ex) {
- // Ignore non-readable resource
- }
- }
- }
- catch (IOException ex) {
- throw new PersistenceException("Failed to scan classpath for unlisted entity classes", ex);
- }
- }
-
- /**
- * Check whether any of the configured entity type filters matches
- * the current class descriptor contained in the metadata reader.
- */
- private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
- for (TypeFilter filter : entityTypeFilters) {
- if (filter.match(reader, readerFactory)) {
- return true;
- }
- }
- return false;
}
/**
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java
new file mode 100644
index 00000000000..738b14294fa
--- /dev/null
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.net.URL;
+import java.util.List;
+
+import jakarta.persistence.spi.PersistenceUnitInfo;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Provide the list of managed persistent types that an entity manager should
+ * consider.
+ *
+ * @author Stephane Nicoll
+ * @since 6.0
+ */
+public interface PersistenceManagedTypes {
+
+ /**
+ * Return the class names the persistence provider must add to its set of
+ * managed classes.
+ * @return the managed class names
+ * @see PersistenceUnitInfo#getManagedClassNames()
+ */
+ List getManagedClassNames();
+
+ /**
+ * Return a list of managed Java packages, to be introspected by the
+ * persistence provider.
+ * @return the managed packages
+ */
+ List getManagedPackages();
+
+ /**
+ * Return the persistence unit root url or {@code null} if it could not be
+ * determined.
+ * @return the persistence unit root url
+ * @see PersistenceUnitInfo#getPersistenceUnitRootUrl()
+ */
+ @Nullable
+ URL getPersistenceUnitRootUrl();
+
+ /**
+ * Create an instance using the specified managed class names.
+ * @param managedClassNames the managed class names
+ * @return a {@link PersistenceManagedTypes}
+ */
+ static PersistenceManagedTypes of(String... managedClassNames) {
+ Assert.notNull(managedClassNames, "'managedClassNames' must not be null");
+ return new SimplePersistenceManagedTypes(List.of(managedClassNames), List.of());
+ }
+
+ /**
+ * Create an instance using the specified managed class names and packages.
+ * @param managedClassNames the managed class names
+ * @param managedPackages the managed packages
+ * @return a {@link PersistenceManagedTypes}
+ */
+ static PersistenceManagedTypes of(List managedClassNames, List managedPackages) {
+ return new SimplePersistenceManagedTypes(managedClassNames, managedPackages);
+ }
+
+}
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java
new file mode 100644
index 00000000000..a6b6684d228
--- /dev/null
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.lang.reflect.Executable;
+import java.util.List;
+
+import javax.lang.model.element.Modifier;
+
+import org.springframework.aot.generate.GeneratedMethod;
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
+import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
+import org.springframework.beans.factory.aot.BeanRegistrationCode;
+import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
+import org.springframework.beans.factory.support.RegisteredBean;
+import org.springframework.javapoet.CodeBlock;
+import org.springframework.javapoet.ParameterizedTypeName;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link BeanRegistrationAotProcessor} implementations for persistence managed
+ * types.
+ *
+ * Allows a {@link PersistenceManagedTypes} to be instantiated at build-time
+ * and replaced by a hard-coded list of managed class names and packages.
+ *
+ * @author Stephane Nicoll
+ * @since 6.0
+ */
+class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
+
+ @Nullable
+ @Override
+ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
+ if (PersistenceManagedTypes.class.isAssignableFrom(registeredBean.getBeanClass())) {
+ return BeanRegistrationAotContribution.ofBeanRegistrationCodeFragmentsCustomizer(codeFragments ->
+ new JpaManagedTypesBeanRegistrationCodeFragments(codeFragments, registeredBean));
+ }
+ return null;
+ }
+
+ private static class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRegistrationCodeFragments {
+
+ private static final ParameterizedTypeName LIST_OF_STRINGS_TYPE = ParameterizedTypeName.get(List.class, String.class);
+
+ private final RegisteredBean registeredBean;
+
+ public JpaManagedTypesBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments,
+ RegisteredBean registeredBean) {
+ super(codeFragments);
+ this.registeredBean = registeredBean;
+ }
+
+ @Override
+ public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
+ BeanRegistrationCode beanRegistrationCode,
+ Executable constructorOrFactoryMethod,
+ boolean allowDirectSupplierShortcut) {
+ PersistenceManagedTypes persistenceManagedTypes = this.registeredBean.getBeanFactory()
+ .getBean(this.registeredBean.getBeanName(), PersistenceManagedTypes.class);
+ GeneratedMethod generatedMethod = beanRegistrationCode.getMethods()
+ .add("getInstance", method -> {
+ Class> beanType = PersistenceManagedTypes.class;
+ method.addJavadoc("Get the bean instance for '$L'.",
+ this.registeredBean.getBeanName());
+ method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
+ method.returns(beanType);
+ method.addStatement("$T managedClassNames = $T.of($L)", LIST_OF_STRINGS_TYPE,
+ List.class, toCodeBlock(persistenceManagedTypes.getManagedClassNames()));
+ method.addStatement("$T managedPackages = $T.of($L)", LIST_OF_STRINGS_TYPE,
+ List.class, toCodeBlock(persistenceManagedTypes.getManagedPackages()));
+ method.addStatement("return $T.of($L, $L)", beanType, "managedClassNames", "managedPackages");
+ });
+ return CodeBlock.of("() -> $T.$L()", beanRegistrationCode.getClassName(), generatedMethod.getName());
+ }
+
+ private CodeBlock toCodeBlock(List values) {
+ return CodeBlock.join(values.stream().map(value -> CodeBlock.of("$S", value)).toList(), ", ");
+ }
+
+ }
+}
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java
new file mode 100644
index 00000000000..74ab9bb8e77
--- /dev/null
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.Converter;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.Entity;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.PersistenceException;
+
+import org.springframework.context.index.CandidateComponentsIndex;
+import org.springframework.context.index.CandidateComponentsIndexLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.core.type.filter.TypeFilter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ResourceUtils;
+
+/**
+ * Scanner of {@link PersistenceManagedTypes}.
+ *
+ * @author Stephane Nicoll
+ * @since 6.0
+ */
+public final class PersistenceManagedTypesScanner {
+
+ private static final String CLASS_RESOURCE_PATTERN = "/**/*.class";
+
+ private static final String PACKAGE_INFO_SUFFIX = ".package-info";
+
+ private static final Set entityTypeFilters;
+
+ static {
+ entityTypeFilters = new LinkedHashSet<>(8);
+ entityTypeFilters.add(new AnnotationTypeFilter(Entity.class, false));
+ entityTypeFilters.add(new AnnotationTypeFilter(Embeddable.class, false));
+ entityTypeFilters.add(new AnnotationTypeFilter(MappedSuperclass.class, false));
+ entityTypeFilters.add(new AnnotationTypeFilter(Converter.class, false));
+ }
+
+ private final ResourcePatternResolver resourcePatternResolver;
+
+ @Nullable
+ private final CandidateComponentsIndex componentsIndex;
+
+
+ public PersistenceManagedTypesScanner(ResourceLoader resourceLoader) {
+ this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
+ this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader());
+ }
+
+ /**
+ * Scan the specified packages and return a {@link PersistenceManagedTypes} that
+ * represents the result of the scanning.
+ * @param packagesToScan the packages to scan
+ * @return the {@link PersistenceManagedTypes} instance
+ */
+ public PersistenceManagedTypes scan(String... packagesToScan) {
+ ScanResult scanResult = new ScanResult();
+ for (String pkg : packagesToScan) {
+ scanPackage(pkg, scanResult);
+ }
+ return scanResult.toJpaManagedTypes();
+ }
+
+ private void scanPackage(String pkg, ScanResult scanResult) {
+ if (this.componentsIndex != null) {
+ Set candidates = new HashSet<>();
+ for (AnnotationTypeFilter filter : entityTypeFilters) {
+ candidates.addAll(this.componentsIndex.getCandidateTypes(pkg, filter.getAnnotationType().getName()));
+ }
+ scanResult.managedClassNames.addAll(candidates);
+ scanResult.managedPackages.addAll(this.componentsIndex.getCandidateTypes(pkg, "package-info"));
+ return;
+ }
+
+ try {
+ String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
+ ClassUtils.convertClassNameToResourcePath(pkg) + CLASS_RESOURCE_PATTERN;
+ Resource[] resources = this.resourcePatternResolver.getResources(pattern);
+ MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
+ for (Resource resource : resources) {
+ try {
+ MetadataReader reader = readerFactory.getMetadataReader(resource);
+ String className = reader.getClassMetadata().getClassName();
+ if (matchesFilter(reader, readerFactory)) {
+ scanResult.managedClassNames.add(className);
+ if (scanResult.persistenceUnitRootUrl == null) {
+ URL url = resource.getURL();
+ if (ResourceUtils.isJarURL(url)) {
+ scanResult.persistenceUnitRootUrl = ResourceUtils.extractJarFileURL(url);
+ }
+ }
+ }
+ else if (className.endsWith(PACKAGE_INFO_SUFFIX)) {
+ scanResult.managedPackages.add(className.substring(0,
+ className.length() - PACKAGE_INFO_SUFFIX.length()));
+ }
+ }
+ catch (FileNotFoundException ex) {
+ // Ignore non-readable resource
+ }
+ }
+ }
+ catch (IOException ex) {
+ throw new PersistenceException("Failed to scan classpath for unlisted entity classes", ex);
+ }
+ }
+
+ /**
+ * Check whether any of the configured entity type filters matches
+ * the current class descriptor contained in the metadata reader.
+ */
+ private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
+ for (TypeFilter filter : entityTypeFilters) {
+ if (filter.match(reader, readerFactory)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class ScanResult {
+
+ private final List managedClassNames = new ArrayList<>();
+
+ private final List managedPackages = new ArrayList<>();
+
+ @Nullable
+ private URL persistenceUnitRootUrl;
+
+ PersistenceManagedTypes toJpaManagedTypes() {
+ return new SimplePersistenceManagedTypes(this.managedClassNames,
+ this.managedPackages, this.persistenceUnitRootUrl);
+ }
+
+ }
+}
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SimplePersistenceManagedTypes.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SimplePersistenceManagedTypes.java
new file mode 100644
index 00000000000..6a9b54642c0
--- /dev/null
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SimplePersistenceManagedTypes.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.net.URL;
+import java.util.List;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * A simple {@link PersistenceManagedTypes} implementation that holds the list
+ * of managed entities.
+ *
+ * @author Stephane Nicoll
+ * @since 6.0
+ */
+class SimplePersistenceManagedTypes implements PersistenceManagedTypes {
+
+ private final List managedClassNames;
+
+ private final List managedPackages;
+
+ @Nullable
+ private final URL persistenceUnitRootUrl;
+
+
+ SimplePersistenceManagedTypes(List managedClassNames, List managedPackages,
+ @Nullable URL persistenceUnitRootUrl) {
+ this.managedClassNames = managedClassNames;
+ this.managedPackages = managedPackages;
+ this.persistenceUnitRootUrl = persistenceUnitRootUrl;
+ }
+
+ SimplePersistenceManagedTypes(List managedClassNames, List managedPackages) {
+ this(managedClassNames, managedPackages, null);
+ }
+
+ @Override
+ public List getManagedClassNames() {
+ return this.managedClassNames;
+ }
+
+ @Override
+ public List getManagedPackages() {
+ return this.managedPackages;
+ }
+
+ @Override
+ @Nullable
+ public URL getPersistenceUnitRootUrl() {
+ return this.persistenceUnitRootUrl;
+ }
+
+}
diff --git a/spring-orm/src/main/resources/META-INF/spring/aot.factories b/spring-orm/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 00000000000..4b2e8097817
--- /dev/null
+++ b/spring-orm/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1,2 @@
+org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
+org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypesBeanRegistrationAotProcessor
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/entity/User.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/entity/User.java
new file mode 100644
index 00000000000..a05641cecf8
--- /dev/null
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/entity/User.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.domain2.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Integer id;
+
+ private String firstName;
+
+ private String lastName;
+
+ public Integer getId() {
+ return this.id;
+ }
+
+ public String getFirstName() {
+ return this.firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return this.lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+}
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java
new file mode 100644
index 00000000000..cc701bff02a
--- /dev/null
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Sample package-info for testing purposes.
+ */
+@TypeDef(name = "test", typeClass = Object.class)
+package org.springframework.orm.jpa.domain2;
+
+import org.hibernate.annotations.TypeDef;
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java
new file mode 100644
index 00000000000..9af72e5e8e0
--- /dev/null
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.util.function.BiConsumer;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aot.test.generator.compile.Compiled;
+import org.springframework.aot.test.generator.compile.TestCompiler;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.aot.ApplicationContextAotGenerator;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.testfixture.aot.generate.TestGenerationContext;
+import org.springframework.orm.jpa.JpaVendorAdapter;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.domain.DriversLicense;
+import org.springframework.orm.jpa.domain.Person;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link PersistenceManagedTypesBeanRegistrationAotProcessor}.
+ *
+ * @author Stephane Nicoll
+ */
+class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
+
+ @Test
+ void processEntityManagerWithPackagesToScan() {
+ GenericApplicationContext context = new AnnotationConfigApplicationContext();
+ context.registerBean(EntityManagerWithPackagesToScanConfiguration.class);
+ compile(context, (initializer, compiled) -> {
+ GenericApplicationContext freshApplicationContext = toFreshApplicationContext(
+ initializer);
+ PersistenceManagedTypes persistenceManagedTypes = freshApplicationContext.getBean(
+ "persistenceManagedTypes", PersistenceManagedTypes.class);
+ assertThat(persistenceManagedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
+ DriversLicense.class.getName(), Person.class.getName());
+ assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
+ assertThat(freshApplicationContext.getBean(
+ EntityManagerWithPackagesToScanConfiguration.class).scanningInvoked).isFalse();
+ });
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private void compile(GenericApplicationContext applicationContext,
+ BiConsumer, Compiled> result) {
+ ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
+ TestGenerationContext generationContext = new TestGenerationContext();
+ generator.processAheadOfTime(applicationContext, generationContext);
+ generationContext.writeGeneratedContent();
+ TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled ->
+ result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
+ }
+
+ private GenericApplicationContext toFreshApplicationContext(
+ ApplicationContextInitializer initializer) {
+ GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
+ initializer.initialize(freshApplicationContext);
+ freshApplicationContext.refresh();
+ return freshApplicationContext;
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ public static class EntityManagerWithPackagesToScanConfiguration {
+
+ private boolean scanningInvoked;
+
+ @Bean
+ public DataSource mockDataSource() {
+ return mock(DataSource.class);
+ }
+
+ @Bean
+ public HibernateJpaVendorAdapter jpaVendorAdapter() {
+ HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
+ jpaVendorAdapter.setDatabase(Database.HSQL);
+ return jpaVendorAdapter;
+ }
+
+ @Bean
+ public PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
+ this.scanningInvoked = true;
+ return new PersistenceManagedTypesScanner(resourceLoader)
+ .scan("org.springframework.orm.jpa.domain");
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
+ JpaVendorAdapter jpaVendorAdapter, PersistenceManagedTypes persistenceManagedTypes) {
+ LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
+ entityManagerFactoryBean.setDataSource(dataSource);
+ entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
+ entityManagerFactoryBean.setManagedTypes(persistenceManagedTypes);
+ return entityManagerFactoryBean;
+ }
+
+ }
+
+}
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScannerTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScannerTests.java
new file mode 100644
index 00000000000..a5ae56a3dd4
--- /dev/null
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScannerTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.orm.jpa.domain.DriversLicense;
+import org.springframework.orm.jpa.domain.Person;
+import org.springframework.orm.jpa.domain2.entity.User;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link PersistenceManagedTypesScanner}.
+ *
+ * @author Stephane Nicoll
+ */
+class PersistenceManagedTypesScannerTests {
+
+ private final PersistenceManagedTypesScanner scanner = new PersistenceManagedTypesScanner(new DefaultResourceLoader());
+
+ @Test
+ void scanPackageWithOnlyEntities() {
+ PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain");
+ assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
+ Person.class.getName(), DriversLicense.class.getName());
+ assertThat(managedTypes.getManagedPackages()).isEmpty();
+ }
+
+ @Test
+ void scanPackageWithEntitiesAndManagedPackages() {
+ PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain2");
+ assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(User.class.getName());
+ assertThat(managedTypes.getManagedPackages()).containsExactlyInAnyOrder(
+ "org.springframework.orm.jpa.domain2");
+ }
+
+ @Test
+ void scanPackageUsesIndexIfPresent() {
+ DefaultResourceLoader resourceLoader = new DefaultResourceLoader(
+ CandidateComponentsTestClassLoader.index(getClass().getClassLoader(),
+ new ClassPathResource("test-spring.components", getClass())));
+ PersistenceManagedTypes managedTypes = new PersistenceManagedTypesScanner(resourceLoader).scan("com.example");
+ assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
+ "com.example.domain.Person", "com.example.domain.Address");
+ assertThat(managedTypes.getManagedPackages()).containsExactlyInAnyOrder(
+ "com.example.domain");
+
+ }
+
+}
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesTests.java
new file mode 100644
index 00000000000..6cae5eaec56
--- /dev/null
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.orm.jpa.persistenceunit;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PersistenceManagedTypes}.
+ *
+ * @author Stephane Nicoll
+ */
+class PersistenceManagedTypesTests {
+
+ @Test
+ void createWithManagedClassNames() {
+ PersistenceManagedTypes managedTypes = PersistenceManagedTypes.of(
+ "com.example.One", "com.example.Two");
+ assertThat(managedTypes.getManagedClassNames()).containsExactly(
+ "com.example.One", "com.example.Two");
+ assertThat(managedTypes.getManagedPackages()).isEmpty();
+ assertThat(managedTypes.getPersistenceUnitRootUrl()).isNull();
+ }
+
+ @Test
+ void createWithNullManagedClasses() {
+ assertThatIllegalArgumentException().isThrownBy(() -> PersistenceManagedTypes.of(null));
+ }
+
+ @Test
+ void createWithManagedClassNamesAndPackages() {
+ PersistenceManagedTypes managedTypes = PersistenceManagedTypes.of(
+ List.of("com.example.One", "com.example.Two"), List.of("com.example"));
+ assertThat(managedTypes.getManagedClassNames()).containsExactly(
+ "com.example.One", "com.example.Two");
+ assertThat(managedTypes.getManagedPackages()).containsExactly("com.example");
+ assertThat(managedTypes.getPersistenceUnitRootUrl()).isNull();
+ }
+
+}
diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/persistenceunit/test-spring.components b/spring-orm/src/test/resources/org/springframework/orm/jpa/persistenceunit/test-spring.components
new file mode 100644
index 00000000000..66f25a48088
--- /dev/null
+++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/persistenceunit/test-spring.components
@@ -0,0 +1,4 @@
+com.example.domain.Person=jakarta.persistence.Entity
+com.example.domain.Address=jakarta.persistence.Entity
+
+com.example.domain=package-info