Skip to content

Commit

Permalink
Introduce ReflectiveScan
Browse files Browse the repository at this point in the history
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 spring-projectsgh-33132
  • Loading branch information
snicoll committed Jul 6, 2024
1 parent f165807 commit 4cad077
Show file tree
Hide file tree
Showing 21 changed files with 799 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.beans.factory.aot;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.lang.Nullable;

/**
* AOT contribution from a {@link BeanFactoryInitializationAotProcessor} used to
Expand All @@ -27,6 +28,7 @@
* {@link org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 6.0
* @see BeanFactoryInitializationAotProcessor
*/
Expand All @@ -41,4 +43,31 @@ public interface BeanFactoryInitializationAotContribution {
void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode);

/**
* Create a contribution that applies the contribution of the first contribution
* followed by the second contribution. Any contribution can be {@code null} to be
* ignored and the concatenated contribution is {@code null} if both inputs are
* {@code null}.
* @param a the first contribution
* @param b the second contribution
* @return the concatenation of the two contributions, or {@code null} if
* they are both {@code null}.
* @since 6.2
*/
@Nullable
static BeanFactoryInitializationAotContribution concat(@Nullable BeanFactoryInitializationAotContribution a,
@Nullable BeanFactoryInitializationAotContribution b) {

if (a == null) {
return b;
}
if (b == null) {
return a;
}
return (generationContext, code) -> {
a.applyTo(generationContext, code);
b.applyTo(generationContext, code);
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;

/**
Expand Down Expand Up @@ -61,9 +62,8 @@
* @author Brian Clozel
* @author Stephane Nicoll
* @since 6.0
* @see org.springframework.aot.hint.RuntimeHints
* @see org.springframework.aot.hint.annotation.Reflective
* @see org.springframework.aot.hint.annotation.RegisterReflection
* @see RuntimeHints
* @see ReflectiveScan
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.annotation;

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.aot.hint.annotation.Reflective;
import org.springframework.core.annotation.AliasFor;

/**
* Scan arbitrary types for use of {@link Reflective}. Typically used on
* {@link Configuration @Configuration} classes but can be added to any bean.
*
* <p>In the example below, {@code com.example.app} and its subpackages are
* scanned: <pre><code class="java">
* &#064;Configuration
* &#064;ReflectiveScan("com.example.app")
* class MyConfiguration {
* // ...
* }</code></pre>
*
* <p>Either {@link #basePackageClasses} or {@link #basePackages} (or its alias
* {@link #value}) may be specified to define specific packages to scan. If specific
* packages are not defined, scanning will occur recursively beginning with the
* package of the class that declares this annotation.
*
* <p>A type does not need to be annotated at class level to be candidate, and
* this performs a "deep scan" by loading every class in the target packages and
* search for {@link Reflective} on types, constructors, methods, and fields.
* Enclosed classes are candidates as well. Classes that fail to load are
* ignored.
*
* <p>Scanning happens during AOT processing, typically at build-time.
*
* @author Stephane Nicoll
* @see Reflective
* @since 6.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ReflectiveScan {

/**
* Alias for {@link #basePackages}.
* <p>Allows for more concise annotation declarations if no other attributes
* are needed &mdash; for example, {@code @ReflectiveScan("org.my.pkg")}
* instead of {@code @ReflectiveScan(basePackages = "org.my.pkg")}.
*/
@AliasFor("basePackages")
String[] value() default {};

/**
* Base packages to scan for reflective usage.
* <p>{@link #value} is an alias for (and mutually exclusive with) this
* attribute.
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor("value")
String[] basePackages() default {};

/**
* Type-safe alternative to {@link #basePackages} for specifying the packages
* to scan for reflection usage. The package of each class specified will be scanned.
* <p>Consider creating a special no-op marker class or interface in each package
* that serves no purpose other than being referenced by this attribute.
*/
Class<?>[] basePackageClasses() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.StreamSupport;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;

/**
* Helper class to create an AOT contribution that detects the presence of
* {@link Reflective @Reflective} on annotated elements and invoke the underlying
* {@link ReflectiveProcessor} implementations.
*
* @author Stephane Nicoll
* @since 6.2
*/
public abstract class ReflectiveProcessorAotContributionProvider {

private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();

/**
* Create an AOT contribution from the given classes by checking the ones
* that use {@link Reflective}. The returned contribution registers the
* necessary reflection hints as a result. If no class amongst the given
* classes use {@link Reflective}, or if the given iterable is empty,
* returns {@code null}.
* @param classes the classes to inspect
* @return an AOT contribution for the classes that use {@link Reflective}
* or {@code null} if they aren't any
*/
@Nullable
public static BeanFactoryInitializationAotContribution from(Iterable<Class<?>> classes) {
return from(StreamSupport.stream(classes.spliterator(), false).toArray(Class<?>[]::new));
}

/**
* Create an AOT contribution from the given classes by checking the ones
* that use {@link Reflective}. The returned contribution registers the
* necessary reflection hints as a result. If no class amongst the given
* classes use {@link Reflective}, or if the given iterable is empty,
* returns {@code null}.
* @param classes the classes to inspect
* @return an AOT contribution for the classes that use {@link Reflective}
* or {@code null} if they aren't any
*/
@Nullable
public static BeanFactoryInitializationAotContribution from(Class<?>[] classes) {
Class<?>[] types = Arrays.stream(classes).filter(registrar::isCandidate).toArray(Class<?>[]::new);
return (types.length > 0 ? new AotContribution(types) : null);
}

/**
* Scan the given {@code packageNames} and their sub-packages for classes
* that uses {@link Reflective} and create an AOT contribution with the
* result. If no candidates were found, return {@code null}.
* <p>This performs a "deep scan" by loading every class in the specified
* packages and search for {@link Reflective} on types, constructors, methods,
* and fields. Enclosed classes are candidates as well. Classes that fail to
* load are ignored.
* @param classLoader the classloader to use
* @param packageNames the package names to scan
* @return an AOT contribution for the identified classes or {@code null} if
* they aren't any
*/
@Nullable
public static BeanFactoryInitializationAotContribution scan(@Nullable ClassLoader classLoader, String... packageNames) {
ReflectiveClassPathScanner scanner = new ReflectiveClassPathScanner(classLoader);
Class<?>[] types = scanner.scan(packageNames);
return (types.length > 0 ? new AotContribution(types) : null);
}

private static class AotContribution implements BeanFactoryInitializationAotContribution {

private final Class<?>[] classes;

public AotContribution(Class<?>[] classes) {
this.classes = classes;
}

@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
registrar.registerRuntimeHints(runtimeHints, this.classes);
}

}

private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {

@Nullable
private final ClassLoader classLoader;

ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) {
super(false);
this.classLoader = classLoader;
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}

Class<?>[] scan(String... packageNames) {
if (logger.isDebugEnabled()) {
logger.debug("Scanning all types for reflective usage from " + Arrays.toString(packageNames));
}
Set<BeanDefinition> candidates = new HashSet<>();
for (String packageName : packageNames) {
candidates.addAll(findCandidateComponents(packageName));
}
return candidates.stream().map(c -> (Class<?>) c.getAttribute("type")).toArray(Class<?>[]::new);
}

@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
String className = beanDefinition.getBeanClassName();
if (className != null) {
try {
Class<?> type = ClassUtils.forName(className, this.classLoader);
beanDefinition.setAttribute("type", type);
return registrar.isCandidate(type);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace("Ignoring '%s' for reflective usage: %s".formatted(className, ex.getMessage()));
}
}
}
return false;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@
package org.springframework.context.aot;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.context.annotation.ReflectiveScan;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;

/**
* AOT {@code BeanFactoryInitializationAotProcessor} that detects the presence
Expand All @@ -39,32 +42,39 @@
*/
class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {

private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();


@Override
@Nullable
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
Class<?>[] beanTypes = Arrays.stream(beanFactory.getBeanDefinitionNames())
Class<?>[] beanClasses = Arrays.stream(beanFactory.getBeanDefinitionNames())
.map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
.toArray(Class<?>[]::new);
return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes);
String[] packagesToScan = findBasePackagesToScan(beanClasses);
return BeanFactoryInitializationAotContribution.concat(
ReflectiveProcessorAotContributionProvider.scan(beanFactory.getBeanClassLoader(), packagesToScan),
ReflectiveProcessorAotContributionProvider.from(beanClasses));
}


private static class ReflectiveProcessorBeanFactoryInitializationAotContribution
implements BeanFactoryInitializationAotContribution {

private final Class<?>[] types;

public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class<?>[] types) {
this.types = types;
protected String[] findBasePackagesToScan(Class<?>[] beanClasses) {
Set<String> basePackages = new LinkedHashSet<>();
for (Class<?> beanClass : beanClasses) {
ReflectiveScan reflectiveScan = AnnotatedElementUtils.getMergedAnnotation(beanClass, ReflectiveScan.class);
if (reflectiveScan != null) {
basePackages.addAll(extractBasePackages(reflectiveScan, beanClass));
}
}
return basePackages.toArray(new String[0]);
}

@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
registrar.registerRuntimeHints(runtimeHints, this.types);
private Set<String> extractBasePackages(ReflectiveScan annotation, Class<?> declaringClass) {
Set<String> basePackages = new LinkedHashSet<>();
Collections.addAll(basePackages, annotation.basePackages());
for (Class<?> clazz : annotation.basePackageClasses()) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
return basePackages;
}

}
Loading

0 comments on commit 4cad077

Please sign in to comment.