From dcade06fa01e48483f70336dc8a4ddcf0c2ae5ab Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 12 Aug 2016 10:49:37 +0200 Subject: [PATCH] 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 --- build.gradle | 11 + settings.gradle | 1 + .../index/CandidateComponentsIndexer.java | 111 ++++++++ .../index/IndexedStereotypesProvider.java | 119 +++++++++ .../context/index/MetadataCollector.java | 107 ++++++++ .../context/index/MetadataStore.java | 83 ++++++ .../index/PackageInfoStereotypesProvider.java | 43 +++ .../index/StandardStereotypesProvider.java | 55 ++++ .../context/index/StereotypesProvider.java | 38 +++ .../context/index/TypeUtils.java | 113 ++++++++ .../metadata/CandidateComponentsMetadata.java | 50 ++++ .../context/index/metadata/ItemMetadata.java | 50 ++++ .../index/metadata/PropertiesMarshaller.java | 56 ++++ .../context/index/metadata/package-info.java | 20 ++ .../context/index/package-info.java | 21 ++ .../javax.annotation.processing.Processor | 1 + .../CandidateComponentsIndexerTests.java | 246 ++++++++++++++++++ .../metadata/PropertiesMarshallerTests.java | 58 +++++ .../index/sample/AbstractController.java | 29 +++ .../context/index/sample/MetaController.java | 37 +++ .../index/sample/MetaControllerIndexed.java | 39 +++ .../context/index/sample/SampleComponent.java | 28 ++ .../index/sample/SampleController.java | 28 ++ .../index/sample/SampleMetaController.java | 26 ++ .../sample/SampleMetaIndexedController.java | 27 ++ .../context/index/sample/SampleNone.java | 30 +++ .../index/sample/SampleRepository.java | 28 ++ .../context/index/sample/SampleService.java | 28 ++ .../index/sample/cdi/SampleManagedBean.java | 28 ++ .../context/index/sample/cdi/SampleNamed.java | 28 ++ .../index/sample/jpa/SampleConverter.java | 28 ++ .../index/sample/jpa/SampleEmbeddable.java | 28 ++ .../index/sample/jpa/SampleEntity.java | 28 ++ .../sample/jpa/SampleMappedSuperClass.java | 28 ++ .../index/sample/jpa/package-info.java | 22 ++ .../index/sample/type/AbstractRepo.java | 23 ++ .../context/index/sample/type/Repo.java | 29 +++ .../index/sample/type/SampleEntity.java | 23 ++ .../context/index/sample/type/SampleRepo.java | 25 ++ .../index/sample/type/SampleSmartRepo.java | 27 ++ .../sample/type/SampleSpecializedRepo.java | 25 ++ .../context/index/sample/type/SmartRepo.java | 28 ++ .../index/sample/type/SpecializedRepo.java | 24 ++ .../context/index/test/Metadata.java | 105 ++++++++ .../context/index/test/TestCompiler.java | 124 +++++++++ ...athScanningCandidateComponentProvider.java | 124 ++++++++- .../index/CandidateComponentsIndex.java | 87 +++++++ .../index/CandidateComponentsIndexLoader.java | 115 ++++++++ .../context/index/package-info.java | 20 ++ .../springframework/stereotype/Component.java | 1 + .../springframework/stereotype/Indexed.java | 87 +++++++ .../java/example/scannable/FooService.java | 4 +- ...anningCandidateComponentProviderTests.java | 191 +++++++++++++- .../CandidateComponentsIndexLoaderTests.java | 111 ++++++++ .../index/CandidateComponentsIndexTests.java | 95 +++++++ .../CandidateComponentsTestClassLoader.java | 96 +++++++ .../example/scannable/spring.components | 9 + .../context/index/empty-spring.components | 3 + .../context/index/spring.components | 3 + .../type/filter/AnnotationTypeFilter.java | 10 +- .../type/filter/AssignableTypeFilter.java | 9 +- .../AnnotatedElementUtilsTests.java | 9 +- .../core/type/AnnotationMetadataTests.java | 2 +- .../springframework/stereotype/Component.java | 3 +- .../springframework/stereotype/Indexed.java | 34 +++ .../DefaultPersistenceUnitManager.java | 80 ++++-- .../DefaultPersistenceUnitManagerTests.java | 67 +++++ .../orm/jpa/domain/spring.components | 2 + 68 files changed, 3221 insertions(+), 47 deletions(-) create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java create mode 100644 spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java create mode 100644 spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java create mode 100644 spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java create mode 100644 spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java create mode 100644 spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java create mode 100644 spring-context/src/main/java/org/springframework/context/index/package-info.java create mode 100644 spring-context/src/main/java/org/springframework/stereotype/Indexed.java create mode 100644 spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java create mode 100644 spring-context/src/test/resources/example/scannable/spring.components create mode 100644 spring-context/src/test/resources/org/springframework/context/index/empty-spring.components create mode 100644 spring-context/src/test/resources/org/springframework/context/index/spring.components create mode 100644 spring-core/src/test/java/org/springframework/stereotype/Indexed.java create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components diff --git a/build.gradle b/build.gradle index 50b2c3ee16..cf15972bf7 100644 --- a/build.gradle +++ b/build.gradle @@ -682,6 +682,17 @@ project("spring-context-support") { } } +project("spring-context-indexer") { + description = "Spring Context Indexer" + + dependencies { + testCompile(project(":spring-context")) + testCompile("javax.inject:javax.inject:1") + testCompile("javax.annotation:javax.annotation-api:${annotationApiVersion}") + testCompile("org.eclipse.persistence:javax.persistence:${jpaVersion}") + } +} + project("spring-web") { description = "Spring Web" apply plugin: "groovy" diff --git a/settings.gradle b/settings.gradle index 6c9be85d55..316022a704 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include "spring-beans" include "spring-beans-groovy" include "spring-context" include "spring-context-support" +include "spring-context-indexer" include "spring-core" include "spring-expression" include "spring-instrument" diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java new file mode 100644 index 0000000000..2c6862b310 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java @@ -0,0 +1,111 @@ +/* + * 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.context.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.springframework.context.index.metadata.CandidateComponentsMetadata; +import org.springframework.context.index.metadata.ItemMetadata; + +/** + * Annotation {@link Processor} that writes {@link CandidateComponentsMetadata} + * file for spring components. + * + * @author Stephane Nicoll + * @since 5.0 + */ +@SupportedAnnotationTypes({"*"}) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class CandidateComponentsIndexer extends AbstractProcessor { + + private MetadataStore metadataStore; + + private MetadataCollector metadataCollector; + + private TypeUtils typeUtils; + + private List stereotypesProviders; + + @Override + public synchronized void init(ProcessingEnvironment env) { + this.stereotypesProviders = getStereotypesProviders(env); + this.typeUtils = new TypeUtils(env); + this.metadataStore = new MetadataStore(env); + this.metadataCollector = new MetadataCollector(env, + this.metadataStore.readMetadata()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.metadataCollector.processing(roundEnv); + roundEnv.getRootElements().forEach(this::processElement); + + if (roundEnv.processingOver()) { + writeMetaData(); + } + return false; + } + + protected List getStereotypesProviders(ProcessingEnvironment env) { + List result = new ArrayList<>(); + TypeUtils typeUtils = new TypeUtils(env); + result.add(new IndexedStereotypesProvider(typeUtils)); + result.add(new StandardStereotypesProvider(typeUtils)); + result.add(new PackageInfoStereotypesProvider()); + return result; + } + + private void processElement(Element element) { + Set stereotypes = new LinkedHashSet<>(); + this.stereotypesProviders.forEach(p -> { + stereotypes.addAll(p.getStereotypes(element)); + + }); + if (!stereotypes.isEmpty()) { + this.metadataCollector.add(new ItemMetadata( + this.typeUtils.getType(element), stereotypes)); + } + } + + protected CandidateComponentsMetadata writeMetaData() { + CandidateComponentsMetadata metadata = this.metadataCollector.getMetadata(); + if (!metadata.getItems().isEmpty()) { + try { + this.metadataStore.writeMetadata(metadata); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to write metadata", ex); + } + return metadata; + } + return null; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java new file mode 100644 index 0000000000..6edb325a51 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java @@ -0,0 +1,119 @@ +/* + * 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.context.index; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * A {@link StereotypesProvider} implementation that extracts the stereotypes + * flagged by the {@value INDEXED_ANNOTATION} annotation. This implementation + * honors stereotypes defined this way on meta-annotations. + * + * @author Stephane Nicoll + */ +class IndexedStereotypesProvider implements StereotypesProvider { + + private static final String INDEXED_ANNOTATION = "org.springframework.stereotype.Indexed"; + + private final TypeUtils typeUtils; + + public IndexedStereotypesProvider(TypeUtils typeUtils) { + this.typeUtils = typeUtils; + } + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + Set seen = new HashSet<>(); + collectStereotypesOnAnnotations(seen, stereotypes, element); + seen = new HashSet<>(); + collectStereotypesOnTypes(seen, stereotypes, element); + return stereotypes; + } + + private void collectStereotypesOnAnnotations(Set seen, Set stereotypes, + Element element) { + for (AnnotationMirror annotation : this.typeUtils.getAllAnnotationMirrors(element)) { + Element next = collectStereotypes(seen, stereotypes, element, annotation); + if (next != null) { + collectStereotypesOnAnnotations(seen, stereotypes, next); + } + } + + } + + private void collectStereotypesOnTypes(Set seen, Set stereotypes, + Element type) { + if (!seen.contains(type)) { + seen.add(type); + if (isAnnotatedWithIndexed(type)) { + stereotypes.add(this.typeUtils.getType(type)); + } + Element superClass = this.typeUtils.getSuperClass(type); + if (superClass != null) { + collectStereotypesOnTypes(seen, stereotypes, superClass); + } + this.typeUtils.getDirectInterfaces(type).forEach( + i -> collectStereotypesOnTypes(seen, stereotypes, i)); + } + } + + private Element collectStereotypes(Set seen, Set stereotypes, + Element element, AnnotationMirror annotation) { + if (isIndexedAnnotation(annotation)) { + stereotypes.add(this.typeUtils.getType(element)); + } + return getCandidateAnnotationElement(seen, annotation); + } + + private Element getCandidateAnnotationElement(Set seen, AnnotationMirror annotation) { + Element element = annotation.getAnnotationType().asElement(); + if (seen.contains(element)) { + return null; + } + // We need to visit all indexed annotations. + if (!isIndexedAnnotation(annotation)) { + seen.add(element); + } + return (!element.toString().startsWith("java.lang") ? element : null); + } + + private boolean isAnnotatedWithIndexed(Element type) { + for (AnnotationMirror annotation : type.getAnnotationMirrors()) { + if (isIndexedAnnotation(annotation)) { + return true; + } + } + return false; + } + + private boolean isIndexedAnnotation(AnnotationMirror annotation) { + return INDEXED_ANNOTATION.equals(annotation.getAnnotationType().toString()); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java new file mode 100644 index 0000000000..22701d2456 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java @@ -0,0 +1,107 @@ +/* + * 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.context.index; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.springframework.context.index.metadata.ItemMetadata; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Used by {@link CandidateComponentsIndexer} to collect {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + */ +class MetadataCollector { + + private final List metadataItems = new ArrayList(); + + private final ProcessingEnvironment processingEnvironment; + + private final CandidateComponentsMetadata previousMetadata; + + private final TypeUtils typeUtils; + + private final Set processedSourceTypes = new HashSet(); + + /** + * Creates a new {@code MetadataProcessor} instance. + * @param processingEnvironment The processing environment of the build + * @param previousMetadata Any previous metadata or {@code null} + */ + public MetadataCollector(ProcessingEnvironment processingEnvironment, + CandidateComponentsMetadata previousMetadata) { + this.processingEnvironment = processingEnvironment; + this.previousMetadata = previousMetadata; + this.typeUtils = new TypeUtils(processingEnvironment); + } + + public void processing(RoundEnvironment roundEnv) { + for (Element element : roundEnv.getRootElements()) { + markAsProcessed(element); + } + } + + private void markAsProcessed(Element element) { + if (element instanceof TypeElement) { + this.processedSourceTypes.add(this.typeUtils.getType(element)); + } + } + + public void add(ItemMetadata metadata) { + this.metadataItems.add(metadata); + } + + public CandidateComponentsMetadata getMetadata() { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + for (ItemMetadata item : this.metadataItems) { + metadata.add(item); + } + if (this.previousMetadata != null) { + List items = this.previousMetadata.getItems(); + for (ItemMetadata item : items) { + if (shouldBeMerged(item)) { + metadata.add(item); + } + } + } + return metadata; + } + + private boolean shouldBeMerged(ItemMetadata itemMetadata) { + String sourceType = itemMetadata.getType(); + return (sourceType != null && !deletedInCurrentBuild(sourceType) + && !processedInCurrentBuild(sourceType)); + } + + private boolean deletedInCurrentBuild(String sourceType) { + return this.processingEnvironment.getElementUtils() + .getTypeElement(sourceType) == null; + } + + private boolean processedInCurrentBuild(String sourceType) { + return this.processedSourceTypes.contains(sourceType); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java new file mode 100644 index 0000000000..70ada70218 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java @@ -0,0 +1,83 @@ +/* + * 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.context.index; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import org.springframework.context.index.metadata.PropertiesMarshaller; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Store {@link CandidateComponentsMetadata} on the filesystem. + * + * @author Stephane Nicoll + */ +class MetadataStore { + + static final String METADATA_PATH = "META-INF/spring.components"; + + private final ProcessingEnvironment environment; + + public MetadataStore(ProcessingEnvironment environment) { + this.environment = environment; + } + + public CandidateComponentsMetadata readMetadata() { + try { + return readMetadata(getMetadataResource().openInputStream()); + } + catch (IOException ex) { + return null; + } + } + + public void writeMetadata(CandidateComponentsMetadata metadata) throws IOException { + if (!metadata.getItems().isEmpty()) { + try (OutputStream outputStream = createMetadataResource().openOutputStream()) { + new PropertiesMarshaller().write(metadata, outputStream); + } + } + } + + private CandidateComponentsMetadata readMetadata(InputStream in) throws IOException { + try { + return new PropertiesMarshaller().read(in); + } + catch (IOException ex) { + return null; + } + finally { + in.close(); + } + } + + private FileObject getMetadataResource() throws IOException { + return this.environment.getFiler() + .getResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + + private FileObject createMetadataResource() throws IOException { + return this.environment.getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java new file mode 100644 index 0000000000..6b0151199d --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java @@ -0,0 +1,43 @@ +/* + * 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.context.index; + +import java.util.HashSet; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} implementation that provides the + * {@value STEREOTYPE} stereotype for each package-info. + * + * @author Stephane Nicoll + */ +class PackageInfoStereotypesProvider implements StereotypesProvider { + + public static final String STEREOTYPE = "package-info"; + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new HashSet<>(); + if (element.getKind() == ElementKind.PACKAGE) { + stereotypes.add(STEREOTYPE); + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java new file mode 100644 index 0000000000..c119934da8 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java @@ -0,0 +1,55 @@ +/* + * 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.context.index; + +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} that extract a stereotype for each + * {@code javax.*} annotation placed on a class or interface. + * + * @author Stephane Nicoll + */ +class StandardStereotypesProvider implements StereotypesProvider { + + private final TypeUtils typeUtils; + + StandardStereotypesProvider(TypeUtils typeUtils) { + this.typeUtils = typeUtils; + } + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + for (AnnotationMirror annotation : this.typeUtils.getAllAnnotationMirrors(element)) { + String type = this.typeUtils.getType(annotation); + if (type.startsWith("javax.")) { + stereotypes.add(type); + } + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java new file mode 100644 index 0000000000..64672f62ce --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java @@ -0,0 +1,38 @@ +/* + * 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.context.index; + +import java.util.Set; +import javax.lang.model.element.Element; + +/** + * Provide the list of stereotypes that match an {@link Element}. If an element + * has one more stereotypes, it is referenced in the index of candidate + * components and each stereotype can be queried individually. + * + * @author Stephane Nicoll + */ +interface StereotypesProvider { + + /** + * Return the stereotypes that are present on the given {@link Element}. + * @param element the element to handle + * @return the stereotypes or an empty set if none were found + */ + Set getStereotypes(Element element); + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java b/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java new file mode 100644 index 0000000000..ee9931294a --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java @@ -0,0 +1,113 @@ +/* + * 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.context.index; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** + * Type utilities. + * + * @author Stephane Nicoll + */ +class TypeUtils { + + private final ProcessingEnvironment env; + + private final Types types; + + TypeUtils(ProcessingEnvironment env) { + this.env = env; + this.types = env.getTypeUtils(); + } + + public String getType(Element element) { + return getType(element != null ? element.asType() : null); + } + + public String getType(AnnotationMirror annotation) { + return getType(annotation != null ? annotation.getAnnotationType() : null); + } + + public String getType(TypeMirror type) { + if (type == null) { + return null; + } + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement != null && enclosingElement instanceof TypeElement) { + return getQualifiedName(enclosingElement) + "$" + + declaredType.asElement().getSimpleName().toString(); + } else { + return getQualifiedName(declaredType.asElement()); + } + } + return type.toString(); + } + + private String getQualifiedName(Element element) { + if (element instanceof QualifiedNameable) { + return ((QualifiedNameable) element).getQualifiedName().toString(); + } + return element.toString(); + } + + /** + * Return the super class of the specified {@link Element} or null if this + * {@code element} represents {@link Object}. + */ + public Element getSuperClass(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + if (superTypes.isEmpty()) { + return null; // reached java.lang.Object + } + return this.types.asElement(superTypes.get(0)); + } + + /** + * Return the interfaces that are directly implemented by the + * specified {@link Element} or an empty list if this {@code element} does not + * implement any interface. + */ + public List getDirectInterfaces(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + List directInterfaces = new ArrayList<>(); + if (superTypes.size() > 1) { // index 0 is the super class + for (int i = 1; i < superTypes.size(); i++) { + Element e = this.types.asElement(superTypes.get(i)); + if (e != null) { + directInterfaces.add(e); + } + } + } + return directInterfaces; + } + + public List getAllAnnotationMirrors(Element e) { + return this.env.getElementUtils().getAllAnnotationMirrors(e); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java new file mode 100644 index 0000000000..fe69d19ea1 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java @@ -0,0 +1,50 @@ +/* + * 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.context.index.metadata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Meta-data for candidate components. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsMetadata { + + private final List items; + + public CandidateComponentsMetadata() { + this.items = new ArrayList<>(); + } + + public void add(ItemMetadata item) { + this.items.add(item); + } + + public List getItems() { + return Collections.unmodifiableList(this.items); + } + + @Override + public String toString() { + return "CandidateComponentsMetadata{" + "items=" + this.items + '}'; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java new file mode 100644 index 0000000000..6555e78f1b --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java @@ -0,0 +1,50 @@ +/* + * 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.context.index.metadata; + +import java.util.HashSet; +import java.util.Set; + +/** + * Represents one entry in the index. The type defines the identify of the target + * candidate (usually fully qualified name) and the stereotypes are "markers" that can + * be used to retrieve the candidates. A typical use case is the presence of a given + * annotation on the candidate. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class ItemMetadata { + + private final String type; + + private final Set stereotypes; + + public ItemMetadata(String type, Set stereotypes) { + this.type = type; + this.stereotypes = new HashSet<>(stereotypes); + } + + public String getType() { + return this.type; + } + + public Set getStereotypes() { + return this.stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java new file mode 100644 index 0000000000..46079597cb --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java @@ -0,0 +1,56 @@ +/* + * 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.context.index.metadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Marshaller to write {@link CandidateComponentsMetadata} as properties. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class PropertiesMarshaller { + + public void write(CandidateComponentsMetadata metadata, OutputStream out) + throws IOException { + + Properties props = new Properties(); + metadata.getItems().forEach(m -> props.put(m.getType(), String.join(",", m.getStereotypes()))); + props.store(out, ""); + } + + public CandidateComponentsMetadata read(InputStream in) throws IOException { + CandidateComponentsMetadata result = new CandidateComponentsMetadata(); + Properties props = new Properties(); + props.load(in); + for (Map.Entry entry : props.entrySet()) { + String type = (String) entry.getKey(); + Set candidates = new HashSet<>(Arrays.asList(((String) entry.getValue()).split(","))); + result.add(new ItemMetadata(type, candidates)); + } + return result; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java new file mode 100644 index 0000000000..b354f78e77 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support package for defining and storing the metadata that forms the index. + */ +package org.springframework.context.index.metadata; \ No newline at end of file diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java b/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java new file mode 100644 index 0000000000..4edf83036f --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Generates a 'META-INF/spring.candidates' at compilation time with all + * the component candidates detected in the module. + */ +package org.springframework.context.index; \ No newline at end of file diff --git a/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..99504ebd31 --- /dev/null +++ b/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.springframework.context.index.CandidateComponentsIndexer \ No newline at end of file diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java new file mode 100644 index 0000000000..abb582b3d9 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java @@ -0,0 +1,246 @@ +/* + * 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.context.index; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import javax.annotation.ManagedBean; +import javax.inject.Named; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import org.springframework.context.index.metadata.CandidateComponentsMetadata; +import org.springframework.context.index.metadata.PropertiesMarshaller; +import org.springframework.context.index.sample.AbstractController; +import org.springframework.context.index.sample.MetaControllerIndexed; +import org.springframework.context.index.sample.SampleComponent; +import org.springframework.context.index.sample.SampleController; +import org.springframework.context.index.sample.SampleMetaController; +import org.springframework.context.index.sample.SampleMetaIndexedController; +import org.springframework.context.index.sample.SampleNone; +import org.springframework.context.index.sample.SampleRepository; +import org.springframework.context.index.sample.SampleService; +import org.springframework.context.index.sample.cdi.SampleManagedBean; +import org.springframework.context.index.sample.cdi.SampleNamed; +import org.springframework.context.index.sample.jpa.SampleConverter; +import org.springframework.context.index.sample.jpa.SampleEmbeddable; +import org.springframework.context.index.sample.jpa.SampleEntity; +import org.springframework.context.index.sample.jpa.SampleMappedSuperClass; +import org.springframework.context.index.sample.type.SampleRepo; +import org.springframework.context.index.sample.type.SampleSmartRepo; +import org.springframework.context.index.sample.type.SampleSpecializedRepo; +import org.springframework.context.index.sample.type.Repo; +import org.springframework.context.index.sample.type.SmartRepo; +import org.springframework.context.index.sample.type.SpecializedRepo; +import org.springframework.context.index.test.TestCompiler; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.context.index.test.Metadata.*; + +/** + * Tests for {@link CandidateComponentsIndexer}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexerTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private TestCompiler compiler; + + @Before + public void createCompiler() throws IOException { + this.compiler = new TestCompiler(this.temporaryFolder); + } + + @Test + public void noCandidate() throws IOException { + CandidateComponentsMetadata metadata = compile(SampleNone.class); + assertThat(metadata.getItems(), hasSize(0)); + } + + @Test + public void noAnnotation() throws IOException { + CandidateComponentsMetadata metadata = compile(CandidateComponentsIndexerTests.class); + assertThat(metadata.getItems(), hasSize(0)); + } + + @Test + public void stereotypeComponent() throws IOException { + testComponent(SampleComponent.class); + } + + @Test + public void stereotypeService() throws IOException { + testComponent(SampleService.class); + } + + @Test + public void stereotypeController() throws IOException { + testComponent(SampleController.class); + } + + @Test + public void stereotypeControllerMetaAnnotation() throws IOException { + testComponent(SampleMetaController.class); + } + + @Test + public void stereotypeRepository() throws IOException { + testSingleComponent(SampleRepository.class, Component.class); + } + + @Test + public void stereotypeControllerMetaIndex() throws IOException { + testSingleComponent(SampleMetaIndexedController.class, + Component.class, MetaControllerIndexed.class); + } + + @Test + public void stereotypeOnAbstractClass() throws IOException { + testComponent(AbstractController.class); + } + + @Test + public void cdiManagedBean() throws IOException { + testSingleComponent(SampleManagedBean.class, ManagedBean.class); + } + + @Test + public void cdiNamed() throws IOException { + testSingleComponent(SampleNamed.class, Named.class); + } + + @Test + public void persistenceEntity() throws IOException { + testSingleComponent(SampleEntity.class, Entity.class); + } + + @Test + public void persistenceMappedSuperClass() throws IOException { + testSingleComponent(SampleMappedSuperClass.class, MappedSuperclass.class); + } + + @Test + public void persistenceEmbeddable() throws IOException { + testSingleComponent(SampleEmbeddable.class, Embeddable.class); + } + + @Test + public void persistenceConverter() throws IOException { + testSingleComponent(SampleConverter.class, Converter.class); + } + + @Test + public void packageInfo() throws IOException { + CandidateComponentsMetadata metadata = compile( + "org/springframework/context/index/sample/jpa/package-info"); + assertThat(metadata, hasComponent( + "org.springframework.context.index.sample.jpa", "package-info")); + } + + @Test + public void typeStereotypeFromMetaInterface() throws IOException { + testSingleComponent(SampleSpecializedRepo.class, Repo.class); + } + + @Test + public void typeStereotypeFromInterfaceFromSuperClass() throws IOException { + testSingleComponent(SampleRepo.class, Repo.class); + } + + @Test + public void typeStereotypeFromSeveralInterfaces() throws IOException { + testSingleComponent(SampleSmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + public void typeStereotypeOnInterface() throws IOException { + testSingleComponent(SpecializedRepo.class, Repo.class); + } + + @Test + public void typeStereotypeOnInterfaceFromSeveralInterfaces() throws IOException { + testSingleComponent(SmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + public void typeStereotypeOnIndexedInterface() throws IOException { + testSingleComponent(Repo.class, Repo.class); + } + + + private void testComponent(Class... classes) throws IOException { + CandidateComponentsMetadata metadata = compile(classes); + for (Class c : classes) { + assertThat(metadata, hasComponent(c, Component.class)); + } + assertThat(metadata.getItems(), hasSize(classes.length)); + } + + private void testSingleComponent(Class target, Class... stereotypes) throws IOException { + CandidateComponentsMetadata metadata = compile(target); + assertThat(metadata, hasComponent(target, stereotypes)); + assertThat(metadata.getItems(), hasSize(1)); + } + + private CandidateComponentsMetadata compile(Class... types) throws IOException { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata compile(String... types) throws IOException { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata readGeneratedMetadata(File outputLocation) { + try { + File metadataFile = new File(outputLocation, + MetadataStore.METADATA_PATH); + if (metadataFile.isFile()) { + return new PropertiesMarshaller() + .read(new FileInputStream(metadataFile)); + } + else { + return new CandidateComponentsMetadata(); + } + } + catch (IOException e) { + throw new RuntimeException("Failed to read metadata from disk", e); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java new file mode 100644 index 0000000000..1914fab276 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java @@ -0,0 +1,58 @@ +/* + * 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.context.index.metadata; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.context.index.test.Metadata.*; + +/** + * Tests for {@link PropertiesMarshaller}. + * + * @author Stephane Nicoll + */ +public class PropertiesMarshallerTests { + + @Test + public void readWrite() throws IOException { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + metadata.add(createItem("com.foo", "first", "second")); + metadata.add(createItem("com.bar", "first")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PropertiesMarshaller marshaller = new PropertiesMarshaller(); + marshaller.write(metadata, outputStream); + CandidateComponentsMetadata readMetadata = marshaller.read( + new ByteArrayInputStream(outputStream.toByteArray())); + assertThat(readMetadata, hasComponent("com.foo", "first", "second")); + assertThat(readMetadata, hasComponent("com.bar", "first")); + assertThat(readMetadata.getItems(), hasSize(2)); + } + + private static ItemMetadata createItem(String type, String... stereotypes) { + return new ItemMetadata(type, new HashSet<>(Arrays.asList(stereotypes))); + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java new file mode 100644 index 0000000000..156ecf1408 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java @@ -0,0 +1,29 @@ +/* + * 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.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Abstract {@link Component} that shouldn't be registered. + * + * @author Stephane Nicoll + */ +@Component +public abstract class AbstractController { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java new file mode 100644 index 0000000000..813228f7ca --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java @@ -0,0 +1,37 @@ +/* + * 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.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; + +/** + * Sample meta-annotation. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +public @interface MetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java new file mode 100644 index 0000000000..9e66cbcc5e --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java @@ -0,0 +1,39 @@ +/* + * 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.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; + +/** + * A test annotation that triggers a dedicated entry in the index. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +@Indexed +public @interface MetaControllerIndexed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java new file mode 100644 index 0000000000..621c7e0d65 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Test candidate for {@link Component}. + * + * @author Stephane Nicoll + */ +@Component +public class SampleComponent { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java new file mode 100644 index 0000000000..e7593d0e7d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample; + +import org.springframework.stereotype.Controller; + +/** + * Test candidate for {@link Controller}. + * + * @author Stephane Nicoll + */ +@Controller +public class SampleController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java new file mode 100644 index 0000000000..314cb49632 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java @@ -0,0 +1,26 @@ +/* + * 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.context.index.sample; + +/** + * Test candidate for a {@code Controller} defined using a meta-annotation. + * + * @author Stephane Nicoll + */ +@MetaController +public class SampleMetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java new file mode 100644 index 0000000000..c620da58ca --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java @@ -0,0 +1,27 @@ +/* + * 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.context.index.sample; + +/** + * Test candidate for a {@code Controller} that adds both the + * {@code Component} and {@code MetaControllerIndexed} stereotypes. + * + * @author Stephane Nicoll + */ +@MetaControllerIndexed +public class SampleMetaIndexedController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java new file mode 100644 index 0000000000..145e89f923 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java @@ -0,0 +1,30 @@ +/* + * 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.context.index.sample; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.type.Scope; + +/** + * Candidate with no matching annotation. + * + * @author Stephane Nicoll + */ +@Scope("None") +@Qualifier("None") +public class SampleNone { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java new file mode 100644 index 0000000000..b254ee7624 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample; + +import org.springframework.stereotype.Repository; + +/** + * Test candidate for {@link Repository}. + * + * @author Stephane Nicoll + */ +@Repository +public class SampleRepository { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java new file mode 100644 index 0000000000..2ed7e6025c --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample; + +import org.springframework.stereotype.Service; + +/** + * Test candidate for {@link Service}. + * + * @author Stephane Nicoll + */ +@Service +public class SampleService { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java new file mode 100644 index 0000000000..54d09f1d74 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.cdi; + +import javax.annotation.ManagedBean; + +/** + * Test candidate for a CDI {@link ManagedBean}. + * + * @author Stephane Nicoll + */ +@ManagedBean +public class SampleManagedBean { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java new file mode 100644 index 0000000000..27364da26d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.cdi; + +import javax.inject.Named; + +/** + * Test candidate for a CDI {@link Named} bean. + * + * @author Stephane Nicoll + */ +@Named +public class SampleNamed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java new file mode 100644 index 0000000000..25e01ca306 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.jpa; + +import javax.persistence.Converter; + +/** + * Test candidate for {@link Converter}. + * + * @author Stephane Nicoll + */ +@Converter +public class SampleConverter { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java new file mode 100644 index 0000000000..53204e472b --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.jpa; + +import javax.persistence.Embeddable; + +/** + * Test candidate for {@link Embeddable}. + * + * @author Stephane Nicoll + */ +@Embeddable +public class SampleEmbeddable { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java new file mode 100644 index 0000000000..b03bdf7619 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.jpa; + +import javax.persistence.Entity; + +/** + * Test candidate for {@link Entity}. + * + * @author Stephane Nicoll + */ +@Entity +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java new file mode 100644 index 0000000000..2e92274183 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.jpa; + +import javax.persistence.MappedSuperclass; + +/** + * Test candidate for {@link MappedSuperclass}. + * + * @author Stephane Nicoll + */ +@MappedSuperclass +public abstract class SampleMappedSuperClass { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java new file mode 100644 index 0000000000..0cbf1db387 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Test candidate for {@code package-info}. + * + * @author Stephane Nicoll + */ +package org.springframework.context.index.sample.jpa; \ No newline at end of file diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java new file mode 100644 index 0000000000..da194359e6 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java @@ -0,0 +1,23 @@ +/* + * 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.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractRepo implements Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java new file mode 100644 index 0000000000..341b28123d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java @@ -0,0 +1,29 @@ +/* + * 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.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A sample interface flagged with {@link Indexed} to indicate that a stereotype + * for all implementations should be added to the index. + * + * @author Stephane Nicoll + */ +@Indexed +public interface Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java new file mode 100644 index 0000000000..83830aaeb3 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java @@ -0,0 +1,23 @@ +/* + * 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.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java new file mode 100644 index 0000000000..4d52f68de1 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java @@ -0,0 +1,25 @@ +/* + * 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.context.index.sample.type; + +/** + * A sample that gets its stereotype via an abstract class. + * + * @author Stephane Nicoll + */ +public class SampleRepo extends AbstractRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java new file mode 100644 index 0000000000..755d2a15fe --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java @@ -0,0 +1,27 @@ +/* + * 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.context.index.sample.type; + +/** + * A sample that implements both interface used to demonstrate that no + * duplicate stereotypes are generated. + * + * @author Stephane Nicoll + */ +public class SampleSmartRepo + implements SmartRepo, Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java new file mode 100644 index 0000000000..fa932a36b5 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java @@ -0,0 +1,25 @@ +/* + * 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.context.index.sample.type; + +/** + * A sample that does not directly implement the {@link Repo} interface. + * + * @author Stephane Nicoll + */ +public class SampleSpecializedRepo implements SpecializedRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java new file mode 100644 index 0000000000..b7dc3b0a1b --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java @@ -0,0 +1,28 @@ +/* + * 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.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A {@link Repo} that requires an extra stereotype. + * + * @author Stephane Nicoll + */ +@Indexed +public interface SmartRepo extends Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java new file mode 100644 index 0000000000..4c2e31e825 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java @@ -0,0 +1,24 @@ +/* + * 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.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public interface SpecializedRepo extends Repo { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java b/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java new file mode 100644 index 0000000000..e5f52000ad --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java @@ -0,0 +1,105 @@ +/* + * 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.context.index.test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +import org.springframework.context.index.metadata.ItemMetadata; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Hamcrest {@link org.hamcrest.Matcher Matcher} to help test {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + */ +public class Metadata { + + public static ItemMetadataMatcher hasComponent(Class type, Class... stereotypes) { + return new ItemMetadataMatcher(type.getName(), stereotypes); + } + + public static ItemMetadataMatcher hasComponent(String type, String... stereotypes) { + return new ItemMetadataMatcher(type, stereotypes); + } + + private static class ItemMetadataMatcher extends BaseMatcher { + + private final String type; + + private final List stereotypes; + + private ItemMetadataMatcher(String type, List stereotypes) { + this.type = type; + this.stereotypes = stereotypes; + } + + public ItemMetadataMatcher(String type, String... stereotypes) { + this(type, Arrays.asList(stereotypes)); + } + + public ItemMetadataMatcher(String type, Class... stereotypes) { + this(type, Arrays.stream(stereotypes) + .map(Class::getName).collect(Collectors.toList())); + } + + @Override + public boolean matches(Object value) { + if (!(value instanceof CandidateComponentsMetadata)) { + return false; + } + ItemMetadata itemMetadata = getFirstItemWithType((CandidateComponentsMetadata) value, this.type); + if (itemMetadata == null) { + return false; + } + if (this.type != null && !this.type.equals(itemMetadata.getType())) { + return false; + } + if (this.stereotypes != null) { + for (String stereotype : this.stereotypes) { + if (!itemMetadata.getStereotypes().contains(stereotype)) { + return false; + } + } + if (this.stereotypes.size() != itemMetadata.getStereotypes().size()) { + return false; + } + } + return true; + } + + private ItemMetadata getFirstItemWithType(CandidateComponentsMetadata metadata, String type) { + for (ItemMetadata item : metadata.getItems()) { + if (item.getType().equals(type)) { + return item; + } + } + return null; + } + + @Override + public void describeTo(Description description) { + description.appendText("Candidates with type ").appendValue(this.type); + description.appendText(" and stereotypes ").appendValue(this.stereotypes); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java new file mode 100644 index 0000000000..ab88bd679d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java @@ -0,0 +1,124 @@ +/* + * 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.context.index.test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import org.junit.rules.TemporaryFolder; + +/** + * Wrapper to make the {@link JavaCompiler} easier to use in tests. + * + * @author Stephane Nicoll + */ +public class TestCompiler { + + public static final File ORIGINAL_SOURCE_FOLDER = new File("src/test/java"); + + private final JavaCompiler compiler; + + private final StandardJavaFileManager fileManager; + + private final File outputLocation; + + public TestCompiler(TemporaryFolder temporaryFolder) throws IOException { + this(ToolProvider.getSystemJavaCompiler(), temporaryFolder); + } + + public TestCompiler(JavaCompiler compiler, TemporaryFolder temporaryFolder) + throws IOException { + this.compiler = compiler; + this.fileManager = compiler.getStandardFileManager(null, null, null); + this.outputLocation = temporaryFolder.newFolder(); + Iterable temp = Collections.singletonList(this.outputLocation); + this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, temp); + this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); + } + + public TestCompilationTask getTask(Class... types) { + List names = Arrays.stream(types).map(Class::getName) + .collect(Collectors.toList()); + return getTask(names.toArray(new String[names.size()])); + } + + public TestCompilationTask getTask(String... types) { + Iterable javaFileObjects = getJavaFileObjects(types); + return getTask(javaFileObjects); + } + + private TestCompilationTask getTask( + Iterable javaFileObjects) { + return new TestCompilationTask(this.compiler.getTask(null, this.fileManager, null, + null, null, javaFileObjects)); + } + + public File getOutputLocation() { + return this.outputLocation; + } + + private Iterable getJavaFileObjects(String... types) { + File[] files = new File[types.length]; + for (int i = 0; i < types.length; i++) { + files[i] = getFile(types[i]); + } + return this.fileManager.getJavaFileObjects(files); + } + + private File getFile(String type) { + return new File(getSourceFolder(), sourcePathFor(type)); + } + + private static String sourcePathFor(String type) { + return type.replace(".", "/") + ".java"; + } + + private File getSourceFolder() { + return ORIGINAL_SOURCE_FOLDER; + } + + /** + * A compilation task. + */ + public static class TestCompilationTask { + + private final JavaCompiler.CompilationTask task; + + public TestCompilationTask(JavaCompiler.CompilationTask task) { + this.task = task; + } + + public void call(Processor... processors) { + this.task.setProcessors(Arrays.asList(processors)); + if (!this.task.call()) { + throw new IllegalStateException("Compilation failed"); + } + } + + } + +} \ No newline at end of file diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index aed66e9b1a..967bc68efc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -18,6 +18,7 @@ package org.springframework.context.annotation; import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -28,9 +29,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; @@ -43,17 +48,24 @@ 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.AssignableTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * A component provider that scans the classpath from a base package. It then - * applies exclude and include filters to the resulting classes to find candidates. + * A component provider that provides candidate components from a base package. Can + * use {@link CandidateComponentsIndex the index} if it is available of scans the + * classpath otherwise. Candidate components are identified by applying exclude and + * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include + * filters on an annotation/super-class that are annotated with {@link Indexed} are + * supported: if any other include filter is specified, the index is ignored and + * classpath scanning is used instead. * *

This implementation is based on Spring's * {@link org.springframework.core.type.classreading.MetadataReader MetadataReader} @@ -63,10 +75,12 @@ import org.springframework.util.ClassUtils; * @author Juergen Hoeller * @author Ramnivas Laddad * @author Chris Beams + * @author Stephane Nicoll * @since 2.5 * @see org.springframework.core.type.classreading.MetadataReaderFactory * @see org.springframework.core.type.AnnotationMetadata * @see ScannedGenericBeanDefinition + * @see CandidateComponentsIndex */ public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { @@ -81,6 +95,8 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + private CandidateComponentsIndex componentsIndex; + private String resourcePattern = DEFAULT_RESOURCE_PATTERN; private final List includeFilters = new LinkedList<>(); @@ -132,6 +148,7 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC public void setResourceLoader(ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader()); } /** @@ -263,6 +280,58 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC * @return a corresponding Set of autodetected bean definitions */ public Set findCandidateComponents(String basePackage) { + if (isIndexSupported()) { + return addCandidateComponentsFromIndex(basePackage); + } + else { + return scanCandidateComponents(basePackage); + } + } + + protected Set addCandidateComponentsFromIndex(String basePackage) { + Set candidates = new LinkedHashSet<>(); + try { + Set types = new HashSet<>(); + for (TypeFilter filter : this.includeFilters) { + String stereotype = extractStereotype(filter); + if (stereotype == null) { + throw new IllegalArgumentException("Failed to extract stereotype from "+ filter); + } + types.addAll(this.componentsIndex.getCandidateTypes(basePackage, stereotype)); + } + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (String type : types) { + MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type); + if (isCandidateComponent(metadataReader)) { + AnnotatedGenericBeanDefinition sbd = new AnnotatedGenericBeanDefinition( + metadataReader.getAnnotationMetadata()); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Using candidate component class from index: " + type); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + type); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because matching an exclude filter: " + type); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; + } + + protected Set scanCandidateComponents(String basePackage) { Set candidates = new LinkedHashSet<>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + @@ -374,6 +443,57 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC return (beanDefinition.getMetadata().isConcrete() && beanDefinition.getMetadata().isIndependent()); } + /** + * Determine if the index can be used by this instance. + * @return {@code true} if the index is available and the configuration of this + * instance is supported by it, {@code false otherwise}. + */ + protected boolean isIndexSupported() { + if (this.componentsIndex == null) { + return false; + } + for (TypeFilter includeFilter : this.includeFilters) { + if (!isIndexSupportsIncludeFilter(includeFilter)) { + return false; + } + } + return true; + } + + /** + * Determine if the specified include {@link TypeFilter} is supported by the index. + * @param filter the filter to check + * @return whether the index supports this include filter + * @see #extractStereotype(TypeFilter) + */ + protected boolean isIndexSupportsIncludeFilter(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + Class annotation = ((AnnotationTypeFilter) filter).getAnnotationType(); + return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotation) + || annotation.getName().startsWith("javax.")); + } + if (filter instanceof AssignableTypeFilter) { + Class target = ((AssignableTypeFilter) filter).getTargetType(); + return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target); + } + return false; + } + + /** + * Extract the stereotype to use for the specified compatible filter. + * @param filter the filter to handle + * @return the stereotype in the index matching this filter + * @see #isIndexSupportsIncludeFilter(TypeFilter) + */ + protected String extractStereotype(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + return ((AnnotationTypeFilter) filter).getAnnotationType().getName(); + } + if (filter instanceof AssignableTypeFilter) { + return ((AssignableTypeFilter) filter).getTargetType().getName(); + } + return null; + } /** * Clear the underlying metadata cache, removing all cached class metadata. diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java new file mode 100644 index 0000000000..16dc5f6d71 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java @@ -0,0 +1,87 @@ +/* + * 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.context.index; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Provide access to the candidates that are defined in {@code META-INF/spring.components}. + *

+ * An arbitrary number of stereotypes can be registered (and queried) on the index: a + * typical example is the fully qualified name of an annotation that flags the class for + * a certain use case. The following call returns all the {@code @Component} + * candidate types for the {@code com.example} package (and its sub-packages): + *

+ * Set<String> candidates = index.getCandidateTypes(
+ *         "com.example", "org.springframework.stereotype.Component");
+ * 
+ * + * The {@code type} is usually the fully qualified name of a class, though this is + * not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of + * a target type but it can be any marker really. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsIndex { + + private final MultiValueMap index; + + CandidateComponentsIndex(List content) { + this.index = parseIndex(content); + } + + /** + * Return the candidate types that are associated with the specified stereotype. + * @param basePackage the package to check for candidates + * @param stereotype the stereotype to use + * @return the candidate types associated with the specified {@code stereotype} + * or an empty set if none has been found for the specified {@code basePackage} + */ + public Set getCandidateTypes(String basePackage, String stereotype) { + List candidates = this.index.get(stereotype); + if (candidates != null) { + return candidates.parallelStream() + .filter(t -> t.startsWith(basePackage)) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + private static MultiValueMap parseIndex(List content) { + MultiValueMap index = new LinkedMultiValueMap<>(); + for (Properties entry : content) { + for (Map.Entry entries : entry.entrySet()) { + String type = (String) entries.getKey(); + String[] stereotypes = ((String) entries.getValue()).split(","); + for (String stereotype : stereotypes) { + index.add(stereotype, type); + } + } + } + return index; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java new file mode 100644 index 0000000000..103df5f9c2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -0,0 +1,115 @@ +/* + * 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.context.index; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Candidate components index loading mechanism for internal use within the framework. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsIndexLoader { + + + private static final Log logger = LogFactory.getLog(CandidateComponentsIndexLoader.class); + + /** + * System property that instructs Spring to ignore the index, i.e. + * to always return {@code null} from {@link #loadIndex(ClassLoader)}. + *

The default is "false", allowing for regular use of the index. Switching this + * flag to {@code true} fulfills a corner case scenario when an index is partially + * available for some libraries (or use cases) but couldn't be built for the whole + * application. In this case, the application context fallbacks to a regular + * classpath arrangement (i.e. as no index was present at all). + */ + public static final String IGNORE_INDEX = "spring.index.ignore"; + + private static final boolean shouldIgnoreIndex = + SpringProperties.getFlag(IGNORE_INDEX); + + + /** + * The location to look for components. + *

Can be present in multiple JAR files. + */ + public static final String COMPONENTS_RESOURCE_LOCATION = "META-INF/spring.components"; + + private static final ConcurrentMap cache + = new ConcurrentReferenceHashMap<>(); + + + /** + * Load and instantiate the {@link CandidateComponentsIndex} from + * {@value #COMPONENTS_RESOURCE_LOCATION}, using the given class loader. If no + * index is available, return {@code null}. + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + * @return the index to use or {@code null} if no index was found + * @throws IllegalArgumentException if any module index cannot + * be loaded or if an error occurs while creating {@link CandidateComponentsIndex} + */ + public static CandidateComponentsIndex loadIndex(ClassLoader classLoader) { + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = CandidateComponentsIndexLoader.class.getClassLoader(); + } + return cache.computeIfAbsent(classLoaderToUse, CandidateComponentsIndexLoader::doLoadIndex); + } + + private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { + if (shouldIgnoreIndex) { + return null; + } + try { + Enumeration urls = classLoader.getResources(COMPONENTS_RESOURCE_LOCATION); + if (!urls.hasMoreElements()) { + return null; + } + List result = new ArrayList<>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + result.add(properties); + } + if (logger.isTraceEnabled()) { + logger.trace("Loaded " + result.size() + "] index(es)"); + } + int totalCount = result.stream().mapToInt(Properties::size).sum(); + return (totalCount > 0 ? new CandidateComponentsIndex(result) : null); + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load indexes from location [" + + COMPONENTS_RESOURCE_LOCATION + "]", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/package-info.java b/spring-context/src/main/java/org/springframework/context/index/package-info.java new file mode 100644 index 0000000000..d0437fdf9a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support package for reading and managing the components index. + */ +package org.springframework.context.index; \ No newline at end of file diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java index 8b3c75dffc..a6fd5b7ca5 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -42,6 +42,7 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Indexed public @interface Component { /** diff --git a/spring-context/src/main/java/org/springframework/stereotype/Indexed.java b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java new file mode 100644 index 0000000000..17d2512e79 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java @@ -0,0 +1,87 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate that the annotated element represents a stereotype for the index. + *

+ * The {@code CandidateComponentsIndex} is an alternative to classpath + * scanning that uses a metadata file generated at compilation time. The + * index allows retrieving the candidate components (i.e. fully qualified + * name) based on a stereotype. This annotation instructs the generator to + * index the element on which the annotated element is present or if it + * implements or extends from the annotated element. The stereotype is the + * fully qualified name of the annotated element. + *

+ * Consider the default {@link Component} annotation that is meta-annotated + * with this annotation. If a component is annotated with {@link Component}, + * an entry for that component will be added to the index using the + * {@code org.springframework.stereotype.Component} stereotype. + *

+ * This annotation is also honored on meta-annotations. Consider this + * custom annotation: + *

+ * package com.example;
+ *
+ * @Target(ElementType.TYPE)
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @Documented
+ * @Indexed
+ * @Service
+ * public @interface PrivilegedService { ... }
+ * 
+ * If this annotation is present on an type, it will be indexed with two + * stereotypes: {@code org.springframework.stereotype.Component} and + * {@code com.example.PrivilegedService}. While {@link Service} isn't directly + * annotated with {@code Indexed}, it is meta-annotated with {@link Component}. + *

+ * It is also possible to index all implementations of a certain interface or + * all the sub-classes of a given class by adding {@code @Indexed} on it. + * Consider this base interface: + *

+ * package com.example;
+ *
+ * @Indexed
+ * public interface AdminService { ... }
+ * 
+ * Now, consider an implementation of this {@code AdminService} somewhere: + *
+ * package com.example.foo;
+ *
+ * import com.example.AdminService;
+ *
+ * public class ConfigurationAdminService implements AdminService { ... }
+ * 
+ * Because this class implements an interface that is indexed, it will be + * automatically included with the {@code com.example.AdminService} stereotype. + * If there are more {@code @Indexed} interfaces and/or super classes in the + * hierarchy, the class will map to all their stereotypes. + * + * @author Stephane Nicoll + * @since 4.3.3 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} diff --git a/spring-context/src/test/java/example/scannable/FooService.java b/spring-context/src/test/java/example/scannable/FooService.java index df48735445..3f259040ca 100644 --- a/spring-context/src/test/java/example/scannable/FooService.java +++ b/spring-context/src/test/java/example/scannable/FooService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -19,11 +19,13 @@ package example.scannable; import java.util.concurrent.Future; import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Indexed; /** * @author Mark Fisher * @author Juergen Hoeller */ +@Indexed public interface FooService { String foo(int id); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index b8c33f7783..6537777beb 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -24,20 +24,28 @@ import java.util.regex.Pattern; import example.profilescan.DevComponent; import example.profilescan.ProfileAnnotatedComponent; import example.profilescan.ProfileMetaAnnotatedComponent; +import example.scannable.AutowiredQualifierFooService; +import example.scannable.CustomStereotype; +import example.scannable.DefaultNamedComponent; import example.scannable.FooDao; import example.scannable.FooService; import example.scannable.FooServiceImpl; import example.scannable.MessageBean; import example.scannable.NamedComponent; import example.scannable.NamedStubDao; +import example.scannable.ScopedProxyTestBean; import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; import org.aspectj.lang.annotation.Aspect; import org.junit.Test; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.index.CandidateComponentsTestClassLoader; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.RegexPatternTypeFilter; @@ -53,6 +61,7 @@ import static org.junit.Assert.*; * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams + * @author Stephane Nicoll */ public class ClassPathScanningCandidateComponentProviderTests { @@ -60,34 +69,192 @@ public class ClassPathScanningCandidateComponentProviderTests { private static final String TEST_PROFILE_PACKAGE = "example.profilescan"; private static final String TEST_DEFAULT_PROFILE_NAME = "testDefault"; + private static final ClassLoader TEST_BASE_CLASSLOADER = CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", NamedComponent.class)); + @Test - public void testWithDefaults() { + public void defaultsWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testDefault(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void defaultsWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testDefault(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testDefault(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertEquals(6, candidates.size()); + assertTrue(containsBeanClass(candidates, DefaultNamedComponent.class)); assertTrue(containsBeanClass(candidates, NamedComponent.class)); assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); assertTrue(containsBeanClass(candidates, StubFooDao.class)); assertTrue(containsBeanClass(candidates, NamedStubDao.class)); assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(6, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); } @Test - public void testWithBogusBasePackage() { + public void bogusPackageWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); Set candidates = provider.findCandidateComponents("bogus"); assertEquals(0, candidates.size()); } @Test - public void testWithPackageExcludeFilter() { + public void bogusPackageWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); - provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*"))); - Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + Set candidates = provider.findCandidateComponents("bogus"); assertEquals(0, candidates.size()); } + @Test + public void customFiltersFollowedByResetUseIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.resetFilters(true); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertBeanDefinitionType(candidates, AnnotatedGenericBeanDefinition.class); + } + + @Test + public void customAnnotationTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAnnotationTypeIncludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customAnnotationTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAnnotationTypeIncludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + testDefault(provider, expectedBeanDefinitionType); + } + + @Test + public void customAssignableTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAssignableTypeIncludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customAssignableTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAssignableTypeIncludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AssignableTypeFilter(FooService.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + // Interfaces/Abstract class are filtered out automatically. + assertTrue(containsBeanClass(candidates, AutowiredQualifierFooService.class)); + assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); + assertTrue(containsBeanClass(candidates, ScopedProxyTestBean.class)); + assertEquals(3, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + + @Test + public void customSupportedIncludeAndExcludedFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomSupportedIncludeAndExcludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customSupportedIncludeAndExcludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomSupportedIncludeAndExcludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, NamedComponent.class)); + assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(2, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + + @Test + public void customSupportIncludeFilterWithNonIndexedTypeUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + // This annotation type is not directly annotated with Indexed so we can use + // the index to find candidates + provider.addIncludeFilter(new AnnotationTypeFilter(CustomStereotype.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, DefaultNamedComponent.class)); + assertEquals(1, candidates.size()); + assertBeanDefinitionType(candidates, ScannedGenericBeanDefinition.class); + } + + @Test + public void customNotSupportedIncludeFilterUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, StubFooDao.class)); + assertEquals(1, candidates.size()); + assertBeanDefinitionType(candidates, ScannedGenericBeanDefinition.class); + } + + @Test + public void excludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void excludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testExclude(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); + assertTrue(containsBeanClass(candidates, StubFooDao.class)); + assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(3, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + @Test public void testWithNoFilters() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); @@ -306,14 +473,20 @@ public class ClassPathScanningCandidateComponentProviderTests { private boolean containsBeanClass(Set candidates, Class beanClass) { for (BeanDefinition candidate : candidates) { - ScannedGenericBeanDefinition definition = (ScannedGenericBeanDefinition) candidate; - if (beanClass.getName().equals(definition.getBeanClassName())) { + if (beanClass.getName().equals(candidate.getBeanClassName())) { return true; } } return false; } + private void assertBeanDefinitionType(Set candidates, + Class expectedType) { + candidates.forEach(c -> { + assertThat(c, is(instanceOf(expectedType))); + }); + } + @Profile(TEST_DEFAULT_PROFILE_NAME) @Component(DefaultProfileAnnotatedComponent.BEAN_NAME) diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java new file mode 100644 index 0000000000..674c3c9481 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java @@ -0,0 +1,111 @@ +/* + * 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.context.index; + +import java.io.IOException; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link CandidateComponentsIndexLoader}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexLoaderTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void validateIndexIsDisabledByDefault() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(null); + assertThat("No spring.components should be available at the default location", index, is(nullValue())); + } + + @Test + public void loadIndexSeveralMatches() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "foo"); + assertThat(components, containsInAnyOrder( + "org.springframework.context.index.Sample1", + "org.springframework.context.index.Sample2")); + } + + @Test + public void loadIndexSingleMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "biz"); + assertThat(components, containsInAnyOrder( + "org.springframework.context.index.Sample3")); + } + + @Test + public void loadIndexNoMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "none"); + assertThat(components, hasSize(0)); + } + + @Test + public void loadIndexNoPackage() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("com.example", "foo"); + assertThat(components, hasSize(0)); + } + + @Test + public void loadIndexNoSpringComponentsResource() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader())); + assertThat(index, is(nullValue())); + } + + @Test + public void loadIndexNoEntry() throws IOException { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("empty-spring.components", getClass()))); + assertThat(index, is(nullValue())); + } + + @Test + public void loadIndexWithException() throws IOException { + final IOException cause = new IOException("test exception"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Unable to load indexes"); + this.thrown.expectCause(is(cause)); + CandidateComponentsIndexLoader.loadIndex(new CandidateComponentsTestClassLoader( + getClass().getClassLoader(), cause)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java new file mode 100644 index 0000000000..b153dc65af --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java @@ -0,0 +1,95 @@ +/* + * 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.context.index; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; +import java.util.Set; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link CandidateComponentsIndex}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexTests { + + @Test + public void getCandidateTypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "service"); + assertThat(actual, containsInAnyOrder("com.example.service.One", + "com.example.service.sub.Two", "com.example.service.Three")); + } + + @Test + public void getCandidateTypesSubPackage() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.sub", "service"); + assertThat(actual, containsInAnyOrder("com.example.service.sub.Two")); + } + + @Test + public void getCandidateTypesSubPackageNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.none", "service"); + assertThat(actual, hasSize(0)); + } + + @Test + public void getCandidateTypesNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "entity"); + assertThat(actual, hasSize(0)); + } + + @Test + public void mergeCandidateStereotypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex(Arrays.asList( + createProperties("com.example.Foo", "service"), + createProperties("com.example.Foo", "entity"))); + assertThat(index.getCandidateTypes("com.example", "service"), + contains("com.example.Foo")); + assertThat(index.getCandidateTypes("com.example", "entity"), + contains("com.example.Foo")); + } + + private static Properties createProperties(String key, String stereotypes) { + Properties properties = new Properties(); + properties.put(key, String.join(",", stereotypes)); + return properties; + } + + private static Properties createSampleProperties() { + Properties properties = new Properties(); + properties.put("com.example.service.One", "service"); + properties.put("com.example.service.sub.Two", "service"); + properties.put("com.example.service.Three", "service"); + properties.put("com.example.domain.Four", "entity"); + return properties; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java new file mode 100644 index 0000000000..10697608d0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java @@ -0,0 +1,96 @@ +/* + * 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.context.index; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.io.Resource; + +/** + * A test {@link ClassLoader} that can be used in testing context to control the + * {@code spring.components} resource that should be loaded. Can also simulate a failure + * by throwing a configurable {@link IOException}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsTestClassLoader extends ClassLoader { + + /** + * Create a test {@link ClassLoader} that disable the use of the index, even + * if resources are present at the standard location. + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} that has no index + * @see CandidateComponentsIndexLoader#COMPONENTS_RESOURCE_LOCATION + */ + public static ClassLoader disableIndex(ClassLoader classLoader) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Collections.emptyList())); + } + + /** + * Create a test {@link ClassLoader} that creates an index with the + * specifed {@link Resource} instances + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} with an index built based on the + * specified resources. + */ + public static ClassLoader index(ClassLoader classLoader, Resource... resources) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Stream.of(resources).map(r -> { + try { + return r.getURL(); + } + catch (Exception ex) { + throw new IllegalArgumentException("Invalid resource " + r, ex); + } + }).collect(Collectors.toList()))); + } + + + private final Enumeration resourceUrls; + + private final IOException cause; + + public CandidateComponentsTestClassLoader(ClassLoader classLoader, Enumeration resourceUrls) { + super(classLoader); + this.resourceUrls = resourceUrls; + this.cause = null; + } + + public CandidateComponentsTestClassLoader(ClassLoader parent, IOException cause) { + super(parent); + this.resourceUrls = null; + this.cause = cause; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (CandidateComponentsIndexLoader.COMPONENTS_RESOURCE_LOCATION.equals(name)) { + if (this.resourceUrls != null) { + return this.resourceUrls; + } + throw this.cause; + } + return super.getResources(name); + } + +} diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components new file mode 100644 index 0000000000..62de4a95cd --- /dev/null +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -0,0 +1,9 @@ +example.scannable.AutowiredQualifierFooService=example.scannable.FooService +example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component +example.scannable.NamedComponent=org.springframework.stereotype.Component +example.scannable.FooService=example.scannable.FooService +example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService +example.scannable.ScopedProxyTestBean=example.scannable.FooService +example.scannable.StubFooDao=org.springframework.stereotype.Component +example.scannable.NamedStubDao=org.springframework.stereotype.Component +example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components new file mode 100644 index 0000000000..405817f874 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components @@ -0,0 +1,3 @@ +# +# Empty file to validate that if there is no entry we get a "null" index. +# \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/index/spring.components b/spring-context/src/test/resources/org/springframework/context/index/spring.components new file mode 100644 index 0000000000..c03f2f061a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/spring.components @@ -0,0 +1,3 @@ +org.springframework.context.index.Sample1=foo +org.springframework.context.index.Sample2=bar,foo +org.springframework.context.index.Sample3=biz \ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java index 2b2faa4d05..3e31aca161 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -76,6 +76,14 @@ public class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter this.considerMetaAnnotations = considerMetaAnnotations; } + /** + * Return the {@link Annotation} that this instance is using to filter + * candidates. + * @since 5.0 + */ + public final Class getAnnotationType() { + return this.annotationType; + } @Override protected boolean matchSelf(MetadataReader metadataReader) { diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java index 6365304627..d67056ff79 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -40,6 +40,13 @@ public class AssignableTypeFilter extends AbstractTypeHierarchyTraversingFilter this.targetType = targetType; } + /** + * Return the {@code type} that this instance is using to filter candidates. + * @since 5.0 + */ + public final Class getTargetType() { + return this.targetType; + } @Override protected boolean matchClassName(String className) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 8230dd59c2..2c236d2ebe 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -39,6 +39,7 @@ import org.junit.rules.ExpectedException; import org.springframework.core.annotation.AnnotationUtilsTests.WebController; import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Indexed; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -76,19 +77,19 @@ public class AnnotatedElementUtilsTests { @Test public void getMetaAnnotationTypesOnClassWithMetaDepth1() { Set names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class); - assertEquals(names(Transactional.class, Component.class), names); + assertEquals(names(Transactional.class, Component.class, Indexed.class), names); names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class.getName()); - assertEquals(names(Transactional.class, Component.class), names); + assertEquals(names(Transactional.class, Component.class, Indexed.class), names); } @Test public void getMetaAnnotationTypesOnClassWithMetaDepth2() { Set names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class); - assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class), names); + assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class), names); names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()); - assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class), names); + assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class), names); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 9a91be3af3..11d8839b5e 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -161,7 +161,7 @@ public class AnnotationMetadataTests { assertThat(metadata.hasAnnotation(Documented.class.getName()), is(true)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false)); - assertThat(metadata.getAnnotationTypes().size(), is(3)); + assertThat(metadata.getAnnotationTypes().size(), is(4)); } /** diff --git a/spring-core/src/test/java/org/springframework/stereotype/Component.java b/spring-core/src/test/java/org/springframework/stereotype/Component.java index e0b637f815..0df4525c66 100644 --- a/spring-core/src/test/java/org/springframework/stereotype/Component.java +++ b/spring-core/src/test/java/org/springframework/stereotype/Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * 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. @@ -42,6 +42,7 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Indexed public @interface Component { /** diff --git a/spring-core/src/test/java/org/springframework/stereotype/Indexed.java b/spring-core/src/test/java/org/springframework/stereotype/Indexed.java new file mode 100644 index 0000000000..386e347e74 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/stereotype/Indexed.java @@ -0,0 +1,34 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the standard {@code Indexed} annotation for testing purposes. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} 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 7dbfd32155..5821735701 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 @@ -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; *

NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0. * * @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 entityTypeFilters; + private static final Set entityTypeFilters; static { entityTypeFilters = new LinkedHashSet<>(4); @@ -147,6 +150,8 @@ public class DefaultPersistenceUnitManager private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private CandidateComponentsIndex componentsIndex; + private final Set persistenceUnitInfoNames = new HashSet<>(); private final Map 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 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); + } + + 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java new file mode 100644 index 0000000000..3b5f055291 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java @@ -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(); + } + +} diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components b/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components new file mode 100644 index 0000000000..4b6de536df --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components @@ -0,0 +1,2 @@ +org.springframework.orm.jpa.domain.Person=javax.persistence.Entity +org.springframework.orm.jpa.domain.DriversLicense=javax.persistence.Entity \ No newline at end of file