Skip to content

Commit a57dead

Browse files
committed
Add support for serialization in RequestMappingReflectiveProcessor
Support reflection-based serialization of parameters annotated with @RequestBody and return values annotated with @responsebody. It leverages a new BindingReflectionHintsRegistrar class that is designed to register transitively the types usually needed for binding and reflection-based serialization on fields, constructors and properties. Generics are taken in account as well. Closes gh-28518
1 parent 12d756a commit a57dead

File tree

4 files changed

+503
-6
lines changed

4 files changed

+503
-6
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2002-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.aot.hint.support;
18+
19+
import java.beans.BeanInfo;
20+
import java.beans.IntrospectionException;
21+
import java.beans.Introspector;
22+
import java.beans.PropertyDescriptor;
23+
import java.lang.reflect.Method;
24+
import java.lang.reflect.Type;
25+
import java.util.HashSet;
26+
import java.util.LinkedHashSet;
27+
import java.util.Set;
28+
import java.util.function.Consumer;
29+
30+
import org.apache.commons.logging.Log;
31+
import org.apache.commons.logging.LogFactory;
32+
33+
import org.springframework.aot.hint.ExecutableHint;
34+
import org.springframework.aot.hint.ExecutableMode;
35+
import org.springframework.aot.hint.MemberCategory;
36+
import org.springframework.aot.hint.ReflectionHints;
37+
import org.springframework.core.MethodParameter;
38+
import org.springframework.core.ResolvableType;
39+
40+
/**
41+
* Register the necessary reflection hints so that the specified type can be bound/serialized at runtime.
42+
* Fields, constructors and property methods are registered, except for a set of types like those in
43+
* {@code java.} package where just the type is registered. Types are discovered transitively and
44+
* generic types are registered as well.
45+
*
46+
* @author Sebastien Deleuze
47+
* @since 6.0
48+
*/
49+
public class BindingReflectionHintsRegistrar {
50+
51+
private static final Log logger = LogFactory.getLog(BindingReflectionHintsRegistrar.class);
52+
53+
private static final Consumer<ExecutableHint.Builder> INVOKE = builder -> builder
54+
.withMode(ExecutableMode.INVOKE);
55+
56+
public void registerReflectionHints(ReflectionHints hints, Type... types) {
57+
for (Type type : types) {
58+
Set<Class<?>> referencedTypes = new LinkedHashSet<>();
59+
collectReferencedTypes(new HashSet<>(), referencedTypes, type);
60+
referencedTypes.forEach(referencedType -> hints.registerType(referencedType, builder -> {
61+
if (shouldRegisterMembers(referencedType)) {
62+
builder.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
63+
try {
64+
BeanInfo beanInfo = Introspector.getBeanInfo(referencedType);
65+
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
66+
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
67+
Method writeMethod = propertyDescriptor.getWriteMethod();
68+
if (writeMethod != null && writeMethod.getDeclaringClass() != Object.class) {
69+
hints.registerMethod(writeMethod, INVOKE);
70+
MethodParameter methodParameter = MethodParameter.forExecutable(writeMethod, 0);
71+
registerReflectionHints(hints, methodParameter.getGenericParameterType());
72+
}
73+
Method readMethod = propertyDescriptor.getReadMethod();
74+
if (readMethod != null && readMethod.getDeclaringClass() != Object.class) {
75+
hints.registerMethod(readMethod, INVOKE);
76+
MethodParameter methodParameter = MethodParameter.forExecutable(readMethod, -1);
77+
registerReflectionHints(hints, methodParameter.getGenericParameterType());
78+
}
79+
}
80+
}
81+
catch (IntrospectionException ex) {
82+
if (logger.isDebugEnabled()) {
83+
logger.debug("Ignoring referenced type [" + referencedType.getName() + "]: " + ex.getMessage());
84+
}
85+
}
86+
}
87+
}));
88+
}
89+
}
90+
91+
/**
92+
* Return whether the members of the type should be registered transitively.
93+
* @param type the type to evaluate
94+
* @return {@code true} if the members of the type should be registered transitively
95+
*/
96+
protected boolean shouldRegisterMembers(Class<?> type) {
97+
return !type.getCanonicalName().startsWith("java.");
98+
}
99+
100+
private void collectReferencedTypes(Set<Type> seen, Set<Class<?>> types, Type type) {
101+
if (seen.contains(type)) {
102+
return;
103+
}
104+
seen.add(type);
105+
ResolvableType resolvableType = ResolvableType.forType(type);
106+
Class<?> clazz = resolvableType.resolve();
107+
if (clazz != null) {
108+
types.add(clazz);
109+
for (ResolvableType genericResolvableType : resolvableType.getGenerics()) {
110+
collectReferencedTypes(seen, types, genericResolvableType.getType());
111+
}
112+
}
113+
}
114+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2002-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.aot.hint.support;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.aot.hint.ExecutableMode;
24+
import org.springframework.aot.hint.MemberCategory;
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.aot.hint.TypeReference;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link BindingReflectionHintsRegistrar}.
32+
*
33+
* @author Sebastien Deleuze
34+
*/
35+
public class BindingReflectionHintsRegistrarTests {
36+
37+
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
38+
private final RuntimeHints hints = new RuntimeHints();
39+
40+
@Test
41+
void registerTypeForSerializationWithEmptyClass() {
42+
bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleEmptyClass.class);
43+
assertThat(this.hints.reflection().typeHints()).singleElement()
44+
.satisfies(typeHint -> {
45+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleEmptyClass.class));
46+
assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder(
47+
MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
48+
assertThat(typeHint.constructors()).isEmpty();
49+
assertThat(typeHint.fields()).isEmpty();
50+
assertThat(typeHint.methods()).isEmpty();
51+
});
52+
}
53+
54+
@Test
55+
void registerTypeForSerializationWithNoProperty() {
56+
bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithNoProperty.class);
57+
assertThat(this.hints.reflection().typeHints()).singleElement()
58+
.satisfies(typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithNoProperty.class)));
59+
}
60+
61+
@Test
62+
void registerTypeForSerializationWithGetter() {
63+
bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithGetter.class);
64+
assertThat(this.hints.reflection().typeHints()).satisfiesExactlyInAnyOrder(
65+
typeHint -> {
66+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class));
67+
assertThat(typeHint.getMemberCategories()).isEmpty();
68+
assertThat(typeHint.constructors()).isEmpty();
69+
assertThat(typeHint.fields()).isEmpty();
70+
assertThat(typeHint.methods()).isEmpty();
71+
},
72+
typeHint -> {
73+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithGetter.class));
74+
assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> {
75+
assertThat(methodHint.getName()).isEqualTo("getName");
76+
assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE);
77+
});
78+
});
79+
}
80+
81+
@Test
82+
void registerTypeForSerializationWithSetter() {
83+
bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithSetter.class);
84+
assertThat(this.hints.reflection().typeHints()).satisfiesExactlyInAnyOrder(
85+
typeHint -> {
86+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class));
87+
assertThat(typeHint.getMemberCategories()).isEmpty();
88+
assertThat(typeHint.constructors()).isEmpty();
89+
assertThat(typeHint.fields()).isEmpty();
90+
assertThat(typeHint.methods()).isEmpty();
91+
},
92+
typeHint -> {
93+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithSetter.class));
94+
assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> {
95+
assertThat(methodHint.getName()).isEqualTo("setName");
96+
assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE);
97+
});
98+
});
99+
}
100+
101+
@Test
102+
void registerTypeForSerializationWithListProperty() {
103+
bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithListProperty.class);
104+
assertThat(this.hints.reflection().typeHints()).satisfiesExactlyInAnyOrder(
105+
typeHint -> {
106+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class));
107+
assertThat(typeHint.getMemberCategories()).isEmpty();
108+
assertThat(typeHint.constructors()).isEmpty();
109+
assertThat(typeHint.fields()).isEmpty();
110+
assertThat(typeHint.methods()).isEmpty();
111+
},
112+
typeHint -> {
113+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(List.class));
114+
assertThat(typeHint.getMemberCategories()).isEmpty();
115+
assertThat(typeHint.constructors()).isEmpty();
116+
assertThat(typeHint.fields()).isEmpty();
117+
assertThat(typeHint.methods()).isEmpty();
118+
},
119+
typeHint -> {
120+
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleClassWithListProperty.class));
121+
assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder(
122+
methodHint -> {
123+
assertThat(methodHint.getName()).isEqualTo("setNames");
124+
assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE);
125+
},
126+
methodHint -> {
127+
assertThat(methodHint.getName()).isEqualTo("getNames");
128+
assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE);
129+
});
130+
});
131+
}
132+
133+
static class SampleEmptyClass {
134+
}
135+
136+
static class SampleClassWithNoProperty {
137+
138+
String name() {
139+
return null;
140+
}
141+
}
142+
143+
static class SampleClassWithGetter {
144+
145+
public String getName() {
146+
return null;
147+
}
148+
149+
public SampleEmptyClass unmanaged() {
150+
return null;
151+
}
152+
}
153+
154+
static class SampleClassWithSetter {
155+
156+
public void setName(String name) {
157+
}
158+
159+
public SampleEmptyClass unmanaged() {
160+
return null;
161+
}
162+
}
163+
164+
static class SampleClassWithListProperty {
165+
166+
public List<String> getNames() {
167+
return null;
168+
}
169+
170+
public void setNames(List<String> names) {
171+
}
172+
}
173+
174+
}

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMappingReflectiveProcessor.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,59 @@
1616

