Introduce PersistenceManagedTypes and AOT processing of managed entities

This commit adds PersistenceManagedTypes, an abstraction that represents
what typically happens at runtime when a persistence unit is built based
on classpath scanning.

PersistenceManagedTypesScanner extracts the logic that used to be in
DefaultPersistenceUnitManager and the latter can be configured with
a PersistenceManagedTypes instance.

This commits adds a bean registration AOT contribution that retrieves
the result of the configured instance and replaces it with the list
of managed entities. This has the result of making sure scanning, if
any, does not happen at runtime. This also could help if additional
hints for managed entities are required.

Closes gh-28287
This commit is contained in:
Stephane Nicoll 2022-08-05 09:47:51 +02:00
parent eac616a83e
commit 4fb4e54c73
13 changed files with 781 additions and 96 deletions

View File

@ -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
* <p>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}).
* <p>Consider setting a {@link PersistenceManagedTypes} instead that allows the
* scanning logic to be optimized by AOT processing.
* <p><b>Note: There may be limitations in comparison to regular JPA scanning.</b>
* 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

View File

@ -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;
* <p>Supports standard JPA scanning for {@code persistence.xml} files,
* with configurable file locations, JDBC DataSource lookup and load-time weaving.
*
* <p>Builds a persistence unit based on the state of a {@link PersistenceManagedTypes},
* typically built using a {@link PersistenceManagedTypesScanner}.</p>
*
* <p>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<AnnotationTypeFilter> 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<String> persistenceUnitInfoNames = new HashSet<>();
private final Map<String, PersistenceUnitInfo> 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
* <p>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}).
* <p>Consider setting a {@link PersistenceManagedTypes} instead that allows the
* scanning logic to be optimized by AOT processing.
* <p>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<SpringPersistenceUnitInfo> readPersistenceUnitInfos() {
List<SpringPersistenceUnitInfo> 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<String> candidates = new HashSet<>();
for (AnnotationTypeFilter filter : entityTypeFilters) {
candidates.addAll(this.componentsIndex.getCandidateTypes(pkg, filter.getAnnotationType().getName()));
}
candidates.forEach(scannedUnit::addManagedClassName);
Set<String> 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;
}
/**

View File

@ -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<String> getManagedClassNames();
/**
* Return a list of managed Java packages, to be introspected by the
* persistence provider.
* @return the managed packages
*/
List<String> 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<String> managedClassNames, List<String> managedPackages) {
return new SimplePersistenceManagedTypes(managedClassNames, managedPackages);
}
}

View File

@ -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.
*
* <p>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<String> values) {
return CodeBlock.join(values.stream().map(value -> CodeBlock.of("$S", value)).toList(), ", ");
}
}
}

View File

@ -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<AnnotationTypeFilter> 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<String> 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<String> managedClassNames = new ArrayList<>();
private final List<String> managedPackages = new ArrayList<>();
@Nullable
private URL persistenceUnitRootUrl;
PersistenceManagedTypes toJpaManagedTypes() {
return new SimplePersistenceManagedTypes(this.managedClassNames,
this.managedPackages, this.persistenceUnitRootUrl);
}
}
}

View File

@ -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<String> managedClassNames;
private final List<String> managedPackages;
@Nullable
private final URL persistenceUnitRootUrl;
SimplePersistenceManagedTypes(List<String> managedClassNames, List<String> managedPackages,
@Nullable URL persistenceUnitRootUrl) {
this.managedClassNames = managedClassNames;
this.managedPackages = managedPackages;
this.persistenceUnitRootUrl = persistenceUnitRootUrl;
}
SimplePersistenceManagedTypes(List<String> managedClassNames, List<String> managedPackages) {
this(managedClassNames, managedPackages, null);
}
@Override
public List<String> getManagedClassNames() {
return this.managedClassNames;
}
@Override
public List<String> getManagedPackages() {
return this.managedPackages;
}
@Override
@Nullable
public URL getPersistenceUnitRootUrl() {
return this.persistenceUnitRootUrl;
}
}

View File

@ -0,0 +1,2 @@
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypesBeanRegistrationAotProcessor

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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<ApplicationContextInitializer<GenericApplicationContext>, 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<GenericApplicationContext> 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;
}
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,4 @@
com.example.domain.Person=jakarta.persistence.Entity
com.example.domain.Address=jakarta.persistence.Entity
com.example.domain=package-info