Add RuntimeHints predicates generator

The `RuntimeHints` API allows to describe hints for the reflection,
proxies and resources behavior at runtime. The need for a particular
behavior can be covered by several types of hints, at different levels.
This knowledge can be important in several cases:

* before contributing additional hints, infrastructure can check if an
  existing hint already covers the behavior
* this can be used in test suites and test infrastructure

This commit adds a new RuntimeHintsPredicates that generates `Predicate`
instances for testing `RuntimeHints` against a desired runtime behavior
for reflection, resources or proxies.

Closes gh-28555
This commit is contained in:
Brian Clozel
2022-06-10 17:04:54 +02:00
parent 100ce9642a
commit 9c9b2356ce
13 changed files with 1318 additions and 55 deletions

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2022 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.aot.hint;
import java.util.Arrays;
import java.util.function.Predicate;
import org.springframework.util.Assert;
/**
* Generator of {@link ProxyHints} predicates, testing whether the given hints
* match the expected behavior for proxies.
* @author Brian Clozel
* @since 6.0
*/
public class ProxyHintsPredicates {
ProxyHintsPredicates() {
}
/**
* Return a predicate that checks whether a {@link org.springframework.aot.hint.JdkProxyHint}
* is registered for the given interfaces.
* <p>Note that the order in which interfaces are given matters.
* @param interfaces the proxied interfaces
* @return the {@link RuntimeHints} predicate
* @see java.lang.reflect.Proxy
*/
public Predicate<RuntimeHints> forInterfaces(Class<?>... interfaces) {
Assert.notEmpty(interfaces, "'interfaces' should not be empty");
return forInterfaces(Arrays.stream(interfaces).map(TypeReference::of).toArray(TypeReference[]::new));
}
/**
* Return a predicate that checks whether a {@link org.springframework.aot.hint.JdkProxyHint}
* is registered for the given interfaces.
* <p>Note that the order in which interfaces are given matters.
* @param interfaces the proxied interfaces as type references
* @return the {@link RuntimeHints} predicate
* @see java.lang.reflect.Proxy
*/
public Predicate<RuntimeHints> forInterfaces(TypeReference... interfaces) {
Assert.notEmpty(interfaces, "'interfaces' should not be empty");
return hints -> hints.proxies().jdkProxies().anyMatch(proxyHint ->
proxyHint.getProxiedInterfaces().equals(Arrays.asList(interfaces)));
}
}

View File

