The index instance will usually be pre-populated for AOT runtime setups + * or test scenarios with pre-configured results for runtime-attempted scans. + * Alternatively, it may be empty for it to get populated during AOT processing + * or a test run, for subsequent introspection the index-recorded candidate types. + * @param classLoader the ClassLoader to add the index for + * @param index the associated CandidateComponentsIndex instance + * @since 7.0 + */ + public static void addIndex(ClassLoader classLoader, CandidateComponentsIndex index) { + cache.put(classLoader, index); + } + + /** + * Clear the runtime index cache. + * @since 7.0 + */ + public static void clearCache() { + cache.clear(); + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index 88619e095a5..db7fda429e8 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -16,10 +16,13 @@ package org.springframework.context.annotation; +import java.io.IOException; + import example.scannable.CustomComponent; import example.scannable.FooService; import example.scannable.FooServiceImpl; import example.scannable.NamedStubDao; +import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; import org.aspectj.lang.annotation.Aspect; import org.junit.jupiter.api.Test; @@ -35,9 +38,13 @@ import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.MessageSource; import org.springframework.context.annotation2.NamedStubDao2; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.stereotype.Component; @@ -61,6 +68,7 @@ class ClassPathBeanDefinitionScannerTests { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); @@ -73,8 +81,8 @@ class ClassPathBeanDefinitionScannerTests { assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); - context.refresh(); + context.refresh(); FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class); assertThat(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")).isTrue(); assertThat(fooService.foo(123)).isEqualTo("bar"); @@ -105,11 +113,30 @@ class ClassPathBeanDefinitionScannerTests { } @Test - void testDoubleScan() { + void testSimpleScanWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); + context.setClassLoader(CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", FooServiceImpl.class))); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isGreaterThanOrEqualTo(12); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); + } + + @Test + void testDoubleScan() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { @@ -129,25 +156,6 @@ class ClassPathBeanDefinitionScannerTests { assertThat(context.containsBean("thoreau")).isTrue(); } - @Test - void testWithIndex() { - GenericApplicationContext context = new GenericApplicationContext(); - context.setClassLoader(CandidateComponentsTestClassLoader.index( - ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), - new ClassPathResource("spring.components", FooServiceImpl.class))); - - ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); - int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(12); - - assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); - assertThat(context.containsBean("fooServiceImpl")).isTrue(); - assertThat(context.containsBean("stubFooDao")).isTrue(); - assertThat(context.containsBean("myNamedComponent")).isTrue(); - assertThat(context.containsBean("myNamedDao")).isTrue(); - assertThat(context.containsBean("thoreau")).isTrue(); - } - @Test void testDoubleScanWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); @@ -157,6 +165,7 @@ class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { @@ -182,8 +191,8 @@ class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(7); + assertThat(beanCount).isGreaterThanOrEqualTo(7); assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); assertThat(context.containsBean("stubFooDao")).isTrue(); @@ -482,12 +491,14 @@ class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(12); - context.refresh(); + assertThat(beanCount).isGreaterThanOrEqualTo(12); + + context.refresh(); FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class); StaticListableBeanFactory myBf = (StaticListableBeanFactory) context.getBean("myBf"); MessageSource ms = (MessageSource) context.getBean("messageSource"); + assertThat(fooService.isInitCalled()).isTrue(); assertThat(fooService.foo(123)).isEqualTo("bar"); assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); @@ -509,9 +520,10 @@ class ClassPathBeanDefinitionScannerTests { scanner.setIncludeAnnotationConfig(false); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(7); - context.refresh(); + assertThat(beanCount).isGreaterThanOrEqualTo(7); + + context.refresh(); try { context.getBean("fooService"); } @@ -545,9 +557,78 @@ class ClassPathBeanDefinitionScannerTests { scanner.setAutowireCandidatePatterns("*NoSuchDao"); scanner.scan(BASE_PACKAGE); context.refresh(); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - context.getBean("fooService")) - .satisfies(ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> context.getBean("fooService")) + .satisfies(ex -> + assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class)); + } + + @Test + void testWithManualProgrammaticIndex() { + // Pre-populating an index in order to replace a runtime scan + + GenericApplicationContext context = new GenericApplicationContext(); + context.setResourceLoader(new RestrictedResourcePatternResolver()); + + CandidateComponentsIndex index = new CandidateComponentsIndex(); + index.registerScan("example"); + index.registerCandidateType(ServiceInvocationCounter.class.getName(), Component.class.getName()); + index.registerCandidateType(FooServiceImpl.class.getName(), Component.class.getName()); + index.registerCandidateType(StubFooDao.class.getName(), Component.class.getName()); + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), index); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + int beanCount = scanner.scan(BASE_PACKAGE); // from index + CandidateComponentsIndexLoader.clearCache(); + + assertThat(beanCount).isEqualTo(3); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + } + + @Test + void testWithDerivedProgrammaticIndex() { + // Recording an index from a scan (e.g. during refreshForAotProcessing) + + GenericApplicationContext context = new GenericApplicationContext(); + + CandidateComponentsIndex scannedIndex = new CandidateComponentsIndex(); + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), scannedIndex); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.scan(BASE_PACKAGE); // actual scan, populating the index instance above + CandidateComponentsIndexLoader.clearCache(); + + // Rebuilding a pre-computed index from the scanned index (AOT style) + // through String-based registerScan and registerCandidateType calls. + + context = new GenericApplicationContext(); + context.setResourceLoader(new RestrictedResourcePatternResolver()); + + CandidateComponentsIndex derivedIndex = new CandidateComponentsIndex(); + for (String basePackage : scannedIndex.getRegisteredScans()) { + derivedIndex.registerScan(basePackage); + } + for (String stereotype : scannedIndex.getRegisteredStereotypes()) { + for (String type : scannedIndex.getCandidateTypes(BASE_PACKAGE, stereotype)) { + derivedIndex.registerCandidateType(type, stereotype); + } + } + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), derivedIndex); + + scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); // from index + CandidateComponentsIndexLoader.clearCache(); + + assertThat(beanCount).isGreaterThanOrEqualTo(12); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); } @@ -565,4 +646,14 @@ class ClassPathBeanDefinitionScannerTests { public class NonStaticInnerClass { } + + private static final class RestrictedResourcePatternResolver extends PathMatchingResourcePatternResolver { + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + throw new UnsupportedOperationException(locationPattern); + } + } + + } 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 index 8161f4f9359..32d4eb7c2a0 100644 --- a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java @@ -32,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Stephane Nicoll */ -@Deprecated -@SuppressWarnings("removal") public class CandidateComponentsIndexLoaderTests { @Test 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 index 2fbf17ce949..32befc3230e 100644 --- a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java @@ -30,8 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Stephane Nicoll */ -@Deprecated -@SuppressWarnings("removal") public class CandidateComponentsIndexTests { @Test diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components index c56e0854cb8..a8e99afa391 100644 --- a/spring-context/src/test/resources/example/scannable/spring.components +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -9,6 +9,5 @@ example.scannable.ScopedProxyTestBean=example.scannable.FooService example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component example.scannable.StubFooDao=org.springframework.stereotype.Component example.scannable.sub.BarComponent=org.springframework.stereotype.Component -example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.scannable.JakartaNamedComponent=jakarta.inject.Named example.indexed.IndexedJakartaNamedComponent=jakarta.inject.Named