Skip to content

Commit dcbf615

Browse files
authored
fix: native build fixes cherry picks (CP: 24.9) (#23031)
* fix: register client callable methods for reflection (#23016) (#23028) Registers reflection hints for client callable methods to ensure they can be detected and invoked when running native executables. Fixes #23014 * fix: register layout as beans for native build hints (#23006) Layout classes referenced by Vaadin routes are instantiated programmatically and they might contain annotation managed by Spring like `@PostConstruct`. This change discovers and registers layout classes as beans during AOT processing so that related information are available for native build. Fixes #23005 * chore: make ClientCallableAotProcessor less verbose (#23000)
1 parent 6791a0c commit dcbf615

File tree

4 files changed

+153
-27
lines changed

4 files changed

+153
-27
lines changed

vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/ClientCallableAotProcessor.java

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
3434
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
35+
import org.springframework.aot.hint.ExecutableMode;
3536
import org.springframework.aot.hint.ReflectionHints;
37+
import org.springframework.aot.hint.TypeReference;
3638
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
3739
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
3840
import org.springframework.beans.factory.config.BeanDefinition;
@@ -74,7 +76,7 @@ public class ClientCallableAotProcessor
7476
public BeanFactoryInitializationAotContribution processAheadOfTime(
7577
ConfigurableListableBeanFactory beanFactory) {
7678

77-
Set<Class<?>> usedTypes = new HashSet<>();
79+
InspectionResult inspectionResult = new InspectionResult();
7880
Collection<String> packagesToScan = getPackagesToScan(beanFactory);
7981
LOGGER.info("Scanning packages {} for @ClientCallable methods",
8082
packagesToScan);
@@ -95,32 +97,59 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(
9597
Class<?> clazz = ClassUtils.forName(
9698
bd.getBeanClassName(),
9799
beanFactory.getBeanClassLoader());
98-
processClass(clazz, usedTypes);
100+
processClass(clazz, inspectionResult);
99101
} catch (ClassNotFoundException e) {
100102
LOGGER.warn("Could not load class {}",
101103
bd.getBeanClassName(), e);
102104
}
103105
}
104106
}
105107
}
106-
if (usedTypes.isEmpty()) {
107-
LOGGER.debug(
108-
"No @ClientCallable types to register for reflection found");
108+
109+
if (inspectionResult.callableMethods.isEmpty()) {
110+
LOGGER.debug("No @ClientCallable to register for reflection found");
109111
return null;
110112
}
111-
LOGGER.debug(
112-
"Found @ClientCallable types to register for reflection: {}",
113-
usedTypes);
113+
114+
LOGGER.trace("Found @ClientCallable to register for reflection: {}",
115+
inspectionResult.callableMethods);
116+
if (!inspectionResult.usedTypes.isEmpty()) {
117+
LOGGER.debug(
118+
"Found @ClientCallable types to register for reflection: {}",
119+
inspectionResult.usedTypes);
120+
}
114121
return (generationContext, beanFactoryInitializationCode) -> {
115-
// Recursively register all types that require reflection hints
116-
BindingReflectionHintsRegistrar registrar = new BindingReflectionHintsRegistrar();
117122
ReflectionHints reflectionHints = generationContext
118123
.getRuntimeHints().reflection();
119-
registrar.registerReflectionHints(reflectionHints,
120-
usedTypes.toArray(new Class[0]));
124+
125+
// Recursively register all types that require reflection hints
126+
if (!inspectionResult.usedTypes.isEmpty()) {
127+
BindingReflectionHintsRegistrar registrar = new BindingReflectionHintsRegistrar();
128+
registrar.registerReflectionHints(reflectionHints,
129+
inspectionResult.usedTypes.toArray(new Class[0]));
130+
}
131+
132+
inspectionResult.callableMethods.forEach(callable -> reflectionHints
133+
.registerType(callable.declaringClass(),
134+
b -> b.withMethod(callable.methodName(),
135+
callable.parameterTypes(),
136+
ExecutableMode.INVOKE)));
121137
};
122138
}
123139

