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
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -165,6 +165,10 @@ public class AnnotatedControllerConfigurer
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
HandlerMethodArgumentResolverComposite getArgumentResolvers() {
|
||||
return this.argumentResolvers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>invocation reflection on {@code @SchemaMapping} and {@code @BatchMapping} annotated controllers methods
|
||||
* <li>binding reflection on controller method arguments, needed for binding or by the GraphQL Java engine itself
|
||||
* <li>reflection for SpEL support and JDK proxy creation for {@code @ProjectedPayload} projections,
|
||||
* if Spring Data Commons is present on the classpath.
|
||||
* </ul>
|
||||
* <p>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.
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.graphql.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor
|
||||
@@ -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<Book> books) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class ArgumentValueController {
|
||||
@MutationMapping
|
||||
public Book addBook(ArgumentValue<BookInput> bookInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class DataLoaderController {
|
||||
@SchemaMapping
|
||||
public void authorWithLoader(DataLoader<Long, Author> loader) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class AsyncReturnTypeController {
|
||||
@SchemaMapping
|
||||
public CompletableFuture<Author> 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<Author> batchMappingFlux() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MonoMapReturnTypeController {
|
||||
@BatchMapping
|
||||
public Mono<Map<Book, Author>> batchMappingMono() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MapReturnTypeController {
|
||||
@BatchMapping
|
||||
public Map<Book, Author> batchMappingMap() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class CollectionReturnTypeController {
|
||||
|
||||
@BatchMapping
|
||||
public Collection<Author> batchMappingCollection() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class CallableCollectionReturnTypeController {
|
||||
@BatchMapping
|
||||
public Callable<Collection<Author>> batchMappingCallableCollection() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class CallableMapReturnTypeController {
|
||||
@BatchMapping
|
||||
public Callable<Map<Book, Author>> 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<Book> projection(@Argument(name = "where") BookProjection projection) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
class OptionalProjectionController {
|
||||
@QueryMapping
|
||||
public List<Book> optionalProjection(@Argument(name = "where") Optional<BookProjection> 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<RuntimeHints> 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<RuntimeHints> predicate = Arrays.stream(types)
|
||||
.map(type -> (Predicate<RuntimeHints>) 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<RuntimeHints> javaBeanBindingOnType(Class<?> type) {
|
||||
Predicate<RuntimeHints> 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user