Support for candidate components index
This commit adds a "spring-context-indexer" module that can be added to any project in order to generate an index of candidate components defined in the project. `CandidateComponentsIndexer` is a standard annotation processor that looks for source files with target annotations (typically `@Component`) and references them in a `META-INF/spring.components` generated file. Each entry in the index is the fully qualified name of a candidate component and the comma-separated list of stereotypes that apply to that candidate. A typical example of a stereotype is `@Component`. If a project has a `com.example.FooService` annotated with `@Component` the following `META-INF/spring.components` file is generated at compile time: ``` com.example.FooService=org.springframework.stereotype.Component ``` A new `@Indexed` annotation can be added on any annotation to instructs the scanner to include a source file that contains that annotation. For instance, `@Component` is meta-annotated with `@Indexed` now and adding `@Indexed` to more annotation types will transparently improve the index with additional information. This also works for interaces or parent classes: adding `@Indexed` on a `Repository` base interface means that the indexed can be queried for its implementation by using the fully qualified name of the `Repository` interface. The indexer also adds any class or interface that has a type-level annotation from the `javax` package. This includes obviously JPA (`@Entity` and related) but also CDI (`@Named`, `@ManagedBean`) and servlet annotations (i.e. `@WebFilter`). These are meant to handle cases where a component needs to identify candidates and use classpath scanning currently. If a `package-info.java` file exists, the package is registered using a "package-info" stereotype. Such files can later be reused by the `ApplicationContext` to avoid using component scan. A global `CandidateComponentsIndex` can be easily loaded from the current classpath using `CandidateComponentsIndexLoader`. The core framework uses such infrastructure in two areas: to retrieve the candidate `@Component`s and to build a default `PersistenceUnitInfo`. Rather than scanning the classpath and using ASM to identify candidates, the index is used if present. As long as the include filters refer to an annotation that is directly annotated with `@Indexed` or an assignable type that is directly annotated with `@Indexed`, the index can be used since a dedicated entry wil be present for that type. If any other unsupported include filter is specified, we fallback on classpath scanning. In case the index is incomplete or cannot be used, The `spring.index.ignore` system property can be set to `true` or, alternatively, in a "spring.properties" at the root of the classpath. Issue: SPR-11890
This commit is contained in:
@@ -40,6 +40,8 @@ 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;
|
||||
@@ -76,6 +78,7 @@ import org.springframework.util.ResourceUtils;
|
||||
* <p><b>NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0.</b>
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0
|
||||
* @see #setPersistenceXmlLocations
|
||||
* @see #setDataSourceLookup
|
||||
@@ -108,7 +111,7 @@ public class DefaultPersistenceUnitManager
|
||||
public final static String ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME = "default";
|
||||
|
||||
|
||||
private static final Set<TypeFilter> entityTypeFilters;
|
||||
private static final Set<AnnotationTypeFilter> entityTypeFilters;
|
||||
|
||||
static {
|
||||
entityTypeFilters = new LinkedHashSet<>(4);
|
||||
@@ -147,6 +150,8 @@ public class DefaultPersistenceUnitManager
|
||||
|
||||
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
private CandidateComponentsIndex componentsIndex;
|
||||
|
||||
private final Set<String> persistenceUnitInfoNames = new HashSet<>();
|
||||
|
||||
private final Map<String, PersistenceUnitInfo> persistenceUnitInfos = new HashMap<>();
|
||||
@@ -406,6 +411,7 @@ public class DefaultPersistenceUnitManager
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
|
||||
this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader());
|
||||
}
|
||||
|
||||
|
||||
@@ -510,33 +516,11 @@ public class DefaultPersistenceUnitManager
|
||||
|
||||
if (this.packagesToScan != null) {
|
||||
for (String pkg : this.packagesToScan) {
|
||||
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) {
|
||||
if (resource.isReadable()) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.componentsIndex != null) {
|
||||
addPackageFromIndex(scannedUnit, pkg);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new PersistenceException("Failed to scan classpath for unlisted entity classes", ex);
|
||||
else {
|
||||
scanPackage(scannedUnit, pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,6 +549,48 @@ public class DefaultPersistenceUnitManager
|
||||
return scannedUnit;
|
||||
}
|
||||
|
||||
private void addPackageFromIndex(SpringPersistenceUnitInfo scannedUnit, String pkg) {
|
||||
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);
|
||||
}
|
||||
|
||||
private void scanPackage(SpringPersistenceUnitInfo scannedUnit, String pkg) {
|
||||
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) {
|
||||
if (resource.isReadable()) {
|
||||
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 (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.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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
|
||||
*
|
||||
* http://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.Test;
|
||||
|
||||
import org.springframework.context.index.CandidateComponentsTestClassLoader;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.orm.jpa.domain.Person;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultPersistenceUnitManager}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class DefaultPersistenceUnitManagerTests {
|
||||
|
||||
private final DefaultPersistenceUnitManager manager = new DefaultPersistenceUnitManager();
|
||||
|
||||
@Test
|
||||
public void defaultDomainWithScan() {
|
||||
this.manager.setPackagesToScan("org.springframework.orm.jpa.domain");
|
||||
this.manager.setResourceLoader(new DefaultResourceLoader(
|
||||
CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader())));
|
||||
testDefaultDomain();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultDomainWithIndex() {
|
||||
this.manager.setPackagesToScan("org.springframework.orm.jpa.domain");
|
||||
this.manager.setResourceLoader(new DefaultResourceLoader(
|
||||
CandidateComponentsTestClassLoader.index(getClass().getClassLoader(),
|
||||
new ClassPathResource("spring.components", Person.class))));
|
||||
testDefaultDomain();
|
||||
}
|
||||
|
||||
private void testDefaultDomain() {
|
||||
SpringPersistenceUnitInfo puInfo = buildDefaultPersistenceUnitInfo();
|
||||
assertThat(puInfo.getManagedClassNames(), containsInAnyOrder(
|
||||
"org.springframework.orm.jpa.domain.Person",
|
||||
"org.springframework.orm.jpa.domain.DriversLicense"));
|
||||
}
|
||||
|
||||
private SpringPersistenceUnitInfo buildDefaultPersistenceUnitInfo() {
|
||||
this.manager.preparePersistenceUnitInfos();
|
||||
return (SpringPersistenceUnitInfo) this.manager.obtainDefaultPersistenceUnitInfo();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.orm.jpa.domain.Person=javax.persistence.Entity
|
||||
org.springframework.orm.jpa.domain.DriversLicense=javax.persistence.Entity
|
||||
Reference in New Issue
Block a user