diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java index 85ff1e636c3..e1c45ae890c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java @@ -17,6 +17,9 @@ package org.springframework.boot.jackson; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; 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.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.BeansException; 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.InitializingBean; 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.core.ResolvableType; 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) || JsonDeserializer.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, List>> 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, List>> innerComponents; + + public JsonComponentAotContribution(Map, List>> 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)); + }); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index 013a7d298a3..36b6ad971e3 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -14,7 +14,8 @@ org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServ org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints 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.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java index 2d7b5757104..8822989cc4d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java @@ -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"); * 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.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 static org.assertj.core.api.Assertions.assertThat; @@ -112,6 +122,25 @@ class JsonComponentModuleTests { 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) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(configs); @@ -181,7 +210,7 @@ class JsonComponentModuleTests { @JsonComponent 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)