From 37c2619fc48c29959e1dd6b4b4c87af9aad123b1 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Sep 2023 15:43:30 +0200 Subject: [PATCH] Add AOT support for TypedStringValue This commit adds support for TypeStringValue when generating AOT code. If the value does not specify an explicit type, it's specified as is. Otherwise, the TypeStringValue instance is restored via the appropriate code generation. Closes gh-29074 --- ...nDefinitionPropertyValueCodeGenerator.java | 31 +++++++++- .../factory/config/TypedStringValue.java | 8 ++- .../PostProcessorRegistrationDelegate.java | 21 ++++++- .../ApplicationContextAotGeneratorTests.java | 57 +++++++++++++++++++ .../GenericApplicationContextTests.java | 29 ++++++++++ ...xtAotGeneratorTests-values-expressions.xml | 27 +++++++++ ...nContextAotGeneratorTests-values-types.xml | 13 +++++ ...icationContextAotGeneratorTests-values.xml | 15 +++++ 8 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml create mode 100644 spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml create mode 100644 spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java index 3d624b076a..0da193a4ec 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java @@ -38,6 +38,7 @@ import org.springframework.aot.generate.GeneratedMethods; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.ManagedSet; @@ -88,7 +89,8 @@ class BeanDefinitionPropertyValueCodeGenerator { new ListDelegate(), new SetDelegate(), new MapDelegate(), - new BeanReferenceDelegate() + new BeanReferenceDelegate(), + new TypedStringValueDelegate() )); } @@ -569,4 +571,31 @@ class BeanDefinitionPropertyValueCodeGenerator { } } + /** + * {@link Delegate} for {@link TypedStringValue} types. + */ + private class TypedStringValueDelegate implements Delegate { + + @Override + public CodeBlock generateCode(Object value, ResolvableType type) { + if (value instanceof TypedStringValue typedStringValue) { + return generateTypeStringValueCode(typedStringValue); + } + return null; + } + + private CodeBlock generateTypeStringValueCode(TypedStringValue typedStringValue) { + String value = typedStringValue.getValue(); + if (typedStringValue.hasTargetType()) { + return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value, + generateCode(typedStringValue.getTargetType())); + } + return generateCode(value); + } + + private CodeBlock generateCode(@Nullable Object value) { + return BeanDefinitionPropertyValueCodeGenerator.this.generateCode(value); + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java index 1471f69200..c4d9c5c8e5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java @@ -16,6 +16,8 @@ package org.springframework.beans.factory.config; +import java.util.Comparator; + import org.springframework.beans.BeanMetadataElement; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -35,7 +37,7 @@ import org.springframework.util.ObjectUtils; * @see BeanDefinition#getPropertyValues * @see org.springframework.beans.MutablePropertyValues#addPropertyValue */ -public class TypedStringValue implements BeanMetadataElement { +public class TypedStringValue implements BeanMetadataElement, Comparable { @Nullable private String value; @@ -213,6 +215,10 @@ public class TypedStringValue implements BeanMetadataElement { return this.dynamic; } + @Override + public int compareTo(@Nullable TypedStringValue o) { + return Comparator.comparing(TypedStringValue::getValue).compare(this, o); + } @Override public boolean equals(@Nullable Object other) { diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java index 112e1f5a13..8d4a29c6bc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java +++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -311,8 +312,9 @@ final class PostProcessorRegistrationDelegate { /** * Selectively invoke {@link MergedBeanDefinitionPostProcessor} instances - * registered in the specified bean factory, resolving bean definitions as - * well as any inner bean definitions that they may contain. + * registered in the specified bean factory, resolving bean definitions and + * any attributes if necessary as well as any inner bean definitions that + * they may contain. * @param beanFactory the bean factory to use */ static void invokeMergedBeanDefinitionPostProcessors(DefaultListableBeanFactory beanFactory) { @@ -483,6 +485,9 @@ final class PostProcessorRegistrationDelegate { resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); } + if (value instanceof TypedStringValue typedStringValue) { + resolveTypeStringValue(typedStringValue); + } } for (ValueHolder valueHolder : bd.getConstructorArgumentValues().getIndexedArgumentValues().values()) { Object value = valueHolder.getValue(); @@ -491,6 +496,9 @@ final class PostProcessorRegistrationDelegate { resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) -> postProcessRootBeanDefinition(postProcessors, innerBeanName, innerBeanType, innerBeanDefinition)); } + if (value instanceof TypedStringValue typedStringValue) { + resolveTypeStringValue(typedStringValue); + } } } @@ -503,6 +511,15 @@ final class PostProcessorRegistrationDelegate { }); } + private void resolveTypeStringValue(TypedStringValue typedStringValue) { + try { + typedStringValue.resolveTargetType(this.beanFactory.getBeanClassLoader()); + } + catch (ClassNotFoundException ex) { + // ignore + } + } + private Class resolveBeanType(AbstractBeanDefinition bd) { if (!bd.hasBeanClass()) { try { diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index d02d504ee8..ea8125c319 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -52,6 +52,8 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.Employee; +import org.springframework.beans.testfixture.beans.Pet; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Implementation; import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.One; @@ -62,6 +64,7 @@ import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.context.testfixture.context.annotation.AutowiredComponent; import org.springframework.context.testfixture.context.annotation.AutowiredGenericTemplate; import org.springframework.context.testfixture.context.annotation.CglibConfiguration; @@ -79,6 +82,7 @@ import org.springframework.context.testfixture.context.generator.SimpleComponent import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.Compiled; @@ -441,6 +445,59 @@ class ApplicationContextAotGeneratorTests { } + @Nested + class XmlSupport { + + @Test + void processAheadOfTimeWhenHasTypedStringValue() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-values.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(freshApplicationContext.getBean("pet", Pet.class) + .getName()).isEqualTo("Fido"); + }); + } + + @Test + void processAheadOfTimeWhenHasTypedStringValueWithType() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-values-types.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(compiled.getSourceFile(".*Employee__BeanDefinitions")) + .contains("new TypedStringValue(\"42\", Integer.class"); + }); + } + + @Test + void processAheadOfTimeWhenHasTypedStringValueWithExpression() { + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext + .load(new ClassPathResource("applicationContextAotGeneratorTests-values-expressions.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + Employee employee = freshApplicationContext.getBean(Employee.class); + assertThat(employee.getName()).isEqualTo("John Smith"); + assertThat(employee.getAge()).isEqualTo(42); + assertThat(employee.getCompany()).isEqualTo("Acme Widgets, Inc."); + assertThat(freshApplicationContext.getBean("pet", Pet.class) + .getName()).isEqualTo("Fido"); + }); + } + + } + private Consumer> doesNotHaveProxyFor(Class target) { return hints -> assertThat(hints).noneMatch(hint -> hint.getProxiedInterfaces().get(0).equals(TypeReference.of(target))); diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index dbcbb90cff..731da7c078 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.GenericBeanDefinition; @@ -362,6 +363,34 @@ class GenericApplicationContextTests { context.close(); } + @Test + void refreshForAotLoadsTypedStringValueClassNameInProperty() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition("java.lang.Integer"); + beanDefinition.getPropertyValues().add("value", new TypedStringValue("42", "java.lang.Integer")); + context.registerBeanDefinition("number", beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + assertThat(getBeanDefinition(context, "number").getPropertyValues().get("value")) + .isInstanceOfSatisfying(TypedStringValue.class, typeStringValue -> + assertThat(typeStringValue.getTargetType()).isEqualTo(Integer.class)); + context.close(); + } + + @Test + void refreshForAotLoadsTypedStringValueClassNameInConstructorArgument() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition beanDefinition = new RootBeanDefinition("java.lang.Integer"); + beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + new TypedStringValue("42", "java.lang.Integer")); + context.registerBeanDefinition("number", beanDefinition); + context.refreshForAotProcessing(new RuntimeHints()); + assertThat(getBeanDefinition(context, "number").getConstructorArgumentValues() + .getIndexedArgumentValue(0, TypedStringValue.class).getValue()) + .isInstanceOfSatisfying(TypedStringValue.class, typeStringValue -> + assertThat(typeStringValue.getTargetType()).isEqualTo(Integer.class)); + context.close(); + } + @Test void refreshForAotInvokesBeanFactoryPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml new file mode 100644 index 0000000000..8bdaddb2c0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-expressions.xml @@ -0,0 +1,27 @@ + + + + + + + John Smith + 42 + Acme Widgets, Inc. + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml new file mode 100644 index 0000000000..58422837ff --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values-types.xml @@ -0,0 +1,13 @@ + + + + + + + 42 + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml new file mode 100644 index 0000000000..6adf0ee52f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/applicationContextAotGeneratorTests-values.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + +