Contribute reflection hints for JsonComponent inner classes

Closes gh-33089
This commit is contained in:
Andy Wilkinson 2022-11-09 19:22:26 +00:00
parent b882de7c68
commit 7eb0fe4c89
3 changed files with 88 additions and 4 deletions

View File

@ -17,6 +17,9 @@
package org.springframework.boot.jackson; package org.springframework.boot.jackson;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -26,6 +29,9 @@ import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
@ -33,6 +39,10 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
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.boot.jackson.JsonComponent.Scope; import org.springframework.boot.jackson.JsonComponent.Scope;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
@ -108,7 +118,7 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
} }
} }
private boolean isSuitableInnerClass(Class<?> innerClass) { private static boolean isSuitableInnerClass(Class<?> innerClass) {
return !Modifier.isAbstract(innerClass.getModifiers()) && (JsonSerializer.class.isAssignableFrom(innerClass) return !Modifier.isAbstract(innerClass.getModifiers()) && (JsonSerializer.class.isAssignableFrom(innerClass)
|| JsonDeserializer.class.isAssignableFrom(innerClass) || JsonDeserializer.class.isAssignableFrom(innerClass)
|| KeyDeserializer.class.isAssignableFrom(innerClass)); || KeyDeserializer.class.isAssignableFrom(innerClass));
@ -148,4 +158,44 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
} }
} }
static class JsonComponentBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(
ConfigurableListableBeanFactory beanFactory) {
String[] jsonComponents = beanFactory.getBeanNamesForAnnotation(JsonComponent.class);
Map<Class<?>, List<Class<?>>> innerComponents = new HashMap<>();
for (String jsonComponent : jsonComponents) {
Class<?> type = beanFactory.getType(jsonComponent, true);
for (Class<?> declaredClass : type.getDeclaredClasses()) {
if (isSuitableInnerClass(declaredClass)) {
innerComponents.computeIfAbsent(type, (t) -> new ArrayList<>()).add(declaredClass);
}
}
}
return innerComponents.isEmpty() ? null : new JsonComponentAotContribution(innerComponents);
}
}
private static class JsonComponentAotContribution implements BeanFactoryInitializationAotContribution {
private final Map<Class<?>, List<Class<?>>> innerComponents;
public JsonComponentAotContribution(Map<Class<?>, List<Class<?>>> innerComponents) {
this.innerComponents = innerComponents;
}
@Override
public void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode) {
ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
this.innerComponents.forEach((outer, inners) -> {
reflection.registerType(outer, MemberCategory.DECLARED_CLASSES);
inners.forEach((inner) -> reflection.registerType(inner, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
});
}
}
} }

View File

@ -14,7 +14,8 @@ org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServ
org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\
org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,6 +27,16 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.AbstractSerializer;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.ConcreteSerializer;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.NotSuitable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -112,6 +122,25 @@ class JsonComponentModuleTests {
assertDeserializeForSpecifiedClasses(module); assertDeserializeForSpecifiedClasses(module);
} }
@Test
void aotContributionRegistersReflectionHintsForSuitableInnerClasses() {
load(ComponentWithInnerAbstractClass.class);
ConfigurableListableBeanFactory beanFactory = this.context.getBeanFactory();
BeanFactoryInitializationAotContribution contribution = new JsonComponentBeanFactoryInitializationAotProcessor()
.processAheadOfTime(beanFactory);
TestGenerationContext generationContext = new TestGenerationContext();
contribution.applyTo(generationContext, null);
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection().onType(ComponentWithInnerAbstractClass.class)
.withMemberCategory(MemberCategory.DECLARED_CLASSES)).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onType(ConcreteSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onType(AbstractSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).negate()).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onType(NotSuitable.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).negate()).accepts(runtimeHints);
}
private void load(Class<?>... configs) { private void load(Class<?>... configs) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configs); context.register(configs);
@ -181,7 +210,7 @@ class JsonComponentModuleTests {
@JsonComponent @JsonComponent
static class ComponentWithInnerAbstractClass { static class ComponentWithInnerAbstractClass {
static class AbstractSerializer extends NameAndAgeJsonComponent.Serializer { static abstract class AbstractSerializer extends NameAndAgeJsonComponent.Serializer {
} }
@ -189,6 +218,10 @@ class JsonComponentModuleTests {
} }
static class NotSuitable {
}
} }
@JsonComponent(scope = JsonComponent.Scope.KEYS) @JsonComponent(scope = JsonComponent.Scope.KEYS)