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