@@ -0,0 +1,380 @@
/*
* Copyright 2002-2022 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.aot.hint;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.core.MethodIntrospector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Generator of {@link ReflectionHints} predicates, testing whether the given hints
* match the expected behavior for reflection.
* @author Brian Clozel
* @since 6.0
*/
public class ReflectionHintsPredicates {
ReflectionHintsPredicates() {
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given type.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param typeReference the type
* @return the {@link RuntimeHints} predicate
*/
public TypeHintPredicate onType(TypeReference typeReference) {
Assert.notNull(typeReference, "'typeReference' should not be null");
return new TypeHintPredicate(typeReference);
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given type.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param type the type
* @return the {@link RuntimeHints} predicate
*/
public TypeHintPredicate onType(Class<?> type) {
Assert.notNull(type, "'type' should not be null");
return new TypeHintPredicate(TypeReference.of(type));
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given constructor.
* By default, both introspection and invocation hints match.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param constructor the constructor
* @return the {@link RuntimeHints} predicate
*/
public ConstructorHintPredicate onConstructor(Constructor<?> constructor) {
Assert.notNull(constructor, "'constructor' should not be null");
return new ConstructorHintPredicate(constructor);
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given method.
* By default, both introspection and invocation hints match.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param method the method
* @return the {@link RuntimeHints} predicate
*/
public MethodHintPredicate onMethod(Method method) {
Assert.notNull(method, "'method' should not be null");
return new MethodHintPredicate(method);
}
/**
* Return a predicate that checks whether a reflection hint is registered for the method that matches the given selector.
* This looks up a method on the given type with the expected name, if unique.
* By default, both introspection and invocation hints match.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param type the type holding the method
* @param methodName the method name
* @return the {@link RuntimeHints} predicate
* @throws IllegalArgumentException if the method cannot be found or if multiple methods are found with the same name.
*/
public MethodHintPredicate onMethod(Class<?> type, String methodName) {
Assert.notNull(type, "'type' should not be null");
Assert.hasText(methodName, "'methodName' should not be null");
return new MethodHintPredicate(getMethod(type, methodName));
}
private Method getMethod(Class<?> type, String methodName) {
ReflectionUtils.MethodFilter selector = method -> methodName.equals(method.getName());
Set<Method> methods = MethodIntrospector.selectMethods(type, selector);
if (methods.size() == 1) {
return methods.iterator().next();
}
else if (methods.size() > 1) {
throw new IllegalArgumentException(String.format("Found multiple methods named '%s' on class %s", methodName, type.getName()));
}
else {
throw new IllegalArgumentException("No method named '" + methodName + "' on class " + type.getName());
}
}
/**
* Return a predicate that checks whether a reflection hint is registered for the field that matches the given selector.
* This looks up a field on the given type with the expected name, if present.
* By default, unsafe or write access are not considered.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param type the type holding the field
* @param fieldName the field name
* @return the {@link RuntimeHints} predicate
* @throws IllegalArgumentException if a field cannot be found with the given name.
*/
public FieldHintPredicate onField(Class<?> type, String fieldName) {
Assert.notNull(type, "'type' should not be null");
Assert.hasText(fieldName, "'fieldName' should not be empty");
Field field = ReflectionUtils.findField(type, fieldName);
if (field == null) {
throw new IllegalArgumentException("No field named '" + fieldName + "' on class " + type.getName());
}
return new FieldHintPredicate(field);
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given field.
* By default, unsafe or write access are not considered.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* @param field the field
* @return the {@link RuntimeHints} predicate
*/
public FieldHintPredicate onField(Field field) {
Assert.notNull(field, "'field' should not be null");
return new FieldHintPredicate(field);
}
public static class TypeHintPredicate implements Predicate<RuntimeHints> {
private final TypeReference type;
TypeHintPredicate(TypeReference type) {
this.type = type;
}
@Nullable
private TypeHint getTypeHint(RuntimeHints hints) {
return hints.reflection().getTypeHint(this.type);
}
@Override
public boolean test(RuntimeHints hints) {
return getTypeHint(hints) != null;
}
/**
* Refine the current predicate to only match if the given {@link MemberCategory} is present.
* @param memberCategory the member category
* @return the refined {@link RuntimeHints} predicate
*/
public Predicate<RuntimeHints> withMemberCategory(MemberCategory memberCategory) {
Assert.notNull(memberCategory, "'memberCategory' should not be null");
return this.and(hints -> getTypeHint(hints).getMemberCategories().contains(memberCategory));
}
/**
* Refine the current predicate to match if any of the given {@link MemberCategory categories} is present.
* @param memberCategories the member categories
* @return the refined {@link RuntimeHints} predicate
*/
public Predicate<RuntimeHints> withAnyMemberCategory(MemberCategory... memberCategories) {
Assert.notEmpty(memberCategories, "'memberCategories' should not be empty");
return this.and(hints -> Arrays.stream(memberCategories)
.anyMatch(memberCategory -> getTypeHint(hints).getMemberCategories().contains(memberCategory)));
}
}
public abstract static class ExecutableHintPredicate<T extends Executable> implements Predicate<RuntimeHints> {
protected final T executable;
protected ExecutableMode executableMode = ExecutableMode.INTROSPECT;
ExecutableHintPredicate(T executable) {
this.executable = executable;
}
/**
* Refine the current predicate to match for reflection introspection on the current type.
* @return the refined {@link RuntimeHints} predicate
*/
public ExecutableHintPredicate<T> introspect() {
this.executableMode = ExecutableMode.INTROSPECT;
return this;
}
/**
* Refine the current predicate to match for reflection invocation on the current type.
* @return the refined {@link RuntimeHints} predicate
*/
public ExecutableHintPredicate<T> invoke() {
this.executableMode = ExecutableMode.INVOKE;
return this;
}
@Override
public boolean test(RuntimeHints runtimeHints) {
return (new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass()))
.withAnyMemberCategory(getPublicMemberCategories())
.and(hints -> Modifier.isPublic(this.executable.getModifiers())))
.or(new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())).withAnyMemberCategory(getDeclaredMemberCategories()))
.or(exactMatch()).test(runtimeHints);
}
abstract MemberCategory[] getPublicMemberCategories();
abstract MemberCategory[] getDeclaredMemberCategories();
abstract Predicate<RuntimeHints> exactMatch();
/**
* Indicate whether the first {@code ExecutableHint} covers the reflection needs for the other one.
* For that, both hints must apply to the same member (same type, name and parameters)
* and the configured {@code ExecutableMode} of the first must cover the second.
*/
static boolean includes(ExecutableHint hint, ExecutableHint other) {
return hint.getName().equals(other.getName())
&& hint.getParameterTypes().equals(other.getParameterTypes())
&& (hint.getModes().contains(ExecutableMode.INVOKE)
|| !other.getModes().contains(ExecutableMode.INVOKE));
}
}
public static class ConstructorHintPredicate extends ExecutableHintPredicate<Constructor<?>> {
ConstructorHintPredicate(Constructor<?> constructor) {
super(constructor);
}
MemberCategory[] getPublicMemberCategories() {
if (this.executableMode == ExecutableMode.INTROSPECT) {
return new MemberCategory[] {MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS};
}
return new MemberCategory[] {MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS};
}
MemberCategory[] getDeclaredMemberCategories() {
if (this.executableMode == ExecutableMode.INTROSPECT) {
return new MemberCategory[] {MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS};
}
return new MemberCategory[] {MemberCategory.INVOKE_DECLARED_CONSTRUCTORS};
}
@Override
Predicate<RuntimeHints> exactMatch() {
return hints -> hints.reflection().getTypeHint(this.executable.getDeclaringClass()).constructors().anyMatch(executableHint -> {
List<TypeReference> parameters = Arrays.stream(this.executable.getParameterTypes()).map(TypeReference::of).toList();
ExecutableHint syntheticHint = ExecutableHint.ofConstructor(parameters)
.setModes(this.executableMode).build();
return includes(executableHint, syntheticHint);
});
}
}
public static class MethodHintPredicate extends ExecutableHintPredicate<Method> {
MethodHintPredicate(Method method) {
super(method);
}
MemberCategory[] getPublicMemberCategories() {
if (this.executableMode == ExecutableMode.INTROSPECT) {
return new MemberCategory[] {MemberCategory.INTROSPECT_PUBLIC_METHODS,
MemberCategory.INVOKE_PUBLIC_METHODS};
}
return new MemberCategory[] {MemberCategory.INVOKE_PUBLIC_METHODS};
}
MemberCategory[] getDeclaredMemberCategories() {
if (this.executableMode == ExecutableMode.INTROSPECT) {
return new MemberCategory[] {MemberCategory.INTROSPECT_DECLARED_METHODS,
MemberCategory.INVOKE_DECLARED_METHODS};
}
return new MemberCategory[] {MemberCategory.INVOKE_DECLARED_METHODS};
}
@Override
Predicate<RuntimeHints> exactMatch() {
return hints -> (hints.reflection().getTypeHint(this.executable.getDeclaringClass()) != null) &&
hints.reflection().getTypeHint(this.executable.getDeclaringClass()).methods().anyMatch(executableHint -> {
List<TypeReference> parameters = Arrays.stream(this.executable.getParameterTypes()).map(TypeReference::of).toList();
ExecutableHint syntheticHint = ExecutableHint.ofMethod(this.executable.getName(), parameters)
.setModes(this.executableMode).build();
return includes(executableHint, syntheticHint);
});
}
}
public static class FieldHintPredicate implements Predicate<RuntimeHints> {
private final Field field;
private boolean allowWrite;
private boolean allowUnsafeAccess;
FieldHintPredicate(Field field) {
this.field = field;
}
/**
* Refine the current predicate to match if write access is allowed on the field.
* @return the refined {@link RuntimeHints} predicate
* @see FieldHint#isAllowWrite()
*/
public FieldHintPredicate allowWrite() {
this.allowWrite = true;
return this;
}
/**
* Refine the current predicate to match if unsafe access is allowed on the field.
* @return the refined {@link RuntimeHints} predicate
* @see FieldHint#isAllowUnsafeAccess() ()
*/
public FieldHintPredicate allowUnsafeAccess() {
this.allowUnsafeAccess = true;
return this;
}
@Override
public boolean test(RuntimeHints runtimeHints) {
TypeHint typeHint = runtimeHints.reflection().getTypeHint(this.field.getDeclaringClass());
if (typeHint == null) {
return false;
}
return memberCategoryMatch(typeHint) || exactMatch(typeHint);
}
private boolean memberCategoryMatch(TypeHint typeHint) {
if (Modifier.isPublic(this.field.getModifiers())) {
return typeHint.getMemberCategories().contains(MemberCategory.PUBLIC_FIELDS)
|| typeHint.getMemberCategories().contains(MemberCategory.DECLARED_FIELDS);
}
else {
return typeHint.getMemberCategories().contains(MemberCategory.DECLARED_FIELDS);
}
}
private boolean exactMatch(TypeHint typeHint) {
return typeHint.fields().anyMatch(fieldHint ->
this.field.getName().equals(fieldHint.getName())
&& (!this.allowWrite || this.allowWrite == fieldHint.isAllowWrite())
&& (!this.allowUnsafeAccess || this.allowUnsafeAccess == fieldHint.isAllowUnsafeAccess()));
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2002-2022 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.aot.hint;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache;
/**
* Generator of {@link ResourceHints} predicates, testing whether the given hints
* match the expected behavior for resources.
* @author Brian Clozel
* @since 6.0
*/
public class ResourceHintsPredicates {
private static final ConcurrentLruCache<String, Pattern> CACHED_RESOURCE_PATTERNS = new ConcurrentLruCache<>(32, Pattern::compile);
ResourceHintsPredicates() {
}
/**
* Return a predicate that checks whether a resource hint is registered for the given bundle name.
* @param bundleName the resource bundle name
* @return the {@link RuntimeHints} predicate
*/
public Predicate<RuntimeHints> forBundle(String bundleName) {
Assert.hasText(bundleName, "resource bundle name should not be empty");
return runtimeHints -> runtimeHints.resources().resourceBundles()
.anyMatch(bundleHint -> bundleName.equals(bundleHint.getBaseName()));
}
/**
* Return a predicate that checks whether a resource hint is registered for the given
* resource name, located in the given type's package.
* <p>For example, {@code forResource(org.example.MyClass, "myResource.txt")}
* will match for {@code "/org/example/myResource.txt"}.
* @param type the type's package where to look for the resource
* @param resourceName the resource name
* @return the {@link RuntimeHints} predicate
*/
public Predicate<RuntimeHints> forResource(TypeReference type, String resourceName) {
String absoluteName = resolveAbsoluteResourceName(type, resourceName);
return forResource(absoluteName);
}
private String resolveAbsoluteResourceName(TypeReference type, String resourceName) {
if (resourceName.startsWith("/")) {
return resourceName;
}
else {
return "/" + type.getPackageName().replace('.', '/')
+ "/" + resourceName;
}
}
/**
* Return a predicate that checks whether a resource hint is registered for
* the given resource name.
* @param resourceName the full resource name
* @return the {@link RuntimeHints} predicate
*/
public Predicate<RuntimeHints> forResource(String resourceName) {
return hints -> hints.resources().resourcePatterns().reduce(ResourcePatternHints::merge)
.map(hint -> {
boolean isExcluded = hint.getExcludes().stream()
.anyMatch(excluded -> CACHED_RESOURCE_PATTERNS.get(excluded.getPattern()).matcher(resourceName).matches());
if (isExcluded) {
return false;
}
return hint.getIncludes().stream()
.anyMatch(included -> CACHED_RESOURCE_PATTERNS.get(included.getPattern()).matcher(resourceName).matches());
}).orElse(false);
}
}

View File

@@ -22,6 +22,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.lang.Nullable;
/**
* A collection of {@link ResourcePatternHint} describing whether
* resources should be made available at runtime through a matching
@@ -43,6 +45,11 @@ public final class ResourcePatternHints {
this.excludes = new ArrayList<>(builder.excludes);
}
private ResourcePatternHints(List<ResourcePatternHint> includes, List<ResourcePatternHint> excludes) {
this.includes = includes;
this.excludes = excludes;
}
/**
* Return the include patterns to use to identify the resources to match.
* @return the include patterns
@@ -59,6 +66,16 @@ public final class ResourcePatternHints {
return this.excludes;
}
ResourcePatternHints merge(ResourcePatternHints resourcePatternHints) {
List<ResourcePatternHint> includes = new ArrayList<>();
includes.addAll(this.includes);
includes.addAll(resourcePatternHints.includes);
List<ResourcePatternHint> excludes = new ArrayList<>();
excludes.addAll(this.excludes);
excludes.addAll(resourcePatternHints.excludes);
return new ResourcePatternHints(includes, excludes);
}
/**
* Builder for {@link ResourcePatternHints}.
@@ -75,7 +92,7 @@ public final class ResourcePatternHints {
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)
* @return {@code this}, to facilitate method chaining
*/
public Builder includes(TypeReference reachableType, String... includes) {
public Builder includes(@Nullable TypeReference reachableType, String... includes) {
List<ResourcePatternHint> newIncludes = Arrays.stream(includes)
.map(include -> new ResourcePatternHint(include, reachableType)).toList();
this.includes.addAll(newIncludes);

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2002-2022 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.aot.hint;
/**
* Static generator of predicates that test whether the given {@link RuntimeHints}
* instance matches the expected behavior for reflection, resource or proxy generation.
* <p>This utility class can be used by {@link RuntimeHintsRegistrar} to conditionally
* register hints depending on what's present already. This can also be used as a
* testing utility for checking proper registration of hints:
* <pre class="code">
* Predicate&lt;RuntimeHints&gt; predicate = RuntimeHintsPredicates.reflection().onMethod(MyClass.class, "someMethod").invoke();
* assertThat(predicate).accepts(runtimeHints);
* </pre>
* @author Brian Clozel
* @since 6.0
*/
public abstract class RuntimeHintsPredicates {
private static final ReflectionHintsPredicates reflection = new ReflectionHintsPredicates();
private static final ResourceHintsPredicates resource = new ResourceHintsPredicates();
private static final ProxyHintsPredicates proxies = new ProxyHintsPredicates();
private RuntimeHintsPredicates() {
}
/**
* Return a predicate generator for {@link ReflectionHints reflection hints}.
* @return the predicate generator
*/
public static ReflectionHintsPredicates reflection() {
return reflection;
}
/**
* Return a predicate generator for {@link ResourceHints resource hints}.
* @return the predicate generator
*/
public static ResourceHintsPredicates resource() {
return resource;
}
/**
* Return a predicate generator for {@link ProxyHints proxy hints}.
* @return the predicate generator
*/
public static ProxyHintsPredicates proxies() {
return proxies;
}
}