Skip to content

Commit

Permalink
Support for candidate components index
Browse files Browse the repository at this point in the history
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.

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. If custom include filters are specified,
the index is not used.

In case the index is incomplete or cannot be used, The
`spring.index.ignore` system property can be set to `false` or,
alternatively, in a "spring.properties" at the root of the classpath.

Issue: SPR-11890
  • Loading branch information
snicoll committed Aug 27, 2016
1 parent e6353f0 commit 4cd158b
Show file tree
Hide file tree
Showing 51 changed files with 2,617 additions and 48 deletions.
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,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"
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StereotypesProvider> 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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
this.metadataCollector.processing(roundEnv);
roundEnv.getRootElements().forEach(this::processElement);

if (roundEnv.processingOver()) {
writeMetaData();
}
return false;
}

protected List<StereotypesProvider> getStereotypesProviders(ProcessingEnvironment env) {
List<StereotypesProvider> 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<String> 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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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;

/**
* 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<String> getStereotypes(Element element) {
Set<String> stereotypes = new LinkedHashSet<>();
ElementKind kind = element.getKind();
if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) {
return stereotypes;
}
Set<Element> seen = new HashSet<>();
collectStereotypes(seen, stereotypes, element);
return stereotypes;
}

private void collectStereotypes(Set<Element> seen, Set<String> stereotypes,
Element element) {
for (AnnotationMirror annotation : this.typeUtils.getAllAnnotationMirrors(element)) {
Element next = collectStereotypes(seen, stereotypes, element, annotation);
if (next != null) {
collectStereotypes(seen, stereotypes, next);
}
}
}

private Element collectStereotypes(Set<Element> seen, Set<String> stereotypes,
Element element, AnnotationMirror annotation) {
if (isIndexedAnnotation(annotation)) {
stereotypes.add(this.typeUtils.getType(element));
}
return getCandidateAnnotationElement(seen, annotation);
}

private Element getCandidateAnnotationElement(Set<Element> 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 isIndexedAnnotation(AnnotationMirror annotation) {
return INDEXED_ANNOTATION.equals(annotation.getAnnotationType().toString());
}
}
Original file line number Diff line number Diff line change
@@ -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<ItemMetadata> metadataItems = new ArrayList<ItemMetadata>();

private final ProcessingEnvironment processingEnvironment;

private final CandidateComponentsMetadata previousMetadata;

private final TypeUtils typeUtils;

private final Set<String> processedSourceTypes = new HashSet<String>();

/**
* 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<ItemMetadata> 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);
}

}
Loading

0 comments on commit 4cd158b

Please sign in to comment.