Skip to content

Commit 303e03a

Browse files
committed
#1866, #1867 - More AOT hints from controller methods and RepresentationModelAssemblers.
We now inspect controller method return types and the generics of RepresentationModelAssembler for EntityModel and CollectionModel types and unwrap the type they box to register that for constructor and method invocation reflection.
1 parent eca8b0c commit 303e03a

File tree

4 files changed

+188
-5
lines changed

4 files changed

+188
-5
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 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+
package org.springframework.hateoas.aot;
17+
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
import org.springframework.aot.hint.MemberCategory;
26+
import org.springframework.aot.hint.ReflectionHints;
27+
import org.springframework.core.ResolvableType;
28+
import org.springframework.hateoas.CollectionModel;
29+
import org.springframework.hateoas.EntityModel;
30+
import org.springframework.http.HttpEntity;
31+
32+
/**
33+
* Some helper classes to register types for reflection.
34+
*
35+
* @author Oliver Drotbohm
36+
* @since 2.0
37+
*/
38+
class AotUtils {
39+
40+
private static final Logger LOGGER = LoggerFactory.getLogger(AotUtils.class);
41+
private static final List<Class<?>> MODEL_TYPES = List.of(EntityModel.class, CollectionModel.class);
42+
private static final Set<Class<?>> SEEN_TYPES = new HashSet<>();
43+
44+
/**
45+
* Registers domain types held in {@link EntityModel} and {@link CollectionModel}s for reflection.
46+
*
47+
* @param type must not be {@literal null}.
48+
* @param reflection must not be {@literal null}.
49+
* @param context must not be {@literal null}.
50+
*/
51+
public static void registerModelDomainTypesForReflection(ResolvableType type, ReflectionHints reflection,
52+
Class<?> context) {
53+
54+
if (HttpEntity.class.isAssignableFrom(type.resolve(Object.class))) {
55+
registerModelDomainTypesForReflection(type.as(HttpEntity.class).getGeneric(0), reflection, context);
56+
}
57+
58+
MODEL_TYPES.stream()
59+
.flatMap(it -> extractGenerics(it, type).stream())
60+
.forEach(it -> registerTypeForReflection(it, reflection, context));
61+
}
62+
63+
/**
64+
* Registers the given type for constructor and method invocation reflection.
65+
*
66+
* @param type must not be {@literal null}.
67+
* @param reflection must not be {@literal null}.
68+
* @param context must not be {@literal null}.
69+
*/
70+
public static void registerTypeForReflection(Class<?> type, ReflectionHints reflection, Class<?> context) {
71+
72+
if (SEEN_TYPES.contains(type)) {
73+
return;
74+
}
75+
76+
LOGGER.info("Registering {} for reflection (for {})", type.getName(), context.getName());
77+
78+
reflection.registerType(type,
79+
MemberCategory.INVOKE_DECLARED_METHODS,
80+
MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS);
81+
82+
SEEN_TYPES.add(type);
83+
}
84+
85+
/**
86+
* Extracts the generics from the given model type if the given {@link ResolvableType} is assignable.
87+
*
88+
* @param modelType must not be {@literal null}.
89+
* @param type must not be {@literal null}.
90+
* @return will never be {@literal null}.
91+
*/
92+
private static Optional<Class<?>> extractGenerics(Class<?> modelType, ResolvableType type) {
93+
94+
if (!modelType.isAssignableFrom(type.resolve(Object.class))) {
95+
return Optional.empty();
96+
}
97+
98+
var unresolved = type.as(modelType).getGeneric(0);
99+
var resolved = unresolved.resolve();
100+
101+
if (resolved == null) {
102+
return Optional.empty();
103+
}
104+
105+
var nested = MODEL_TYPES.stream()
106+
.filter(it -> it.isAssignableFrom(resolved))
107+
.toList();
108+
109+
// No nested matches -> return original
110+
if (nested.isEmpty()) {
111+
return Optional.of(resolved);
112+
}
113+
114+
return nested.stream()
115+
.flatMap(it -> extractGenerics(it, unresolved).stream())
116+
.findFirst();
117+
}
118+
}

