Introduce ReflectiveScan
This commit allows `@Reflective` to be used on arbitrary types, not only Spring beans. This makes the feature much more powerful as components can be tagged directly. Scanning happens during AOT processing (typically at build-time) when `@ReflectiveScan` is used. Types do not need to have a particular annotation, and types that can't be loaded are ignored. This commit also exposes the infrastructure that does the scanning so that custom code can do the scanning in an AOT contribution if they don't want to rely on the annotation. Closes gh-33132
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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.aot;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.assertj.core.api.ObjectArrayAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
|
||||
import org.springframework.context.testfixture.context.aot.scan.noreflective.ReflectiveNotUsed;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnConstructor;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnField;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInnerField;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInterface;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnMethod;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnNestedType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnRecord;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReflectiveProcessorAotContributionBuilder}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ReflectiveProcessorAotContributionBuilderTests {
|
||||
|
||||
@Test
|
||||
void classesWithMatchingCandidates() {
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.withClasses(List.of(String.class, ReflectiveOnInterface.class, Integer.class)).build();
|
||||
assertDetectedClasses(contribution).containsOnly(ReflectiveOnInterface.class).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void classesWithMatchingCandidatesFiltersDuplicates() {
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class))
|
||||
.withClasses(new Class<?>[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class })
|
||||
.build();
|
||||
assertDetectedClasses(contribution)
|
||||
.containsOnly(ReflectiveOnInterface.class, ReflectiveOnField.class, ReflectiveOnMethod.class)
|
||||
.hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanWithMatchingCandidates() {
|
||||
String packageName = ReflectiveOnType.class.getPackageName();
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.scan(getClass().getClassLoader(), packageName).build();
|
||||
assertDetectedClasses(contribution).containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class,
|
||||
ReflectiveOnRecord.class, ReflectiveOnField.class, ReflectiveOnConstructor.class,
|
||||
ReflectiveOnMethod.class, ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanWithMatchingCandidatesInSubPackages() {
|
||||
String packageName = Reflective2OnType.class.getPackageName();
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.scan(getClass().getClassLoader(), packageName).build();
|
||||
assertDetectedClasses(contribution).containsOnly(Reflective2OnType.class,
|
||||
Reflective21OnType.class, Reflective22OnType.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanWithNoCandidate() {
|
||||
String packageName = ReflectiveNotUsed.class.getPackageName();
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.scan(getClass().getClassLoader(), packageName).build();
|
||||
assertThat(contribution).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void classesAndScanWithDuplicatesFiltersThem() {
|
||||
BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder()
|
||||
.withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class))
|
||||
.withClasses(new Class<?>[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class })
|
||||
.scan(null, ReflectiveOnType.class.getPackageName())
|
||||
.build();
|
||||
assertDetectedClasses(contribution)
|
||||
.containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class, ReflectiveOnRecord.class,
|
||||
ReflectiveOnField.class, ReflectiveOnConstructor.class, ReflectiveOnMethod.class,
|
||||
ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class)
|
||||
.hasSize(8);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private ObjectArrayAssert<Class> assertDetectedClasses(@Nullable BeanFactoryInitializationAotContribution contribution) {
|
||||
assertThat(contribution).isNotNull();
|
||||
return assertThat(contribution).extracting("classes", InstanceOfAssertFactories.array(Class[].class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import java.lang.reflect.Constructor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.TypeHint;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
import org.springframework.aot.hint.predicate.ReflectionHintsPredicates;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
@@ -30,6 +32,10 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContrib
|
||||
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.ReflectiveScan;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType;
|
||||
import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -68,6 +74,48 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessorTests {
|
||||
.accepts(this.generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTriggerScanningIfBeanUsesReflectiveScan() {
|
||||
process(SampleBeanWithReflectiveScan.class);
|
||||
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints().map(TypeHint::getType))
|
||||
.containsExactlyInAnyOrderElementsOf(TypeReference.listOf(
|
||||
Reflective2OnType.class, Reflective21OnType.class, Reflective22OnType.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBasePackagesToScanWhenNoCandidateIsEmpty() {
|
||||
Class<?>[] candidates = { String.class };
|
||||
assertThat(this.processor.findBasePackagesToScan(candidates)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBasePackagesToScanWithBasePackageClasses() {
|
||||
Class<?>[] candidates = { SampleBeanWithReflectiveScan.class };
|
||||
assertThat(this.processor.findBasePackagesToScan(candidates))
|
||||
.containsOnly(Reflective2OnType.class.getPackageName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBasePackagesToScanWithBasePackages() {
|
||||
Class<?>[] candidates = { SampleBeanWithReflectiveScanWithName.class };
|
||||
assertThat(this.processor.findBasePackagesToScan(candidates))
|
||||
.containsOnly(Reflective2OnType.class.getPackageName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBasePackagesToScanWithBasePackagesAndClasses() {
|
||||
Class<?>[] candidates = { SampleBeanWithMultipleReflectiveScan.class };
|
||||
assertThat(this.processor.findBasePackagesToScan(candidates))
|
||||
.containsOnly(Reflective21OnType.class.getPackageName(), Reflective22OnType.class.getPackageName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findBasePackagesToScanWithDuplicatesFiltersThem() {
|
||||
Class<?>[] candidates = { SampleBeanWithReflectiveScan.class, SampleBeanWithReflectiveScanWithName.class };
|
||||
assertThat(this.processor.findBasePackagesToScan(candidates))
|
||||
.containsOnly(Reflective2OnType.class.getPackageName());
|
||||
}
|
||||
|
||||
private void process(Class<?>... beanClasses) {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
for (Class<?> beanClass : beanClasses) {
|
||||
@@ -103,4 +151,17 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessorTests {
|
||||
|
||||
}
|
||||
|
||||
@ReflectiveScan(basePackageClasses = Reflective2OnType.class)
|
||||
static class SampleBeanWithReflectiveScan {
|
||||
}
|
||||
|
||||
@ReflectiveScan("org.springframework.context.testfixture.context.aot.scan.reflective2")
|
||||
static class SampleBeanWithReflectiveScanWithName {
|
||||
}
|
||||
|
||||
@ReflectiveScan(basePackageClasses = Reflective22OnType.class,
|
||||
basePackages = "org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21")
|
||||
static class SampleBeanWithMultipleReflectiveScan {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user