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 @@ + + + + + + + + + + + + + +