From 757d48255a6d132ff9d992edd2d27390e2b6fc7a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 15 Nov 2022 21:17:02 +0100 Subject: [PATCH] Add AOT support for annotated Controllers Prior to this commit, Spring for GraphQL would not support AOT and GraalVM Native. Applications can perform reflection, load resources or require JDK proxies at runtime and we need to contribute `RuntimeHints` during the AOT phase. This commit adds a new `BeanFactoryInitializationAotProcessor` component that introspects GraphQL controllers and registers the relevant reflection hints for binding on argument types and schema types. This also registers JDK proxies for `@ProjectedPayload` support if Spring Data Commons is present in the classpath. Closes gh-495 --- spring-graphql/build.gradle | 1 + .../AnnotatedControllerConfigurer.java | 4 + ...BeanFactoryInitializationAotProcessor.java | 244 ++++++++++ .../resources/META-INF/spring/aot.factories | 1 + ...actoryInitializationAotProcessorTests.java | 433 ++++++++++++++++++ 5 files changed, 683 insertions(+) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java create mode 100644 spring-graphql/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index 1320e0ce..e49ea7b3 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -35,6 +35,7 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.mockito:mockito-core' testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework:spring-core-test' testImplementation 'org.springframework:spring-messaging' testImplementation 'org.springframework:spring-test' testImplementation 'org.springframework:spring-webflux' diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index a86146f4..051db7f4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -165,6 +165,10 @@ public class AnnotatedControllerConfigurer this.applicationContext = applicationContext; } + @Nullable + HandlerMethodArgumentResolverComposite getArgumentResolvers() { + return this.argumentResolvers; + } @Override public void afterPropertiesSet() { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java new file mode 100644 index 00000000..8e18ccba --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java @@ -0,0 +1,244 @@ +/* + * Copyright 2020-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.graphql.data.method.annotation.support; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.Arrays; + +import org.springframework.aop.SpringProxy; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.BindingReflectionHintsRegistrar; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.projection.TargetAware; +import org.springframework.graphql.data.ArgumentValue; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite; +import org.springframework.graphql.data.method.annotation.BatchMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; + +/** + * {@link BeanFactoryInitializationAotProcessor} implementation for registering + * runtime hints discoverable through GraphQL controllers, such as: + * + *

This processor is using a {@link HandlerMethodArgumentResolver} resolution mechanism similar + * to the one used in {@link AnnotatedControllerConfigurer}. The type of runtime hints registered + * for each method argument depends on the {@link HandlerMethodArgumentResolver} resolved. + *

Manual registration of {@link graphql.schema.DataFetcher} cannot be detected by this + * processor; developers will need to declare bound types with {@link RegisterReflectionForBinding} + * annotations on their configuration class. + * + * @author Brian Clozel + * @see org.springframework.graphql.data.method.HandlerMethodArgumentResolver + * @since 1.1.0 + */ +class SchemaMappingBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { + + private final static boolean springDataPresent = ClassUtils.isPresent( + "org.springframework.data.projection.SpelAwareProxyProjectionFactory", + SchemaMappingBeanFactoryInitializationAotProcessor.class.getClassLoader()); + + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + Class[] controllerTypes = Arrays.stream(beanFactory.getBeanDefinitionNames()) + .map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass()) + .filter(this::isController) + .toArray(Class[]::new); + return new SchemaMappingBeanFactoryInitializationAotContribution(controllerTypes); + } + + private boolean isController(AnnotatedElement element) { + return MergedAnnotations.from(element, TYPE_HIERARCHY).isPresent(Controller.class); + } + + private static class SchemaMappingBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { + + private final Class[] controllers; + + private final HandlerMethodArgumentResolverComposite argumentResolvers; + + public SchemaMappingBeanFactoryInitializationAotContribution(Class[] controllers) { + this.controllers = controllers; + this.argumentResolvers = createArgumentResolvers(); + } + + private HandlerMethodArgumentResolverComposite createArgumentResolvers() { + AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer(); + controllerConfigurer.setApplicationContext(new StaticApplicationContext()); + controllerConfigurer.afterPropertiesSet(); + HandlerMethodArgumentResolverComposite argumentResolverComposite = controllerConfigurer.getArgumentResolvers(); + Assert.notNull(argumentResolverComposite, "argument resolvers should not be null"); + return argumentResolverComposite; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + registerSpringDataSpelSupport(runtimeHints); + Arrays.stream(this.controllers).forEach(controller -> { + runtimeHints.reflection().registerType(controller); + ReflectionUtils.doWithMethods(controller, method -> processSchemaMappingMethod(runtimeHints, method), this::isGraphQlHandlerMethod); + }); + } + + private void registerSpringDataSpelSupport(RuntimeHints runtimeHints) { + if (springDataPresent) { + runtimeHints.reflection() + .registerType(SpelAwareProxyProjectionFactory.class) + .registerType(TypeReference.of("org.springframework.data.projection.SpelEvaluatingMethodInterceptor$TargetWrapper"), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)); + } + } + + private boolean isGraphQlHandlerMethod(AnnotatedElement element) { + MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, TYPE_HIERARCHY); + return mergedAnnotations.isPresent(SchemaMapping.class) + || mergedAnnotations.isPresent(BatchMapping.class); + } + + private void processSchemaMappingMethod(RuntimeHints runtimeHints, Method method) { + runtimeHints.reflection().registerMethod(method, ExecutableMode.INVOKE); + for (Parameter parameter : method.getParameters()) { + processMethodParameter(runtimeHints, MethodParameter.forParameter(parameter)); + } + processReturnType(runtimeHints, MethodParameter.forExecutable(method, -1)); + } + + private void processMethodParameter(RuntimeHints runtimeHints, MethodParameter methodParameter) { + MethodParameterRuntimeHintsRegistrar.fromMethodParameter(this.argumentResolvers, methodParameter) + .apply(runtimeHints); + } + + private void processReturnType(RuntimeHints runtimeHints, MethodParameter methodParameter) { + new ArgumentBindingHints(methodParameter).apply(runtimeHints); + } + + } + + @FunctionalInterface + private interface MethodParameterRuntimeHintsRegistrar { + + BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); + + void apply(RuntimeHints runtimeHints); + + static MethodParameterRuntimeHintsRegistrar fromMethodParameter(HandlerMethodArgumentResolverComposite argumentResolvers, MethodParameter methodParameter) { + HandlerMethodArgumentResolver argumentResolver = argumentResolvers.getArgumentResolver(methodParameter); + if (argumentResolver instanceof ArgumentMethodArgumentResolver + || argumentResolver instanceof ArgumentsMethodArgumentResolver) { + return new ArgumentBindingHints(methodParameter); + } + if (argumentResolver instanceof DataLoaderMethodArgumentResolver) { + return new DataLoaderHints(methodParameter); + } + if (springDataPresent) { + if (argumentResolver instanceof ProjectedPayloadMethodArgumentResolver) { + return new ProjectedPayloadHints(methodParameter); + } + } + return new NoHintsRequired(); + } + + } + + private static class NoHintsRequired implements MethodParameterRuntimeHintsRegistrar { + + @Override + public void apply(RuntimeHints runtimeHints) { + // no runtime hints are required for this type of argument + } + } + + private static class ArgumentBindingHints implements MethodParameterRuntimeHintsRegistrar { + + private final MethodParameter methodParameter; + + public ArgumentBindingHints(MethodParameter methodParameter) { + this.methodParameter = methodParameter; + } + + @Override + public void apply(RuntimeHints runtimeHints) { + Type parameterType = this.methodParameter.getGenericParameterType(); + if (ArgumentValue.class.isAssignableFrom(methodParameter.getParameterType())) { + parameterType = this.methodParameter.nested().getNestedGenericParameterType(); + } + bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), parameterType); + } + } + + private static class DataLoaderHints implements MethodParameterRuntimeHintsRegistrar { + + private final MethodParameter methodParameter; + + public DataLoaderHints(MethodParameter methodParameter) { + this.methodParameter = methodParameter; + } + + @Override + public void apply(RuntimeHints runtimeHints) { + bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), + this.methodParameter.nested().getNestedGenericParameterType()); + } + } + + private static class ProjectedPayloadHints implements MethodParameterRuntimeHintsRegistrar { + + private final MethodParameter methodParameter; + + public ProjectedPayloadHints(MethodParameter methodParameter) { + this.methodParameter = methodParameter; + } + + @Override + public void apply(RuntimeHints runtimeHints) { + Class parameterType = this.methodParameter.nestedIfOptional().getNestedParameterType(); + runtimeHints.reflection().registerType(parameterType); + runtimeHints.proxies().registerJdkProxy(parameterType, TargetAware.class, SpringProxy.class, DecoratingProxy.class); + } + } + +} diff --git a/spring-graphql/src/main/resources/META-INF/spring/aot.factories b/spring-graphql/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000..95d83aed --- /dev/null +++ b/spring-graphql/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.graphql.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor \ No newline at end of file diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java new file mode 100644 index 00000000..3ffb582d --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2020-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.graphql.data.method.annotation.support; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingFieldSelectionSet; +import org.dataloader.DataLoader; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.SpringProxy; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.DecoratingProxy; +import org.springframework.data.projection.TargetAware; +import org.springframework.data.web.ProjectedPayload; +import org.springframework.graphql.Author; +import org.springframework.graphql.Book; +import org.springframework.graphql.data.ArgumentValue; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.BatchMapping; +import org.springframework.graphql.data.method.annotation.ContextValue; +import org.springframework.graphql.data.method.annotation.LocalContextValue; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SchemaMappingBeanFactoryInitializationAotProcessor}. + * + * @author Brian Clozel + */ +class SchemaMappingBeanFactoryInitializationAotProcessorTests { + + private GenerationContext generationContext = new TestGenerationContext(); + + private SchemaMappingBeanFactoryInitializationAotProcessor processor = new SchemaMappingBeanFactoryInitializationAotProcessor(); + + @Test + void processorIsRegisteredInAotFactories() { + assertThat(AotServices.factories(getClass().getClassLoader()).load(BeanFactoryInitializationAotProcessor.class)) + .anyMatch(SchemaMappingBeanFactoryInitializationAotProcessor.class::isInstance); + } + + @Nested + class ArgumentTests { + + @Test + void registerBindingReflectionOnReturnType() { + processController(ReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class); + } + + @Test + void registerBindingReflectionOnInput() { + processController(InputController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, BookInput.class); + } + + @Test + void registerBindingReflectionOnArgumentCollection() { + processController(ArgumentCollectionController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class); + } + + @Test + void registerBindingReflectionOnArgumentValue() { + processController(ArgumentValueController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, BookInput.class); + assertThatHintsAreNotRegisteredForTypes(ArgumentValue.class); + } + + @Test + void registerBindingReflectionOnDataLoaderArgument() { + processController(DataLoaderController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Author.class); + assertThatHintsAreNotRegisteredForTypes(DataLoader.class); + } + + @Test + void registerBindingReflectionOnAsyncReturnType() { + processController(AsyncReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Author.class); + } + + + @Controller + static class ReturnTypeController { + @QueryMapping + public Book bookById(@Argument Long id) { + return null; + } + + } + + @Controller + static class InputController { + @MutationMapping + public Book addBook(@Argument BookInput bookInput) { + return null; + } + + } + + @Controller + static class ArgumentCollectionController { + @MutationMapping + public void addBooks(@Argument List books) { + } + + } + + @Controller + static class ArgumentValueController { + @MutationMapping + public Book addBook(ArgumentValue bookInput) { + return null; + } + + } + + @Controller + static class DataLoaderController { + @SchemaMapping + public void authorWithLoader(DataLoader loader) { + } + + } + + @Controller + static class AsyncReturnTypeController { + @SchemaMapping + public CompletableFuture author(Long bookId) { + return null; + } + + } + + static class BookInput { + + String name; + + Long authorId; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAuthorId() { + return this.authorId; + } + + public void setAuthorId(Long authorId) { + this.authorId = authorId; + } + } + + } + + @Nested + class BatchMappingTests { + + @Test + void registerBindingReflectionOnFluxReturnType() { + processController(FluxReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Author.class); + } + + @Test + void registerBindingReflectionOnMonoMapReturnType() { + processController(MonoMapReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, Author.class); + } + + @Test + void registerBindingReflectionOnMapReturnType() { + processController(MapReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, Author.class); + } + + @Test + void registerBindingReflectionOnCollectionReturnType() { + processController(CollectionReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Author.class); + } + + @Test + void registerBindingReflectionOnCallableCollectionReturnType() { + processController(CallableCollectionReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Author.class); + } + + @Test + void registerBindingReflectionOnCallableMapReturnType() { + processController(CallableMapReturnTypeController.class); + assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, Author.class); + } + + @Controller + static class FluxReturnTypeController { + @BatchMapping + public Flux batchMappingFlux() { + return null; + } + } + + @Controller + static class MonoMapReturnTypeController { + @BatchMapping + public Mono> batchMappingMono() { + return null; + } + } + + @Controller + static class MapReturnTypeController { + @BatchMapping + public Map batchMappingMap() { + return null; + } + } + + @Controller + static class CollectionReturnTypeController { + + @BatchMapping + public Collection batchMappingCollection() { + return null; + } + } + + @Controller + static class CallableCollectionReturnTypeController { + @BatchMapping + public Callable> batchMappingCallableCollection() { + return null; + } + } + + @Controller + static class CallableMapReturnTypeController { + @BatchMapping + public Callable> batchMappingCallableMap() { + return null; + } + } + + } + + + @Nested + class ContextTests { + + @Test + void doNotRegisterBindingForContextArguments() { + processController(ContextArgumentsController.class); + assertThatHintsAreNotRegisteredForTypes(GraphQLContext.class, DataFetchingFieldSelectionSet.class, Locale.class); + } + + @Test + void doNotRegisterBindingForAnnotatedContextArguments() { + processController(AnnotatedContextArgumentController.class); + assertThatHintsAreNotRegisteredForTypes(Book.class); + } + + @Controller + static class ContextArgumentsController { + @SchemaMapping + public void dataFetchingEnvironment(GraphQLContext context, DataFetchingFieldSelectionSet selectionSet, Locale locale) { + } + } + + @Controller + class AnnotatedContextArgumentController { + @SchemaMapping + public void contextValue(@ContextValue Book book, @LocalContextValue Book localBook) { + } + } + + } + + @Nested + class ProjectionTests { + + @Test + void registerSpringDataSpelSupport() { + processController(); + TypeReference targetWrapper = TypeReference.of("org.springframework.data.projection.SpelEvaluatingMethodInterceptor$TargetWrapper"); + assertThat(RuntimeHintsPredicates.reflection().onType(targetWrapper) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(generationContext.getRuntimeHints()); + } + + @Test + void registerProxyForProjection() { + processController(ProjectionController.class); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(BookProjection.class, TargetAware.class, + SpringProxy.class, DecoratingProxy.class)).accepts(generationContext.getRuntimeHints()); + } + + + @Test + void registerProxyForOptionalProjection() { + processController(OptionalProjectionController.class); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(BookProjection.class, TargetAware.class, + SpringProxy.class, DecoratingProxy.class)).accepts(generationContext.getRuntimeHints()); + } + + + @Controller + class ProjectionController { + @QueryMapping + public List projection(@Argument(name = "where") BookProjection projection) { + return null; + } + } + + @Controller + class OptionalProjectionController { + @QueryMapping + public List optionalProjection(@Argument(name = "where") Optional projection) { + return null; + } + } + + @ProjectedPayload + interface BookProjection { + String getAuthor(); + } + + } + + + private void processController(Class... controllers) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + for (Class beanClass : controllers) { + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + } + BeanFactoryInitializationAotContribution contribution = this.processor.processAheadOfTime(beanFactory); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, mock(BeanFactoryInitializationCode.class)); + } + + private void assertThatHintsForJavaBeanBindingRegisteredForTypes(Class... types) { + Predicate predicate = Arrays.stream(types) + .map(this::javaBeanBindingOnType) + .reduce(Predicate::and) + .orElseThrow(() -> new IllegalArgumentException("Could not generate predicate for types " + types)); + assertThat(predicate).accepts(this.generationContext.getRuntimeHints()); + } + + private void assertThatHintsAreNotRegisteredForTypes(Class... types) { + Predicate predicate = Arrays.stream(types) + .map(type -> (Predicate) RuntimeHintsPredicates.reflection().onType(type)) + .reduce(Predicate::and) + .orElseThrow(() -> new IllegalArgumentException("Could not generate predicate for types " + types)); + assertThat(predicate).rejects(this.generationContext.getRuntimeHints()); + } + + + private Predicate javaBeanBindingOnType(Class type) { + Predicate predicate = RuntimeHintsPredicates.reflection().onType(type) + .withMemberCategories(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + try { + BeanInfo beanInfo = Introspector.getBeanInfo(type); + PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + Method readMethod = propertyDescriptor.getReadMethod(); + if (readMethod != null && readMethod.getDeclaringClass() != Object.class) { + predicate = predicate.and(RuntimeHintsPredicates.reflection().onMethod(readMethod)); + } + Method writeMethod = propertyDescriptor.getWriteMethod(); + if (writeMethod != null && writeMethod.getDeclaringClass() != Object.class) { + predicate = predicate.and(RuntimeHintsPredicates.reflection().onMethod(writeMethod)); + } + } + } + catch (IntrospectionException e) { + // ignoring type + } + return predicate; + } + +} \ No newline at end of file