1717
package org.springframework.web.bind.annotation;
1818

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Method;
21+
import java.lang.reflect.Parameter;
2022

23+
import org.springframework.aot.hint.ExecutableMode;
2124
import org.springframework.aot.hint.ReflectionHints;
2225
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
23-
import org.springframework.aot.hint.annotation.SimpleReflectiveProcessor;
26+
import org.springframework.aot.hint.support.BindingReflectionHintsRegistrar;
27+
import org.springframework.core.MethodParameter;
28+
import org.springframework.core.annotation.AnnotatedElementUtils;
2429

2530
/**
2631
* {@link ReflectiveProcessor} implementation for {@link RequestMapping}
2732
* annotated types. On top of registering reflection hints for invoking
28-
* the annotated method, this implementation handles return types that
29-
* are serialized as well as TBD.
33+
* the annotated method, this implementation handles return types annotated
34+
* with {@link ResponseBody} and parameters annotated with {@link RequestBody}
35+
* which are serialized as well.
36+
*
3037
*
3138
* @author Stephane Nicoll
39+
* @author Sebastien Deleuze
3240
* @since 6.0
3341
*/
34-
class RequestMappingReflectiveProcessor extends SimpleReflectiveProcessor {
42+
class RequestMappingReflectiveProcessor implements ReflectiveProcessor {
43+
44+
private BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
3545

3646
@Override
47+
public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
48+
if (element instanceof Class<?> type) {
49+
registerTypeHint(hints, type);
50+
}
51+
else if (element instanceof Method method) {
52+
registerMethodHint(hints, method);
53+
}
54+
}
55+
56+
protected void registerTypeHint(ReflectionHints hints, Class<?> type) {
57+
hints.registerType(type, hint -> {});
58+
}
59+
3760
protected void registerMethodHint(ReflectionHints hints, Method method) {
38-
super.registerMethodHint(hints, method);
39-
// TODO
61+
hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE));
62+
for (Parameter parameter : method.getParameters()) {
63+
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
64+
if (methodParameter.hasParameterAnnotation(RequestBody.class)) {
65+
this.bindingRegistrar.registerReflectionHints(hints, methodParameter.getGenericParameterType());
66+
}
67+
}
68+
MethodParameter returnType = MethodParameter.forExecutable(method, -1);
69+
if (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
70+
returnType.hasMethodAnnotation(ResponseBody.class)) {
71+
this.bindingRegistrar.registerReflectionHints(hints, returnType.getGenericParameterType());
72+
}
4073
}
4174
}

0 commit comments

Comments
 (0)