diff --git a/pom.xml b/pom.xml index 13143c9f6f..daaee056d9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-GH-3362-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index e6935c5bb4..f3a99e17a7 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; +import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -35,6 +36,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.data.domain.ManagedTypes; import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeCollector; import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; import org.springframework.util.ClassUtils; @@ -125,9 +127,19 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { */ protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean) { - return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, this::contributeType); + return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, + typeCollectorCustomizer(), this::contributeType); } + /** + * Customization hook to configure {@link TypeCollector}. + * + * @return a {@link Consumer} to customize the {@link TypeCollector}, must not be {@literal null}. + * @since 4.0 + */ + protected Consumer typeCollectorCustomizer() { + return typeCollector -> {}; + } /** * Hook to contribute configuration for a given {@literal type}. * @@ -142,14 +154,27 @@ protected void contributeType(ResolvableType type, GenerationContext generationC Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); - aotContext.typeConfiguration(type, config -> config.forDataBinding() // - .contributeAccessors() // - .forQuerydsl().contribute(environment.get(), generationContext)); + configureTypeContribution(type.toClass(), aotContext); + + aotContext.typeConfiguration(type, config -> { + config.contribute(environment.get(), generationContext); + }); TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); } + /** + * Customization hook to configure the {@link TypeContributor} used to register the given {@literal type}. + * + * @param type the class to configure the contribution for. + * @param aotContext AOT context for type configuration. + * @since 4.0 + */ + protected void configureTypeContribution(Class type, AotContext aotContext) { + aotContext.typeConfiguration(type, config -> config.forDataBinding().contributeAccessors().forQuerydsl()); + } + protected boolean isMatch(@Nullable Class beanType, @Nullable String beanName) { return matchesByType(beanType) && matchesPrefix(beanName); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index e86f8c161f..2463576a4d 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -18,11 +18,12 @@ import java.lang.reflect.Method; import java.util.Collections; import java.util.List; -import java.util.function.BiConsumer; +import java.util.function.Consumer; import javax.lang.model.element.Modifier; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; @@ -74,17 +75,18 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContribution { private final AotContext aotContext; - private final ManagedTypes managedTypes; private final Lazy>> sourceTypes; + private final Consumer typeCollectorCustomizer; private final TypeRegistration contributionAction; private final RegisteredBean source; public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, - RegisteredBean registeredBean, TypeRegistration contributionAction) { + RegisteredBean registeredBean, Consumer typeCollectorCustomizer, + TypeRegistration contributionAction) { this.aotContext = aotContext; - this.managedTypes = managedTypes; this.sourceTypes = Lazy.of(managedTypes::toList); + this.typeCollectorCustomizer = typeCollectorCustomizer; this.contributionAction = contributionAction; this.source = registeredBean; } @@ -95,7 +97,8 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be List> types = sourceTypes.get(); if (!types.isEmpty()) { - TypeCollector.inspect(types).forEach(type -> contributionAction.register(type, generationContext, aotContext)); + TypeCollector.inspect(typeCollectorCustomizer, types) + .forEach(type -> contributionAction.register(type, generationContext, aotContext)); } } @@ -103,10 +106,6 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments(GenerationContext generationContext, BeanRegistrationCodeFragments codeFragments) { - if (managedTypes == null) { - return codeFragments; - } - ManagedTypesInstanceCodeFragment fragment = new ManagedTypesInstanceCodeFragment(sourceTypes.get(), source, codeFragments); return fragment.canGenerateCode() ? fragment : codeFragments; diff --git a/src/main/java/org/springframework/data/util/Predicates.java b/src/main/java/org/springframework/data/util/Predicates.java index f8ca3ec5b8..e22bce52f7 100644 --- a/src/main/java/org/springframework/data/util/Predicates.java +++ b/src/main/java/org/springframework/data/util/Predicates.java @@ -33,11 +33,9 @@ public interface Predicates { Predicate IS_ENUM_MEMBER = member -> member.getDeclaringClass().isEnum(); - Predicate IS_HIBERNATE_MEMBER = member -> member.getName().startsWith("$$_hibernate"); // this - // should - // go - // into - // JPA + + @Deprecated(since = "4.0", forRemoval = true) + Predicate IS_HIBERNATE_MEMBER = member -> member.getName().startsWith("$$_hibernate"); Predicate IS_OBJECT_MEMBER = member -> Object.class.equals(member.getDeclaringClass()); Predicate IS_JAVA = member -> member.getDeclaringClass().getPackageName().startsWith("java."); Predicate IS_NATIVE = member -> Modifier.isNative(member.getModifiers()); @@ -51,6 +49,16 @@ public interface Predicates { Predicate IS_BRIDGE_METHOD = Method::isBridge; + /** + * A {@link Predicate} that introspects the declaring class of the member. + * + * @return a {@link Predicate} that introspects the declaring class of the member. + * @since 4.0 + */ + static Predicate declaringClass(Predicate> predicate) { + return t -> predicate.test(t.getDeclaringClass()); + } + /** * A {@link Predicate} that yields always {@code true}. * diff --git a/src/main/java/org/springframework/data/util/TypeCollector.java b/src/main/java/org/springframework/data/util/TypeCollector.java index 0d605c8169..2e5f55b324 100644 --- a/src/main/java/org/springframework/data/util/TypeCollector.java +++ b/src/main/java/org/springframework/data/util/TypeCollector.java @@ -36,6 +36,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.core.ResolvableType; import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; @@ -46,44 +47,85 @@ *

* Type inspection walks through all class members (fields, methods, constructors) and introspects those for additional * types that are part of the domain model. + *

+ * Type collection can be customized by providing filters that stop introspection when encountering a {@link Predicate} + * that returns {@code false}. Filters are {@link Predicate#and(Predicate) combined} so that multiple filters can be + * taken into account. A type/field/method must pass all filters to be considered for further inspection. + *

+ * The collector uses {@link AotServices} to discover implementations of {@link TypeCollectorFilters} so that + * components using {@link TypeCollector} can contribute their own filtering logic to exclude types, fields, and methods + * from being inspected. * * @author Christoph Strobl * @author Sebastien Deleuze * @author John Blum + * @author Mark Paluch * @since 3.0 */ public class TypeCollector { private static final Log logger = LogFactory.getLog(TypeCollector.class); - static final Set EXCLUDED_DOMAINS = Set.of("java", "sun.", "jdk.", "reactor.", "kotlinx.", "kotlin.", "org.springframework.core.", - "org.springframework.data.mapping.", "org.springframework.data.repository.", "org.springframework.boot.", - "org.springframework.context.", "org.springframework.beans."); + private static final AotServices providers = AotServices.factories() + .load(TypeCollectorFilters.class); - private final Predicate> excludedDomainsFilter = type -> { - String packageName = type.getPackageName() + "."; - return EXCLUDED_DOMAINS.stream().noneMatch(packageName::startsWith); - }; + private Predicate> typeFilter = Predicates.isTrue(); - private Predicate> typeFilter = excludedDomainsFilter - .and(it -> !it.isLocalClass() && !it.isAnonymousClass()); + private Predicate methodFilter = Predicates.isTrue(); - private final Predicate methodFilter = createMethodFilter(); + private Predicate fieldFilter = Predicates.isTrue(); - private Predicate fieldFilter = createFieldFilter(); + /** + * Create a new {@link TypeCollector} applying all {@link TypeCollectorFilters} discovered through + * {@link AotServices}. + */ + public TypeCollector() { - @Contract("_ -> this") - public TypeCollector filterFields(Predicate filter) { - this.fieldFilter = filter.and(filter); - return this; + providers.forEach(provider -> { + filterTypes(provider.classPredicate()); + filterMethods(provider.methodPredicate()); + filterFields(provider.fieldPredicate()); + }); } + /** + * Add a filter to exclude types from being introspected. + * + * @param filter filter predicate matching a {@link Class}. + * @return {@code this} TypeCollector instance. + */ @Contract("_ -> this") public TypeCollector filterTypes(Predicate> filter) { this.typeFilter = this.typeFilter.and(filter); return this; } + /** + * Add a filter to exclude methods from being introspected. + * + * @param filter filter predicate matching a {@link Class}. + * @return {@code this} TypeCollector instance. + * @since 4.0 + */ + @Contract("_ -> this") + public TypeCollector filterMethods(Predicate filter) { + this.methodFilter = methodFilter.and(filter); + return this; + } + + /** + * Add a filter to exclude fields from being introspected. + * + * @param filter filter predicate matching a {@link Class}. + * @return {@code this} TypeCollector instance. + * @since 4.0 + */ + @Contract("_ -> this") + public TypeCollector filterFields(Predicate filter) { + this.fieldFilter = fieldFilter.and(filter); + return this; + } + /** * Inspect the given type and resolve those reachable via fields, methods, generics, ... * @@ -94,8 +136,40 @@ public static ReachableTypes inspect(Class... types) { return inspect(Arrays.asList(types)); } + /** + * Inspect the given type and resolve those reachable via fields, methods, generics, ... + * + * @param types the types to inspect + * @return a type model collector for the type + */ public static ReachableTypes inspect(Collection> types) { - return new ReachableTypes(new TypeCollector(), types); + return inspect(it -> {}, types); + } + + /** + * Inspect the given type and resolve those reachable via fields, methods, generics, ... + * + * @param collectorCustomizer the customizer function to configure the {@link TypeCollector}. + * @param types the types to inspect. + * @return a type model collector for the type. + * @since 4.0 + */ + public static ReachableTypes inspect(Consumer collectorCustomizer, Class... types) { + return inspect(collectorCustomizer, Arrays.asList(types)); + } + + /** + * Inspect the given type and resolve those reachable via fields, methods, generics, ... + * + * @param collectorCustomizer the customizer function to configure the {@link TypeCollector}. + * @param types the types to inspect. + * @return a type model collector for the type. + * @since 4.0 + */ + public static ReachableTypes inspect(Consumer collectorCustomizer, Collection> types) { + TypeCollector typeCollector = new TypeCollector(); + collectorCustomizer.accept(typeCollector); + return new ReachableTypes(typeCollector, types); } private void process(Class root, Consumer consumer) { @@ -130,7 +204,7 @@ private void processType(ResolvableType type, InspectionCache cache, Consumer visitConstructorsOfType(ResolvableType type) { + private Set visitConstructorsOfType(ResolvableType type) { if (!typeFilter.test(type.toClass())) { return Collections.emptySet(); @@ -153,7 +227,7 @@ Set visitConstructorsOfType(ResolvableType type) { return new HashSet<>(discoveredTypes); } - Set visitMethodsOfType(ResolvableType type) { + private Set visitMethodsOfType(ResolvableType type) { if (!typeFilter.test(type.toClass())) { return Collections.emptySet(); @@ -178,7 +252,7 @@ Set visitMethodsOfType(ResolvableType type) { return new HashSet<>(discoveredTypes); } - Set visitFieldsOfType(ResolvableType type) { + private Set visitFieldsOfType(ResolvableType type) { Set discoveredTypes = new LinkedHashSet<>(); @@ -196,51 +270,37 @@ Set visitFieldsOfType(ResolvableType type) { return discoveredTypes; } - private Predicate createMethodFilter() { - - Predicate excludedDomainsPredicate = methodToTest -> excludedDomainsFilter - .test(methodToTest.getDeclaringClass()); - - Predicate excludedMethodsPredicate = Predicates.IS_BRIDGE_METHOD // - .or(Predicates.IS_STATIC) // - .or(Predicates.IS_SYNTHETIC) // - .or(Predicates.IS_NATIVE) // - .or(Predicates.IS_PRIVATE) // - .or(Predicates.IS_PROTECTED) // - .or(Predicates.IS_OBJECT_MEMBER) // - .or(Predicates.IS_HIBERNATE_MEMBER) // - .or(Predicates.IS_ENUM_MEMBER) // - .or(excludedDomainsPredicate.negate()); // - - return excludedMethodsPredicate.negate(); - } - - @SuppressWarnings("rawtypes") - private Predicate createFieldFilter() { - - Predicate excludedFieldPredicate = Predicates.IS_HIBERNATE_MEMBER // - .or(Predicates.IS_SYNTHETIC) // - .or(Predicates.IS_JAVA); - - return (Predicate) excludedFieldPredicate.negate(); - } - + /** + * Container for reachable types starting from a set of root types. + */ public static class ReachableTypes { private final Iterable> roots; private final Lazy>> reachableTypes = Lazy.of(this::collect); private final TypeCollector typeCollector; - public ReachableTypes(TypeCollector typeCollector, Iterable> roots) { + ReachableTypes(TypeCollector typeCollector, Iterable> roots) { this.typeCollector = typeCollector; this.roots = roots; } - public void forEach(Consumer consumer) { - roots.forEach(it -> typeCollector.process(it, consumer)); + /** + * Performs the given action for each element of the reachable types until all elements have been processed or the + * action throws an exception. Actions are performed in the order of iteration, if that order is specified. + * Exceptions thrown by the action are relayed to the caller. + * + * @param action The action to be performed for each element + */ + public void forEach(Consumer action) { + roots.forEach(it -> typeCollector.process(it, action)); } + /** + * Return all reachable types as list of {@link Class classes}. The resulting list is unmodifiable. + * + * @return an unmodifiable list of reachable types. + */ public List> list() { return reachableTypes.get(); } @@ -248,8 +308,9 @@ public List> list() { private List> collect() { List> target = new ArrayList<>(); forEach(it -> target.add(it.toClass())); - return target; + return List.copyOf(target); } + } static class InspectionCache { @@ -275,5 +336,111 @@ public boolean isEmpty() { public int size() { return mutableCache.size(); } + } + + /** + * Strategy interface providing predicates to filter types, fields, and methods from being introspected and + * contributed to AOT processing. + *

+ * {@code BeanRegistrationAotProcessor} implementations must be registered in a + * {@value AotServices#FACTORIES_RESOURCE_LOCATION} resource. This interface serves as SPI and can be provided through + * {@link org.springframework.beans.factory.aot.AotServices}. + *

+ * {@link TypeCollector} discovers all implementations and applies the combined predicates returned by this interface + * to filter unwanted reachable types from AOT contribution. + * + * @author Mark Paluch + * @since 4.0 + */ + public interface TypeCollectorFilters { + + /** + * Return a predicate to filter types. + * + * @return a predicate to filter types. + */ + default Predicate> classPredicate() { + return Predicates.isTrue(); + } + + /** + * Return a predicate to filter fields. + * + * @return a predicate to filter fields. + */ + default Predicate fieldPredicate() { + return Predicates.isTrue(); + } + + /** + * Return a predicate to filter methods for method signature introspection. not provided. + * + * @return a predicate to filter methods. + */ + default Predicate methodPredicate() { + return Predicates.isTrue(); + } + + } + + /** + * Default implementation of {@link TypeCollectorFilters} that excludes types from certain packages and + * filters out unwanted fields and methods. + * + * @since 4.0 + */ + private static class DefaultTypeCollectorFilters implements TypeCollectorFilters { + + private static final Set EXCLUDED_DOMAINS = Set.of("java", "sun.", "jdk.", "reactor.", "kotlinx.", + "kotlin.", "org.springframework.core.", "org.springframework.data.mapping.", + "org.springframework.data.repository.", "org.springframework.boot.", "org.springframework.context.", + "org.springframework.beans."); + + private static final Predicate> PACKAGE_PREDICATE = type -> { + + String packageName = type.getPackageName() + "."; + + for (String excludedDomain : EXCLUDED_DOMAINS) { + if (packageName.startsWith(excludedDomain)) { + return true; + } + } + + return false; + }; + + private static final Predicate> UNREACHABLE_CLASS = type -> type.isLocalClass() || type.isAnonymousClass(); + + private static final Predicate UNWANTED_FIELDS = Predicates.IS_SYNTHETIC // + .or(Predicates.IS_JAVA) // + .or(Predicates.declaringClass(PACKAGE_PREDICATE)); + + private static final Predicate UNWANTED_METHODS = Predicates.IS_BRIDGE_METHOD // + .or(Predicates.IS_STATIC) // + .or(Predicates.IS_SYNTHETIC) // + .or(Predicates.IS_NATIVE) // + .or(Predicates.IS_PRIVATE) // + .or(Predicates.IS_PROTECTED) // + .or(Predicates.IS_OBJECT_MEMBER) // + .or(Predicates.IS_ENUM_MEMBER) // + .or(Predicates.declaringClass(PACKAGE_PREDICATE)); + + @Override + public Predicate> classPredicate() { + return UNREACHABLE_CLASS.or(PACKAGE_PREDICATE).negate(); + } + + @Override + public Predicate fieldPredicate() { + return (Predicate) UNWANTED_FIELDS.negate(); + } + + @Override + public Predicate methodPredicate() { + return UNWANTED_METHODS.negate(); + } + + } + } diff --git a/src/main/resources/META-INF/spring/aot.factories b/src/main/resources/META-INF/spring/aot.factories index 34bca58483..977f848a81 100644 --- a/src/main/resources/META-INF/spring/aot.factories +++ b/src/main/resources/META-INF/spring/aot.factories @@ -8,3 +8,6 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.data.aot.AuditingBeanRegistrationAotProcessor + +org.springframework.data.util.TypeCollector$TypeCollectorFilters=\ + org.springframework.data.util.TypeCollector$DefaultTypeCollectorFilters diff --git a/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java b/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java index 2c6574f4ff..83a7a53e41 100644 --- a/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java +++ b/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java @@ -22,7 +22,10 @@ import org.springframework.data.util.TypeCollector; /** + * Unit tests for {@link TypeCollector}. + * * @author Christoph Strobl + * @author Mark Paluch */ public class TypeCollectorUnitTests { @@ -66,4 +69,12 @@ void skipsCoreFrameworkType() { assertThat(TypeCollector.inspect(org.springframework.core.AliasRegistry.class).list()).isEmpty(); } + @Test // GH-3362 + void appliesFilterPredicate() { + assertThat(TypeCollector + .inspect(it -> it.filterTypes(cls -> cls == EmptyType1.class || cls == TypesInMethodSignatures.class), + TypesInMethodSignatures.class) + .list()).containsOnly(TypesInMethodSignatures.class, EmptyType1.class); + } + }