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