src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.hateoas.aot;
1717

18+
import static org.springframework.hateoas.aot.AotUtils.*;
19+
1820
import java.lang.annotation.Annotation;
1921
import java.lang.reflect.Constructor;
2022
import java.lang.reflect.Modifier;
@@ -32,6 +34,7 @@
3234
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
3335
import org.springframework.beans.factory.aot.BeanRegistrationCode;
3436
import org.springframework.beans.factory.support.RegisteredBean;
37+
import org.springframework.core.ResolvableType;
3538
import org.springframework.core.annotation.AnnotatedElementUtils;
3639
import org.springframework.hateoas.server.core.DummyInvocationUtils;
3740
import org.springframework.hateoas.server.core.LastInvocationAware;
@@ -54,8 +57,7 @@ public class ControllerMethodReturnTypeAotProcessor implements BeanRegistrationA
5457
private final Class<? extends Annotation> controllerAnnotationType;
5558

5659
/**
57-
* Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes annotated with
58-
* {@link Controller}.
60+
* Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes annotated with {@link Controller}.
5961
*/
6062
public ControllerMethodReturnTypeAotProcessor() {
6163
this(Controller.class);
@@ -134,8 +136,13 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be
134136
return;
135137
}
136138

139+
var runtimeHints = generationContext.getRuntimeHints();
140+
var methodReturnType = ResolvableType.forMethodReturnType(method);
141+
142+
registerModelDomainTypesForReflection(methodReturnType, runtimeHints.reflection(), beanClass);
143+
137144
if (returnType.isInterface()) {
138-
generationContext.getRuntimeHints().proxies().registerJdkProxy(returnType);
145+
runtimeHints.proxies().registerJdkProxy(returnType);
139146
return;
140147
}
141148

@@ -198,5 +205,4 @@ private Class<?> createProxyClass(Class<?> type, Class<?> beanClass) {
198205
return null;
199206
}
200207
}
201-
202208
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 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+
package org.springframework.hateoas.aot;
17+
18+
import static org.springframework.hateoas.aot.AotUtils.*;
19+
20+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
21+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
22+
import org.springframework.beans.factory.support.RegisteredBean;
23+
import org.springframework.hateoas.CollectionModel;
24+
import org.springframework.hateoas.EntityModel;
25+
import org.springframework.hateoas.server.RepresentationModelAssembler;
26+
27+
/**
28+
* A {@link BeanRegistrationAotProcessor} that inspects {@link RepresentationModelAssembler}'s generics for domain types
29+
* wrapped in {@link EntityModel} and {@link CollectionModel}.
30+
*
31+
* @author Oliver Drotbohm
32+
* @since 2.0
33+
*/
34+
class RepresentationModelAssemblerAotProcessor implements BeanRegistrationAotProcessor {
35+
36+
/*
37+
* (non-Javadoc)
38+
* @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean)
39+
*/
40+
@Override
41+
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
42+
43+
var beanClass = registeredBean.getBeanClass();
44+
45+
if (!RepresentationModelAssembler.class.isAssignableFrom(beanClass)) {
46+
return null;
47+
}
48+
49+
var modelType = registeredBean.getBeanType().as(RepresentationModelAssembler.class).getGeneric(1);
50+
51+
return (context, code) -> {
52+
53+
var reflection = context.getRuntimeHints().reflection();
54+
55+
registerModelDomainTypesForReflection(modelType, reflection, beanClass);
56+
};
57+
}
58+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
22
org.springframework.hateoas.aot.ControllerMethodReturnTypeAotProcessor,\
3-
org.springframework.hateoas.aot.HypermediaTypeAotProcessor
3+
org.springframework.hateoas.aot.HypermediaTypeAotProcessor,\
4+
org.springframework.hateoas.aot.RepresentationModelAssemblerAotProcessor
45

56
org.springframework.aot.hint.RuntimeHintsRegistrar=\
67
org.springframework.hateoas.aot.RepresentationModelRuntimeHints

0 commit comments

Comments
 (0)