> 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);
+ }
+
}