Support for candidate components index

This commit adds a "spring-context-indexer" module that can be added to
any project in order to generate an index of candidate components defined
in the project.

`CandidateComponentsIndexer` is a standard annotation processor that
looks for source files with target annotations (typically `@Component`)
and references them in a `META-INF/spring.components` generated file.

Each entry in the index is the fully qualified name of a candidate
component and the comma-separated list of stereotypes that apply to that
candidate. A typical example of a stereotype is `@Component`. If a
project has a `com.example.FooService` annotated with `@Component` the
following `META-INF/spring.components` file is generated at compile time:

```
com.example.FooService=org.springframework.stereotype.Component
```

A new `@Indexed` annotation can be added on any annotation to instructs
the scanner to include a source file that contains that annotation. For
instance, `@Component` is meta-annotated with `@Indexed` now and adding
`@Indexed` to more annotation types will transparently improve the index
with additional information. This also works for interaces or parent
classes: adding `@Indexed` on a `Repository` base interface means that
the indexed can be queried for its implementation by using the fully
qualified name of the `Repository` interface.

The indexer also adds any class or interface that has a type-level
annotation from the `javax` package. This includes obviously JPA
(`@Entity` and related) but also CDI (`@Named`, `@ManagedBean`) and
servlet annotations (i.e. `@WebFilter`). These are meant to handle
cases where a component needs to identify candidates and use classpath
scanning currently.

If a `package-info.java` file exists, the package is registered using
a "package-info" stereotype.

Such files can later be reused by the `ApplicationContext` to avoid
using component scan. A global `CandidateComponentsIndex` can be easily
loaded from the current classpath using `CandidateComponentsIndexLoader`.

The core framework uses such infrastructure in two areas: to retrieve
the candidate `@Component`s and to build a default `PersistenceUnitInfo`.
Rather than scanning the classpath and using ASM to identify candidates,
the index is used if present.

As long as the include filters refer to an annotation that is directly
annotated with `@Indexed` or an assignable type that is directly
annotated with `@Indexed`, the index can be used since a dedicated entry
wil be present for that type. If any other unsupported include filter is
specified, we fallback on classpath scanning.

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

Issue: SPR-11890
This commit is contained in:
Stephane Nicoll
2016-08-12 10:49:37 +02:00
parent 48d67a245b
commit dcade06fa0
68 changed files with 3221 additions and 47 deletions

View File

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

View File

@@ -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<? extends BeanDefinition> expectedBeanDefinitionType) {
Set<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER));
Set<BeanDefinition> 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<BeanDefinition> 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<? extends BeanDefinition> 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<? extends BeanDefinition> expectedBeanDefinitionType) {
provider.addIncludeFilter(new AssignableTypeFilter(FooService.class));
Set<BeanDefinition> 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<? extends BeanDefinition> expectedBeanDefinitionType) {
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class));
Set<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> 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<? extends BeanDefinition> expectedBeanDefinitionType) {
Set<BeanDefinition> 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<BeanDefinition> 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<BeanDefinition> candidates,
Class<? extends BeanDefinition> expectedType) {
candidates.forEach(c -> {
assertThat(c, is(instanceOf(expectedType)));
});
}
@Profile(TEST_DEFAULT_PROFILE_NAME)
@Component(DefaultProfileAnnotatedComponent.BEAN_NAME)

View File

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

View File

@@ -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<String> 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<String> 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<String> actual = index.getCandidateTypes("com.example.service.none", "service");
assertThat(actual, hasSize(0));
}
@Test
public void getCandidateTypesNoMatch() {
CandidateComponentsIndex index = new CandidateComponentsIndex(
Collections.singletonList(createSampleProperties()));
Set<String> 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;
}
}

View File

@@ -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<URL> resourceUrls;
private final IOException cause;
public CandidateComponentsTestClassLoader(ClassLoader classLoader, Enumeration<URL> 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<URL> 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);
}
}