140+
private record ClientCallableMethod(Class<?> declaringClass,
141+
String methodName, List<TypeReference> parameterTypes) {
142+
ClientCallableMethod(Method method) {
143+
this(method.getDeclaringClass(), method.getName(),
144+
TypeReference.listOf(method.getParameterTypes()));
145+
}
146+
}
147+
148+
private static class InspectionResult {
149+
Set<Class<?>> usedTypes = new HashSet<>();
150+
Set<ClientCallableMethod> callableMethods = new HashSet<>();
151+
}
152+
124153
// Visible for testing
125154
void configureScanner(ClassPathScanningCandidateComponentProvider scanner) {
126155
scanner.addIncludeFilter(new AssignableTypeFilter(Component.class));
@@ -175,10 +204,10 @@ private static Collection<String> getPackagesToScan(
175204
*
176205
* @param clazz
177206
* the class to process
178-
* @param types
179-
* the set to accumulate discovered types
207+
* @param result
208+
* the object to accumulate discovered callables and types
180209
*/
181-
private void processClass(Class<?> clazz, Set<Class<?>> types) {
210+
private void processClass(Class<?> clazz, InspectionResult result) {
182211
// Flow looks for ClientCallable methods only in classes that extend
183212
// Component
184213
if (!Component.class.isAssignableFrom(clazz)) {
@@ -187,7 +216,7 @@ private void processClass(Class<?> clazz, Set<Class<?>> types) {
187216
while (Component.class != clazz) {
188217
for (Method method : clazz.getDeclaredMethods()) {
189218
if (method.isAnnotationPresent(ClientCallable.class)) {
190-
processMethod(method, types);
219+
processMethod(method, result);
191220
}
192221
}
193222
clazz = clazz.getSuperclass();
@@ -200,19 +229,20 @@ private void processClass(Class<?> clazz, Set<Class<?>> types) {
200229
*
201230
* @param method
202231
* the method to process
203-
* @param types
204-
* the set to accumulate discovered types
232+
* @param result
233+
* the object to accumulate discovered callables and types
205234
*/
206-
private void processMethod(Method method, Set<Class<?>> types) {
207-
LOGGER.info("Processing @ClientCallable method {}", method);
235+
private void processMethod(Method method, InspectionResult result) {
236+
LOGGER.debug("Processing @ClientCallable method {}", method);
237+
result.callableMethods.add(new ClientCallableMethod(method));
208238
// Process return type
209239
Type returnType = method.getGenericReturnType();
210-
processType(returnType, types);
240+
processType(returnType, result.usedTypes);
211241

212242
// Process parameter types
213243
Type[] paramTypes = method.getGenericParameterTypes();
214244
for (Type paramType : paramTypes) {
215-
processType(paramType, types);
245+
processType(paramType, result.usedTypes);
216246
}
217247
}
218248

vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinBeanFactoryInitializationAotProcessor.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import com.vaadin.flow.component.Component;
2626
import com.vaadin.flow.component.ComponentEvent;
27+
import com.vaadin.flow.component.UI;
2728
import com.vaadin.flow.component.page.AppShellConfigurator;
2829
import com.vaadin.flow.router.HasErrorParameter;
2930
import com.vaadin.flow.router.HasUrlParameter;
@@ -164,10 +165,37 @@ private <T extends BeanFactory & BeanDefinitionRegistry> void findAndRegisterRou
164165
registeredClasses.add(c.getName());
165166
logger.debug("Registering a bean for route class {}",
166167
c.getName());
167-
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
168-
.rootBeanDefinition(c).setScope("prototype")
169-
.getBeanDefinition();
168+
AbstractBeanDefinition beanDefinition = createPrototypeBeanDefinition(
169+
c);
170170
beanFactory.registerBeanDefinition(c.getName(), beanDefinition);
171+
172+
// Layouts classes are instantiated programmatically, and they
173+
// might need to be
174+
// managed by Spring (e.g. because of @PostConstruct annotated
175+
// methods)
176+
Set<Class<? extends RouterLayout>> definedLayouts = new HashSet<>();
177+
if (c.isAnnotationPresent(Route.class)) {
178+
definedLayouts.add(c.getAnnotation(Route.class).layout());
179+
} else if (c.isAnnotationPresent(RouteAlias.class)) {
180+
definedLayouts
181+
.add(c.getAnnotation(RouteAlias.class).layout());
182+
} else if (c.isAnnotationPresent(RouteAlias.Container.class)) {
183+
for (RouteAlias alias : c
184+
.getAnnotation(RouteAlias.Container.class)
185+
.value()) {
186+
definedLayouts.add(alias.layout());
187+
}
188+
}
189+
definedLayouts.removeIf(
190+
layout -> registeredClasses.contains(layout.getName())
191+
|| layout == RouterLayout.class
192+
|| UI.class.isAssignableFrom(layout));
193+
for (Class<? extends RouterLayout> layout : definedLayouts) {
194+
beanFactory.registerBeanDefinition(layout.getName(),
195+
createPrototypeBeanDefinition(layout));
196+
registeredClasses.add(layout.getName());
197+
}
198+
171199
}
172200
}
173201

@@ -176,6 +204,12 @@ private <T extends BeanFactory & BeanDefinitionRegistry> void findAndRegisterRou
176204

177205
}
178206

207+
private static AbstractBeanDefinition createPrototypeBeanDefinition(
208+
Class<?> c) {
209+
return BeanDefinitionBuilder.rootBeanDefinition(c).setScope("prototype")
210+
.getBeanDefinition();
211+
}
212+
179213
private static Collection<Class<?>> getRouteTypesFor(
180214
Reflections reflections, String packageName) {
181215
var routeTypes = new HashSet<Class<?>>();

vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected Stream<String> getExcludedPatterns() {
9494
"com\\.vaadin\\.flow\\.spring\\.VaadinConfigurationProperties",
9595
"com\\.vaadin\\.flow\\.spring\\.SpringDevToolsPortHandler",
9696
"com\\.vaadin\\.flow\\.spring\\.springnative\\.AtmosphereHintsRegistrar",
97-
"com\\.vaadin\\.flow\\.spring\\.springnative\\.ClientCallableAotProcessor",
97+
"com\\.vaadin\\.flow\\.spring\\.springnative\\.ClientCallableAotProcessor(\\$.*)?",
9898
"com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor",
9999
"com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor\\$Marker",
100100
"com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinHintsRegistrar",

vaadin-spring/src/test/java/com/vaadin/flow/spring/springnative/ClientCallableAotProcessorTest.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.mockito.Mockito;
2525
import org.springframework.aot.generate.GenerationContext;
2626
import org.springframework.aot.hint.RuntimeHints;
27+
import org.springframework.aot.hint.TypeReference;
2728
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
2829
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
2930
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
@@ -66,6 +67,21 @@ void processAheadOfTime_multipleClientCallables_typesDetected() {
6667
.rejects(hints);
6768
assertThat(RuntimeHintsPredicates.reflection().onType(int.class))
6869
.as("Primitive should not be registered").rejects(hints);
70+
71+
assertThat(RuntimeHintsPredicates.reflection()
72+
.onMethod(TestComponent.class, "getSimpleData")).accepts(hints);
73+
assertThat(RuntimeHintsPredicates.reflection()
74+
.onMethod(TestComponent.class, "processData")).accepts(hints);
75+
assertThat(RuntimeHintsPredicates.reflection()
76+
.onMethod(TestComponent.class, "processDataWithPrimitive"))
77+
.accepts(hints);
78+
assertThat(RuntimeHintsPredicates.reflection()
79+
.onMethod(TestComponent.class, "getNestedList")).accepts(hints);
80+
assertThat(RuntimeHintsPredicates.reflection()
81+
.onMethod(TestComponent.class, "handleVoid")).accepts(hints);
82+
assertThat(RuntimeHintsPredicates.reflection()
83+
.onMethod(TestComponent.class, "handleGenericDefinition"))
84+
.accepts(hints);
6985
}
7086

7187
@Test
@@ -91,6 +107,24 @@ void processAheadOfTime_componentSubClass_typesDetected() {
91107
.rejects(hints);
92108
assertThat(RuntimeHintsPredicates.reflection().onType(int.class))
93109
.as("Primitive should not be registered").rejects(hints);
110+
111+
assertThat(RuntimeHintsPredicates.reflection()
112+
.onMethod(TestComponent.class, "getSimpleData")).accepts(hints);
113+
assertThat(RuntimeHintsPredicates.reflection()
114+
.onMethod(TestComponent.class, "processData")).accepts(hints);
115+
assertThat(RuntimeHintsPredicates.reflection()
116+
.onMethod(TestComponent.class, "processDataWithPrimitive"))
117+
.accepts(hints);
118+
assertThat(RuntimeHintsPredicates.reflection()
119+
.onMethod(TestComponent.class, "getNestedList")).accepts(hints);
120+
assertThat(RuntimeHintsPredicates.reflection()
121+
.onMethod(TestComponent.class, "handleVoid")).accepts(hints);
122+
assertThat(RuntimeHintsPredicates.reflection()
123+
.onMethod(TestComponent.class, "handleGenericDefinition"))
124+
.accepts(hints);
125+
assertThat(RuntimeHintsPredicates.reflection()
126+
.onMethod(ExtendedComponent.class, "getExtendedData"))
127+
.accepts(hints);
94128
}
95129

96130
@Test
@@ -132,6 +166,17 @@ void processAheadOfTime_clientCallableOnInterface_typesDetected() {
132166
.accepts(hints);
133167
}
134168

169+
@Test
170+
void processAheadOfTime_clientCallableOnAbstractSuperClass_typesDetected() {
171+
RuntimeHints hints = processAotForComponents(SubComponent.class);
172+
173+
assertThat(RuntimeHintsPredicates.reflection().onType(SimpleDto.class))
174+
.accepts(hints);
175+
assertThat(RuntimeHintsPredicates.reflection()
176+
.onMethod(AbstractComponent.class, "getExtendedData"))
177+
.accepts(hints);
178+
}
179+
135180
@Test
136181
void processAheadOfTime_returnType_typesDetected() {
137182
RuntimeHints hints = processAotForComponents(
@@ -202,7 +247,11 @@ void processAheadOfTime_primitiveTypes_noTypesRegistered() {
202247
PrimitiveParameterComponent.class);
203248

204249
assertThat(hints.reflection().typeHints())
205-
.as("Should not register types from primitive types").isEmpty();
250+
.as("Should not register types from primitive types")
251+
.filteredOn(hint -> !TypeReference
252+
.of(PrimitiveParameterComponent.class)
253+
.equals(hint.getType()))
254+
.isEmpty();
206255
}
207256

208257
@Test
@@ -224,6 +273,8 @@ void processAheadOfTime_voidReturnType_noTypesRegistered() {
224273

225274
assertThat(hints.reflection().typeHints())
226275
.as("Should not register hints from void return type")
276+
.filteredOn(hint -> !TypeReference.of(VoidMethodComponent.class)
277+
.equals(hint.getType()))
227278
.isEmpty();
228279
}
229280

@@ -444,6 +495,17 @@ public List<NestedDto> getExtendedData() {
444495
}
445496
}
446497

498+
// Extended component for multi-level inheritance testing
499+
public static abstract class AbstractComponent extends Component {
500+
@ClientCallable
501+
private List<SimpleDto> getExtendedData() {
502+
return null;
503+
}
504+
}
505+
506+
public static class SubComponent extends AbstractComponent {
507+
}
508+
447509
// Test DTOs
448510
public static class SimpleDto {
449511
private String name;

0 commit comments

Comments
 (0)