From b0b2818f0b03bc972fa91f91c1b8290987881811 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2022 16:05:30 +0100 Subject: [PATCH] Improve type discovery in Logback AOT contribution Logback can infer the Java class to which an XML tag should be mapped by looking for a setter method on the class to which the parent tag was mapped. This commits ensures that reflection hints are added for such classes. Fixes gh-32839 --- .../logback/SpringBootJoranConfigurator.java | 87 +++++++++++++++---- ...backConfigurationAotContributionTests.java | 54 ++++++++++++ 2 files changed, 124 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index bcf1abd86ef8..eca2312f0e0d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Stream; import ch.qos.logback.classic.joran.JoranConfigurator; @@ -38,6 +39,7 @@ import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.joran.spi.ElementSelector; import ch.qos.logback.core.joran.spi.RuleStore; +import ch.qos.logback.core.joran.util.PropertySetter; import ch.qos.logback.core.joran.util.beans.BeanDescription; import ch.qos.logback.core.model.ComponentModel; import ch.qos.logback.core.model.Model; @@ -46,6 +48,7 @@ import ch.qos.logback.core.model.processor.ModelInterpretationContext; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.spi.ContextAwareBase; +import ch.qos.logback.core.util.AggregationType; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; @@ -62,6 +65,7 @@ import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.SingletonSupplier; /** * Extended version of the Logback {@link JoranConfigurator} that adds additional Spring @@ -201,32 +205,64 @@ private Set> serializationTypes(Model model) { } private Set reflectionTypes(Model model) { + return reflectionTypes(model, () -> null); + } + + private Set reflectionTypes(Model model, Supplier parent) { Set reflectionTypes = new HashSet<>(); - if (model instanceof ComponentModel) { - String className = ((ComponentModel) model).getClassName(); - processComponent(className, reflectionTypes); - } - String tag = model.getTag(); - if (tag != null) { - String componentType = this.modelInterpretationContext.getDefaultNestedComponentRegistry() - .findDefaultComponentTypeByTag(tag); + Class componentType = determineType(model, parent); + if (componentType != null) { processComponent(componentType, reflectionTypes); } + Supplier componentSupplier = SingletonSupplier.ofNullable(() -> instantiate(componentType)); for (Model submodel : model.getSubModels()) { - reflectionTypes.addAll(reflectionTypes(submodel)); + reflectionTypes.addAll(reflectionTypes(submodel, componentSupplier)); } return reflectionTypes; } - private void processComponent(String componentTypeName, Set reflectionTypes) { - if (componentTypeName != null) { - componentTypeName = this.modelInterpretationContext.getImport(componentTypeName); - BeanDescription beanDescription = this.modelInterpretationContext.getBeanDescriptionCache() - .getBeanDescription(loadComponentType(componentTypeName)); - reflectionTypes.addAll(parameterTypesNames(beanDescription.getPropertyNameToAdder().values())); - reflectionTypes.addAll(parameterTypesNames(beanDescription.getPropertyNameToSetter().values())); - reflectionTypes.add(componentTypeName); + private Class determineType(Model model, Supplier parentSupplier) { + String className = null; + if (model instanceof ComponentModel) { + className = ((ComponentModel) model).getClassName(); + } + if (className == null) { + String tag = model.getTag(); + if (tag != null) { + className = this.modelInterpretationContext.getDefaultNestedComponentRegistry() + .findDefaultComponentTypeByTag(tag); + if (className == null) { + Class type = inferTypeFromParent(parentSupplier, tag); + if (type != null) { + return type; + } + } + } + } + if (className != null) { + className = this.modelInterpretationContext.getImport(className); + return loadComponentType(className); + } + return null; + } + + private Class inferTypeFromParent(Supplier parentSupplier, String tag) { + Object parent = parentSupplier.get(); + if (parent != null) { + try { + Class typeFromSetter = new PropertySetter( + this.modelInterpretationContext.getBeanDescriptionCache(), parent) + .getClassNameViaImplicitRules(tag, AggregationType.AS_COMPLEX_PROPERTY, + this.modelInterpretationContext.getDefaultNestedComponentRegistry()); + if (typeFromSetter != null) { + return typeFromSetter; + } + } + catch (Exception ex) { + // Continue + } } + return null; } private Class loadComponentType(String componentType) { @@ -238,6 +274,23 @@ private Class loadComponentType(String componentType) { } } + private Object instantiate(Class type) { + try { + return type.getConstructor().newInstance(); + } + catch (Exception ex) { + return null; + } + } + + private void processComponent(Class componentType, Set reflectionTypes) { + BeanDescription beanDescription = this.modelInterpretationContext.getBeanDescriptionCache() + .getBeanDescription(componentType); + reflectionTypes.addAll(parameterTypesNames(beanDescription.getPropertyNameToAdder().values())); + reflectionTypes.addAll(parameterTypesNames(beanDescription.getPropertyNameToSetter().values())); + reflectionTypes.add(componentType.getCanonicalName()); + } + private Collection parameterTypesNames(Collection methods) { return methods.stream() .filter((method) -> !method.getDeclaringClass().equals(ContextAware.class) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java index 0ee5ba2a73c3..976aa1e833b9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java @@ -31,6 +31,7 @@ import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.FileAppender; import ch.qos.logback.core.Layout; +import ch.qos.logback.core.joran.spi.DefaultClass; import ch.qos.logback.core.model.ComponentModel; import ch.qos.logback.core.model.ImplicitModel; import ch.qos.logback.core.model.ImportModel; @@ -149,6 +150,34 @@ void componentModelReferencingImportedClassNameIsRegisteredForReflection() { .accepts(generationContext.getRuntimeHints()); } + @Test + void typeFromParentsSetterIsRegisteredForReflection() { + ImplicitModel implementation = new ImplicitModel(); + implementation.setTag("implementation"); + ComponentModel component = new ComponentModel(); + component.setClassName(Outer.class.getName()); + component.getSubModels().add(implementation); + TestGenerationContext generationContext = applyContribution(component); + assertThat(invokePublicConstructorsAndInspectAndInvokePublicMethodsOf(Outer.class)) + .accepts(generationContext.getRuntimeHints()); + assertThat(invokePublicConstructorsAndInspectAndInvokePublicMethodsOf(Implementation.class)) + .accepts(generationContext.getRuntimeHints()); + } + + @Test + void typeFromParentsDefaultClassAnnotatedSetterIsRegisteredForReflection() { + ImplicitModel contract = new ImplicitModel(); + contract.setTag("contract"); + ComponentModel component = new ComponentModel(); + component.setClassName(OuterWithDefaultClass.class.getName()); + component.getSubModels().add(contract); + TestGenerationContext generationContext = applyContribution(component); + assertThat(invokePublicConstructorsAndInspectAndInvokePublicMethodsOf(OuterWithDefaultClass.class)) + .accepts(generationContext.getRuntimeHints()); + assertThat(invokePublicConstructorsAndInspectAndInvokePublicMethodsOf(Implementation.class)) + .accepts(generationContext.getRuntimeHints()); + } + private Predicate invokePublicConstructorsOf(String name) { return RuntimeHintsPredicates.reflection().onType(TypeReference.of(name)) .withMemberCategory(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); @@ -202,4 +231,29 @@ private void withSystemProperty(String name, String value, Runnable action) { } } + public static class Outer { + + public void setImplementation(Implementation implementation) { + + } + + } + + public static class OuterWithDefaultClass { + + @DefaultClass(Implementation.class) + public void setContract(Contract contract) { + + } + + } + + public static class Implementation implements Contract { + + } + + public interface Contract { + + } + }