diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java index e1ff8ecc3a3..9b53915eb58 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java @@ -28,8 +28,8 @@ import javax.lang.model.element.Modifier; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedMethods; -import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; import org.springframework.aot.test.generator.compile.Compiled; import org.springframework.aot.test.generator.compile.TestCompiler; import org.springframework.beans.factory.config.BeanDefinition; @@ -277,15 +277,8 @@ class BeanDefinitionPropertiesCodeGeneratorTests { } private void assertHasMethodInvokeHints(Class beanType, String... methodNames) { - assertThat(this.hints.reflection().getTypeHint(beanType)).satisfies(typeHint -> { - for (String methodName : methodNames) { - assertThat(typeHint.methods()).anySatisfy(methodHint -> { - assertThat(methodHint.getName()).isEqualTo(methodName); - assertThat(methodHint.getModes()) - .containsExactly(ExecutableMode.INVOKE); - }); - } - }); + assertThat(methodNames).allMatch(methodName -> + RuntimeHintsPredicates.reflection().onMethod(beanType, methodName).invoke().test(this.hints)); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java index 60516491bb4..548977aa1c3 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanRegistrationAotProcessorTests.java @@ -29,6 +29,7 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.InMemoryGeneratedFiles; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -96,8 +97,7 @@ class ReflectiveProcessorBeanRegistrationAotProcessorTests { void shouldRegisterAnnotation() { process(SampleMethodMetaAnnotatedBean.class); RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); - assertThat(runtimeHints.reflection().getTypeHint(SampleInvoker.class)).satisfies(typeHint -> - assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThat(RuntimeHintsPredicates.reflection().onType(SampleInvoker.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(runtimeHints); assertThat(runtimeHints.proxies().jdkProxies()).isEmpty(); } @@ -105,11 +105,8 @@ class ReflectiveProcessorBeanRegistrationAotProcessorTests { void shouldRegisterAnnotationAndProxyWithAliasFor() { process(SampleMethodMetaAnnotatedBeanWithAlias.class); RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); - assertThat(runtimeHints.reflection().getTypeHint(RetryInvoker.class)).satisfies(typeHint -> - assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_DECLARED_METHODS)); - assertThat(runtimeHints.proxies().jdkProxies()).anySatisfy(jdkProxyHint -> - assertThat(jdkProxyHint.getProxiedInterfaces()).containsExactly( - TypeReference.of(RetryInvoker.class), TypeReference.of(SynthesizedAnnotation.class))); + assertThat(RuntimeHintsPredicates.reflection().onType(RetryInvoker.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(runtimeHints); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(RetryInvoker.class, SynthesizedAnnotation.class)).accepts(runtimeHints); } @Nullable @@ -196,7 +193,7 @@ class ReflectiveProcessorBeanRegistrationAotProcessorTests { } - @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Reflective @@ -206,7 +203,7 @@ class ReflectiveProcessorBeanRegistrationAotProcessorTests { } - @Target({ ElementType.METHOD }) + @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @SampleInvoker diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ProxyHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/ProxyHintsPredicates.java new file mode 100644 index 00000000000..f4d83126c28 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/ProxyHintsPredicates.java @@ -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. + *

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 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. + *

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 forInterfaces(TypeReference... interfaces) { + Assert.notEmpty(interfaces, "'interfaces' should not be empty"); + return hints -> hints.proxies().jdkProxies().anyMatch(proxyHint -> + proxyHint.getProxiedInterfaces().equals(Arrays.asList(interfaces))); + } +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHintsPredicates.java new file mode 100644 index 00000000000..1f88ea15029 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHintsPredicates.java @@ -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. + *

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. + *

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. + *

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. + *

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. + *

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 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. + *

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. + *

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 { + + 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 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 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 implements Predicate { + + 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 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 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 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> { + + 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 exactMatch() { + return hints -> hints.reflection().getTypeHint(this.executable.getDeclaringClass()).constructors().anyMatch(executableHint -> { + List 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 { + + + 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 exactMatch() { + return hints -> (hints.reflection().getTypeHint(this.executable.getDeclaringClass()) != null) && + hints.reflection().getTypeHint(this.executable.getDeclaringClass()).methods().anyMatch(executableHint -> { + List 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 { + + 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())); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHintsPredicates.java new file mode 100644 index 00000000000..6de4d64979f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHintsPredicates.java @@ -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 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 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. + *

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 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 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); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java index 280e1d8feb6..e3568c14f34 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java @@ -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 includes, List 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 includes = new ArrayList<>(); + includes.addAll(this.includes); + includes.addAll(resourcePatternHints.includes); + List 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 newIncludes = Arrays.stream(includes) .map(include -> new ResourcePatternHint(include, reachableType)).toList(); this.includes.addAll(newIncludes); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsPredicates.java new file mode 100644 index 00000000000..0c7635e823f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsPredicates.java @@ -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. + *

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: + *

+ * Predicate<RuntimeHints> predicate = RuntimeHintsPredicates.reflection().onMethod(MyClass.class, "someMethod").invoke();
+ * assertThat(predicate).accepts(runtimeHints);
+ * 
+ * @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; + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ProxyHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ProxyHintsPredicatesTests.java new file mode 100644 index 00000000000..d641e3fe205 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/ProxyHintsPredicatesTests.java @@ -0,0 +1,76 @@ +/* + * 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ProxyHintsPredicates}. + * + * @author Brian Clozel + */ +class ProxyHintsPredicatesTests { + + private final ProxyHintsPredicates proxy = new ProxyHintsPredicates(); + + private RuntimeHints runtimeHints; + + @BeforeEach + void setup() { + this.runtimeHints = new RuntimeHints(); + } + + @Test + void shouldFailForEmptyInterfacesArray() { + assertThatThrownBy(() -> this.proxy.forInterfaces(new Class[] {})).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void proxyForInterfacesMatchesProxyHint() { + this.runtimeHints.proxies().registerJdkProxy(FirstTestInterface.class, SecondTestInterface.class); + assertPredicateMatches(this.proxy.forInterfaces(FirstTestInterface.class, SecondTestInterface.class)); + } + + @Test + void proxyForInterfacesDoesNotMatchProxyHintDifferentOrder() { + this.runtimeHints.proxies().registerJdkProxy(SecondTestInterface.class, FirstTestInterface.class); + assertPredicateDoesNotMatch(this.proxy.forInterfaces(FirstTestInterface.class, SecondTestInterface.class)); + } + + interface FirstTestInterface { + + } + + interface SecondTestInterface { + + } + + private void assertPredicateMatches(Predicate predicate) { + assertThat(predicate.test(this.runtimeHints)).isTrue(); + } + + private void assertPredicateDoesNotMatch(Predicate predicate) { + assertThat(predicate.test(this.runtimeHints)).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsPredicatesTests.java new file mode 100644 index 00000000000..95504552208 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsPredicatesTests.java @@ -0,0 +1,509 @@ +/* + * 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.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ReflectionHintsPredicates} + * + * @author Brian Clozel + */ +class ReflectionHintsPredicatesTests { + + private static Constructor privateConstructor; + + private static Constructor publicConstructor; + + private final ReflectionHintsPredicates reflection = new ReflectionHintsPredicates(); + + private RuntimeHints runtimeHints; + + + @BeforeAll + static void setupAll() throws Exception { + privateConstructor = SampleClass.class.getDeclaredConstructor(String.class); + publicConstructor = SampleClass.class.getConstructor(); + } + + @BeforeEach + void setup() { + this.runtimeHints = new RuntimeHints(); + } + + // Reflection on type + + @Test + void shouldFailForNullType() { + assertThatThrownBy(() -> reflection.onType((TypeReference) null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void reflectionOnClassShouldMatchIntrospection() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> { + }); + assertPredicateMatches(reflection.onType(SampleClass.class)); + } + + @Test + void reflectionOnTypeReferenceShouldMatchIntrospection() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> { + }); + assertPredicateMatches(reflection.onType(TypeReference.of(SampleClass.class))); + } + + @Test + void reflectionOnDifferentClassShouldNotMatchIntrospection() { + this.runtimeHints.reflection().registerType(Integer.class, builder -> { + }); + assertPredicateDoesNotMatch(reflection.onType(TypeReference.of(SampleClass.class))); + } + + @Test + void typeWithMemberCategoryFailsWithNullCategory() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatThrownBy(() -> reflection.onType(SampleClass.class).withMemberCategory(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void typeWithMemberCategoryMatchesCategory() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateMatches(reflection.onType(SampleClass.class).withMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + } + + @Test + void typeWithMemberCategoryDoesNotMatchOtherCategory() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onType(SampleClass.class).withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)); + } + + @Test + void typeWithAnyMemberCategoryFailsWithNullCategories() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertThatThrownBy(() -> reflection.onType(SampleClass.class).withAnyMemberCategory(new MemberCategory[]{})).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void typeWithAnyMemberCategoryMatchesCategory() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateMatches(reflection.onType(SampleClass.class).withAnyMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + } + + @Test + void typeWithAnyMemberCategoryDoesNotMatchOtherCategory() { + this.runtimeHints.reflection().registerType(SampleClass.class, builder -> builder.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onType(SampleClass.class).withAnyMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)); + } + + // Reflection on constructor + + @Test + void constructorIntrospectionMatchesConstructorHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), constructorHint -> { + })); + assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); + } + + @Test + void constructorIntrospectionMatchesIntrospectPublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); + } + + @Test + void constructorIntrospectionMatchesInvokePublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); + } + + @Test + void constructorIntrospectionMatchesIntrospectDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); + } + + @Test + void constructorIntrospectionMatchesInvokeDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); + } + + @Test + void constructorInvocationDoesNotMatchConstructorHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), constructorHint -> { + })); + assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void constructorInvocationMatchesConstructorInvocationHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(Collections.emptyList(), constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE))); + assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void constructorInvocationDoesNotMatchIntrospectPublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void constructorInvocationMatchesInvokePublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void constructorInvocationDoesNotMatchIntrospectDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void constructorInvocationMatchesInvokeDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); + } + + @Test + void privateConstructorIntrospectionMatchesConstructorHint() { + List parameterTypes = Collections.singletonList(TypeReference.of(String.class)); + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(parameterTypes, constructorHint -> { + })); + assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); + } + + @Test + void privateConstructorIntrospectionDoesNotMatchIntrospectPublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).introspect()); + } + + @Test + void privateConstructorIntrospectionDoesNotMatchInvokePublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).introspect()); + } + + @Test + void privateConstructorIntrospectionMatchesIntrospectDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); + } + + @Test + void privateConstructorIntrospectionMatchesInvokeDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); + } + + @Test + void privateConstructorInvocationDoesNotMatchConstructorHint() { + List parameterTypes = Collections.singletonList(TypeReference.of(String.class)); + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(parameterTypes, constructorHint -> { + })); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + } + + @Test + void privateConstructorInvocationMatchesConstructorInvocationHint() { + List parameterTypes = Collections.singletonList(TypeReference.of(String.class)); + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(parameterTypes, constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE))); + assertPredicateMatches(reflection.onConstructor(privateConstructor).invoke()); + } + + @Test + void privateConstructorInvocationDoesNotMatchIntrospectPublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + } + + @Test + void privateConstructorInvocationDoesNotMatchInvokePublicConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + } + + @Test + void privateConstructorInvocationDoesNotMatchIntrospectDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS)); + assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + } + + @Test + void privateConstructorInvocationMatchesInvokeDeclaredConstructors() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + assertPredicateMatches(reflection.onConstructor(privateConstructor).invoke()); + } + + // Reflection on method + + @Test + void methodIntrospectionMatchesMethodHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), methodHint -> { + })); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + + @Test + void methodIntrospectionMatchesIntrospectPublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + + @Test + void methodIntrospectionMatchesInvokePublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + + @Test + void methodIntrospectionMatchesIntrospectDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + + @Test + void methodIntrospectionMatchesInvokeDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + + @Test + void methodInvocationDoesNotMatchMethodHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), methodHint -> { + })); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void methodInvocationMatchesMethodInvocationHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("publicMethod", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void methodInvocationDoesNotMatchIntrospectPublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void methodInvocationMatchesInvokePublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void methodInvocationDoesNotMatchIntrospectDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void methodInvocationMatchesInvokeDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").invoke()); + } + + @Test + void privateMethodIntrospectionMatchesMethodHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("privateMethod", Collections.emptyList(), methodHint -> { + })); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + } + + @Test + void privateMethodIntrospectionDoesNotMatchIntrospectPublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + } + + @Test + void privateMethodIntrospectionDoesNotMatchInvokePublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + } + + @Test + void privateMethodIntrospectionMatchesIntrospectDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + } + + @Test + void privateMethodIntrospectionMatchesInvokeDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + } + + @Test + void privateMethodInvocationDoesNotMatchMethodHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("privateMethod", Collections.emptyList(), methodHint -> { + })); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + @Test + void privateMethodInvocationMatchesMethodInvocationHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMethod("privateMethod", Collections.emptyList(), methodHint -> methodHint.withMode(ExecutableMode.INVOKE))); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + @Test + void privateMethodInvocationDoesNotMatchIntrospectPublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + @Test + void privateMethodInvocationDoesNotMatchInvokePublicMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + @Test + void privateMethodInvocationDoesNotMatchIntrospectDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS)); + assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + @Test + void privateMethodInvocationMatchesInvokeDeclaredMethods() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").invoke()); + } + + // Reflection on field + + @Test + void shouldFailForMissingField() { + assertThatThrownBy(() -> reflection.onField(SampleClass.class, "missingField")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void fieldReflectionMatchesFieldHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField", fieldHint -> { + })); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); + } + + @Test + void fieldWriteReflectionDoesNotMatchFieldHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField", fieldHint -> { + })); + assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "publicField").allowWrite()); + } + + @Test + void fieldUnsafeReflectionDoesNotMatchFieldHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField", fieldHint -> { + })); + assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "publicField").allowUnsafeAccess()); + } + + @Test + void fieldWriteReflectionMatchesFieldHintWithWrite() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> + typeHint.withField("publicField", fieldHint -> fieldHint.allowWrite(true))); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField").allowWrite()); + } + + @Test + void fieldUnsafeReflectionMatchesFieldHintWithUnsafe() { + this.runtimeHints.reflection().registerType(SampleClass.class, + typeHint -> typeHint.withField("publicField", fieldHint -> fieldHint.allowUnsafeAccess(true))); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField").allowUnsafeAccess()); + } + + @Test + void fieldReflectionMatchesPublicFieldsHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); + } + + @Test + void fieldReflectionMatchesDeclaredFieldsHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); + } + + @Test + void privateFieldReflectionMatchesFieldHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("privateField", fieldHint -> { + })); + assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); + } + + @Test + void privateFieldReflectionDoesNotMatchPublicFieldsHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); + assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "privateField")); + } + + @Test + void privateFieldReflectionMatchesDeclaredFieldsHint() { + this.runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); + } + + + private void assertPredicateMatches(Predicate predicate) { + assertThat(predicate).accepts(this.runtimeHints); + } + + private void assertPredicateDoesNotMatch(Predicate predicate) { + assertThat(predicate).rejects(this.runtimeHints); + } + + + static class SampleClass { + + private String privateField; + + public String publicField; + + public SampleClass() { + + } + + private SampleClass(String message) { + + } + + public void publicMethod() { + + } + + private void privateMethod() { + + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsPredicatesTests.java new file mode 100644 index 00000000000..8b38a0fc6d3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsPredicatesTests.java @@ -0,0 +1,88 @@ +/* + * 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectionHintsPredicates}. + * + * @author Brian Clozel + */ +class ResourceHintsPredicatesTests { + + private final ResourceHintsPredicates resources = new ResourceHintsPredicates(); + + private RuntimeHints runtimeHints; + + + @BeforeEach + void setup() { + this.runtimeHints = new RuntimeHints(); + } + + @Test + void resourcePatternMatchesResourceName() { + this.runtimeHints.resources().registerPattern("/test/spring.*"); + assertPredicateMatches(resources.forResource("/test/spring.properties")); + } + + @Test + void resourcePatternDoesNotMatchResourceName() { + this.runtimeHints.resources().registerPattern("/test/spring.*"); + assertPredicateDoesNotMatch(resources.forResource("/test/other.properties")); + } + + @Test + void resourcePatternMatchesTypeAndResourceName() { + this.runtimeHints.resources().registerPattern("/org/springframework/aot/hint/spring.*"); + assertPredicateMatches(resources.forResource(TypeReference.of(getClass()), "spring.properties")); + } + + @Test + void resourcePatternDoesNotMatchTypeAndResourceName() { + this.runtimeHints.resources().registerPattern("/spring.*"); + assertPredicateDoesNotMatch(resources.forResource(TypeReference.of(getClass()), "spring.properties")); + } + + @Test + void resourceBundleMatchesBundleName() { + this.runtimeHints.resources().registerResourceBundle("spring"); + assertPredicateMatches(resources.forBundle("spring")); + } + + @Test + void resourceBundleDoesNotMatchBundleName() { + this.runtimeHints.resources().registerResourceBundle("spring"); + assertPredicateDoesNotMatch(resources.forBundle("other")); + } + + + private void assertPredicateMatches(Predicate predicate) { + assertThat(predicate.test(this.runtimeHints)).isTrue(); + } + + private void assertPredicateDoesNotMatch(Predicate predicate) { + assertThat(predicate.test(this.runtimeHints)).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/CoreAnnotationsRuntimeHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/CoreAnnotationsRuntimeHintsRegistrarTests.java index cad643cccb4..d21f25fe8c2 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/CoreAnnotationsRuntimeHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/CoreAnnotationsRuntimeHintsRegistrarTests.java @@ -21,8 +21,8 @@ import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.aot.hint.TypeReference; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.Order; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -49,16 +49,14 @@ class CoreAnnotationsRuntimeHintsRegistrarTests { @Test void aliasForHasHints() { - assertThat(this.hints.reflection().getTypeHint(TypeReference.of(AliasFor.class))) - .satisfies(hint -> assertThat(hint.getMemberCategories()) - .containsExactly(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThat(RuntimeHintsPredicates.reflection().onType(AliasFor.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.hints); } @Test void orderAnnotationHasHints() { - assertThat(this.hints.reflection().getTypeHint(TypeReference.of(Order.class))) - .satisfies(hint -> assertThat(hint.getMemberCategories()) - .containsExactly(MemberCategory.INVOKE_DECLARED_METHODS)); + assertThat(RuntimeHintsPredicates.reflection().onType(Order.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.hints); } } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHintsRegistrarTests.java index bf2150c3730..d6fb1c1eda4 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHintsRegistrarTests.java @@ -20,11 +20,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ResourcePatternHint; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.aot.hint.TypeHint; -import org.springframework.aot.hint.TypeReference; import org.springframework.core.io.support.DummyFactory; import org.springframework.core.io.support.MyDummyFactory1; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -51,28 +49,19 @@ class SpringFactoriesLoaderRuntimeHintsRegistrarTests { @Test void resourceLocationHasHints() { - assertThat(this.hints.resources().resourcePatterns()) - .anySatisfy(hint -> assertThat(hint.getIncludes()).map(ResourcePatternHint::getPattern) - .contains(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION)); + assertThat(RuntimeHintsPredicates.resource().forResource(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION)).accepts(this.hints); } @Test void factoryTypeHasHint() { - TypeReference type = TypeReference.of(DummyFactory.class); - assertThat(this.hints.reflection().getTypeHint(type)) - .satisfies(this::expectedHints); + assertThat(RuntimeHintsPredicates.reflection().onType(DummyFactory.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.hints); } @Test void factoryImplementationHasHint() { - TypeReference type = TypeReference.of(MyDummyFactory1.class); - assertThat(this.hints.reflection().getTypeHint(type)) - .satisfies(this::expectedHints); - } - - private void expectedHints(TypeHint hint) { - assertThat(hint.getMemberCategories()) - .containsExactly(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + assertThat(RuntimeHintsPredicates.reflection().onType(MyDummyFactory1.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.hints); } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java index 9737a01b5cf..e6b0c36ab22 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java @@ -25,8 +25,8 @@ import javax.lang.model.element.Modifier; import org.junit.jupiter.api.Test; -import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsPredicates; import org.springframework.aot.test.generator.compile.Compiled; import org.springframework.aot.test.generator.compile.TestCompiler; import org.springframework.beans.testfixture.beans.TestBean; @@ -87,11 +87,8 @@ class InjectionCodeGeneratorTests { TestBean bean = new TestBean(); Field field = ReflectionUtils.findField(bean.getClass(), "age"); this.generator.generateInjectionCode(field, INSTANCE_VARIABLE, CodeBlock.of("$L", 123)); - assertThat(this.hints.reflection().getTypeHint(TestBean.class)) - .satisfies(hint -> assertThat(hint.fields()).anySatisfy(fieldHint -> { - assertThat(fieldHint.getName()).isEqualTo("age"); - assertThat(fieldHint.isAllowWrite()).isTrue(); - })); + assertThat(RuntimeHintsPredicates.reflection().onField(TestBean.class, "age").allowWrite()) + .accepts(this.hints); } @Test @@ -127,11 +124,8 @@ class InjectionCodeGeneratorTests { TestBeanWithPrivateMethod bean = new TestBeanWithPrivateMethod(); Method method = ReflectionUtils.findMethod(bean.getClass(), "setAge", int.class); this.generator.generateInjectionCode(method, INSTANCE_VARIABLE, CodeBlock.of("$L", 123)); - assertThat(this.hints.reflection().getTypeHint(TestBeanWithPrivateMethod.class)) - .satisfies(hint -> assertThat(hint.methods()).anySatisfy(methodHint -> { - assertThat(methodHint.getName()).isEqualTo("setAge"); - assertThat(methodHint.getModes()).contains(ExecutableMode.INVOKE); - })); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(TestBeanWithPrivateMethod.class, "setAge").invoke()).accepts(this.hints); } @SuppressWarnings("unchecked")