Skip to content

Commit 757d482

Browse files
committed
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
1 parent fb636ad commit 757d482

File tree

5 files changed

+683
-0
lines changed

5 files changed

+683
-0
lines changed

spring-graphql/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies {
3535
testImplementation 'org.assertj:assertj-core'
3636
testImplementation 'org.mockito:mockito-core'
3737
testImplementation 'io.projectreactor:reactor-test'
38+
testImplementation 'org.springframework:spring-core-test'
3839
testImplementation 'org.springframework:spring-messaging'
3940
testImplementation 'org.springframework:spring-test'
4041
testImplementation 'org.springframework:spring-webflux'

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ public void setApplicationContext(ApplicationContext applicationContext) {
165165
this.applicationContext = applicationContext;
166166
}
167167

168+
@Nullable
169+
HandlerMethodArgumentResolverComposite getArgumentResolvers() {
170+
return this.argumentResolvers;
171+
}
168172

169173
@Override
170174
public void afterPropertiesSet() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.lang.reflect.Method;
21+
import java.lang.reflect.Parameter;
22+
import java.lang.reflect.Type;
23+
import java.util.Arrays;
24+
25+
import org.springframework.aop.SpringProxy;
26+
import org.springframework.aot.generate.GenerationContext;
27+
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
28+
import org.springframework.aot.hint.ExecutableMode;
29+
import org.springframework.aot.hint.MemberCategory;
30+
import org.springframework.aot.hint.RuntimeHints;
31+
import org.springframework.aot.hint.TypeReference;
32+
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
33+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
34+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
35+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
36+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
37+
import org.springframework.beans.factory.support.RegisteredBean;
38+
import org.springframework.context.support.StaticApplicationContext;
39+
import org.springframework.core.DecoratingProxy;
40+
import org.springframework.core.MethodParameter;
41+
import org.springframework.core.annotation.MergedAnnotations;
42+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
43+
import org.springframework.data.projection.TargetAware;
44+
import org.springframework.graphql.data.ArgumentValue;
45+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
46+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
47+
import org.springframework.graphql.data.method.annotation.BatchMapping;
48+
import org.springframework.graphql.data.method.annotation.SchemaMapping;
49+
import org.springframework.stereotype.Controller;
50+
import org.springframework.util.Assert;
51+
import org.springframework.util.ClassUtils;
52+
import org.springframework.util.ReflectionUtils;
53+
54+
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;
55+
56+
/**
57+
* {@link BeanFactoryInitializationAotProcessor} implementation for registering
58+
* runtime hints discoverable through GraphQL controllers, such as:
59+
* <ul>
60+
* <li>invocation reflection on {@code @SchemaMapping} and {@code @BatchMapping} annotated controllers methods
61+
* <li>binding reflection on controller method arguments, needed for binding or by the GraphQL Java engine itself
62+
* <li>reflection for SpEL support and JDK proxy creation for {@code @ProjectedPayload} projections,
63+
* if Spring Data Commons is present on the classpath.
64+
* </ul>
65+
* <p>This processor is using a {@link HandlerMethodArgumentResolver} resolution mechanism similar
66+
* to the one used in {@link AnnotatedControllerConfigurer}. The type of runtime hints registered
67+
* for each method argument depends on the {@link HandlerMethodArgumentResolver} resolved.
68+
* <p>Manual registration of {@link graphql.schema.DataFetcher} cannot be detected by this
69+
* processor; developers will need to declare bound types with {@link RegisterReflectionForBinding}
70+
* annotations on their configuration class.
71+
*
72+
* @author Brian Clozel
73+
* @see org.springframework.graphql.data.method.HandlerMethodArgumentResolver
74+
* @since 1.1.0
75+
*/
76+
class SchemaMappingBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
77+
78+
private final static boolean springDataPresent = ClassUtils.isPresent(
79+
"org.springframework.data.projection.SpelAwareProxyProjectionFactory",
80+
SchemaMappingBeanFactoryInitializationAotProcessor.class.getClassLoader());
81+
82+
83+
@Override
84+
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
85+
Class<?>[] controllerTypes = Arrays.stream(beanFactory.getBeanDefinitionNames())
86+
.map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
87+
.filter(this::isController)
88+
.toArray(Class<?>[]::new);
89+
return new SchemaMappingBeanFactoryInitializationAotContribution(controllerTypes);
90+
}
91+
92+
private boolean isController(AnnotatedElement element) {
93+
return MergedAnnotations.from(element, TYPE_HIERARCHY).isPresent(Controller.class);
94+
}
95+
96+
private static class SchemaMappingBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution {
97+
98+
private final Class<?>[] controllers;
99+
100+
private final HandlerMethodArgumentResolverComposite argumentResolvers;
101+
102+
public SchemaMappingBeanFactoryInitializationAotContribution(Class<?>[] controllers) {
103+
this.controllers = controllers;
104+
this.argumentResolvers = createArgumentResolvers();
105+
}
106+
107+
private HandlerMethodArgumentResolverComposite createArgumentResolvers() {
108+
AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer();
109+
controllerConfigurer.setApplicationContext(new StaticApplicationContext());
110+
controllerConfigurer.afterPropertiesSet();
111+
HandlerMethodArgumentResolverComposite argumentResolverComposite = controllerConfigurer.getArgumentResolvers();
112+
Assert.notNull(argumentResolverComposite, "argument resolvers should not be null");
113+
return argumentResolverComposite;
114+
}
115+
116+
@Override
117+
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
118+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
119+
registerSpringDataSpelSupport(runtimeHints);
120+
Arrays.stream(this.controllers).forEach(controller -> {
121+
runtimeHints.reflection().registerType(controller);
122+
ReflectionUtils.doWithMethods(controller, method -> processSchemaMappingMethod(runtimeHints, method), this::isGraphQlHandlerMethod);
123+
});
124+
}
125+
126+
private void registerSpringDataSpelSupport(RuntimeHints runtimeHints) {
127+
if (springDataPresent) {
128+
runtimeHints.reflection()
129+
.registerType(SpelAwareProxyProjectionFactory.class)
130+
.registerType(TypeReference.of("org.springframework.data.projection.SpelEvaluatingMethodInterceptor$TargetWrapper"),
131+
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
132+
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS));
133+
}
134+
}
135+
136+
private boolean isGraphQlHandlerMethod(AnnotatedElement element) {
137+
MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, TYPE_HIERARCHY);
138+
return mergedAnnotations.isPresent(SchemaMapping.class)
139+
|| mergedAnnotations.isPresent(BatchMapping.class);
140+
}
141+
142+
private void processSchemaMappingMethod(RuntimeHints runtimeHints, Method method) {
143+
runtimeHints.reflection().registerMethod(method, ExecutableMode.INVOKE);
144+
for (Parameter parameter : method.getParameters()) {
145+
processMethodParameter(runtimeHints, MethodParameter.forParameter(parameter));
146+
}
147+
processReturnType(runtimeHints, MethodParameter.forExecutable(method, -1));
148+
}
149+
150+
private void processMethodParameter(RuntimeHints runtimeHints, MethodParameter methodParameter) {
151+
MethodParameterRuntimeHintsRegistrar.fromMethodParameter(this.argumentResolvers, methodParameter)
152+
.apply(runtimeHints);
153+
}
154+
155+
private void processReturnType(RuntimeHints runtimeHints, MethodParameter methodParameter) {
156+
new ArgumentBindingHints(methodParameter).apply(runtimeHints);
157+
}
158+
159+
}
160+
161+
@FunctionalInterface
162+
private interface MethodParameterRuntimeHintsRegistrar {
163+
164+
BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
165+
166+
void apply(RuntimeHints runtimeHints);
167+
168+
static MethodParameterRuntimeHintsRegistrar fromMethodParameter(HandlerMethodArgumentResolverComposite argumentResolvers, MethodParameter methodParameter) {
169+
HandlerMethodArgumentResolver argumentResolver = argumentResolvers.getArgumentResolver(methodParameter);
170+
if (argumentResolver instanceof ArgumentMethodArgumentResolver
171+
|| argumentResolver instanceof ArgumentsMethodArgumentResolver) {
172+
return new ArgumentBindingHints(methodParameter);
173+
}
174+
if (argumentResolver instanceof DataLoaderMethodArgumentResolver) {
175+
return new DataLoaderHints(methodParameter);
176+
}
177+
if (springDataPresent) {
178+
if (argumentResolver instanceof ProjectedPayloadMethodArgumentResolver) {
179+
return new ProjectedPayloadHints(methodParameter);
180+
}
181+
}
182+
return new NoHintsRequired();
183+
}
184+
185+
}
186+
187+
private static class NoHintsRequired implements MethodParameterRuntimeHintsRegistrar {
188+
189+
@Override
190+
public void apply(RuntimeHints runtimeHints) {
191+
// no runtime hints are required for this type of argument
192+
}
193+
}
194+
195+
private static class ArgumentBindingHints implements MethodParameterRuntimeHintsRegistrar {
196+
197+
private final MethodParameter methodParameter;
198+
199+
public ArgumentBindingHints(MethodParameter methodParameter) {
200+
this.methodParameter = methodParameter;
201+
}
202+
203+
@Override
204+
public void apply(RuntimeHints runtimeHints) {
205+
Type parameterType = this.methodParameter.getGenericParameterType();
206+
if (ArgumentValue.class.isAssignableFrom(methodParameter.getParameterType())) {
207+
parameterType = this.methodParameter.nested().getNestedGenericParameterType();
208+
}
209+
bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), parameterType);
210+
}
211+
}
212+
213+
private static class DataLoaderHints implements MethodParameterRuntimeHintsRegistrar {
214+
215+
private final MethodParameter methodParameter;
216+
217+
public DataLoaderHints(MethodParameter methodParameter) {
218+
this.methodParameter = methodParameter;
219+
}
220+
221+
@Override
222+
public void apply(RuntimeHints runtimeHints) {
223+
bindingRegistrar.registerReflectionHints(runtimeHints.reflection(),
224+
this.methodParameter.nested().getNestedGenericParameterType());
225+
}
226+
}
227+
228+
private static class ProjectedPayloadHints implements MethodParameterRuntimeHintsRegistrar {
229+
230+
private final MethodParameter methodParameter;
231+
232+
public ProjectedPayloadHints(MethodParameter methodParameter) {
233+
this.methodParameter = methodParameter;
234+
}
235+
236+
@Override
237+
public void apply(RuntimeHints runtimeHints) {
238+
Class<?> parameterType = this.methodParameter.nestedIfOptional().getNestedParameterType();
239+
runtimeHints.reflection().registerType(parameterType);
240+
runtimeHints.proxies().registerJdkProxy(parameterType, TargetAware.class, SpringProxy.class, DecoratingProxy.class);
241+
}
242+
}
243+
244+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.graphql.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor

0 commit comments

Comments
 (0)