From ac16432fade485bb3862dbbe0396b5eb65776b12 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 7 Jun 2022 14:12:12 +0200 Subject: [PATCH] Add AOT support for immutable ConfigurationProperties bean definitions This commit introduces a dedicated AotProcessors for immutable configuration properties beans as their bean definition use an instance supplier that needs special handling. If such a bean definition is detected, dedicated code is generated that replicates the behavior of the instance supplier. Closes gh-31247 --- .../ConfigurationPropertiesBeanRegistrar.java | 14 +-- ...ropertiesBeanRegistrationAotProcessor.java | 95 ++++++++++++++++ .../ConstructorBindingValueSupplier.java | 50 +++++++++ .../resources/META-INF/spring/aot.factories | 5 +- ...tiesBeanRegistrationAotProcessorTests.java | 102 ++++++++++++++++++ 5 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBindingValueSupplier.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java index 51e72231c1..73fe793ff3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java @@ -93,20 +93,10 @@ final class ConfigurationPropertiesBeanRegistrar { RootBeanDefinition definition = new RootBeanDefinition(type); definition.setAttribute(BindMethod.class.getName(), bindMethod); if (bindMethod == BindMethod.VALUE_OBJECT) { - definition.setInstanceSupplier(() -> createValueObject(beanName, type)); + definition.setInstanceSupplier( + () -> ConstructorBindingValueSupplier.createValueObject(this.beanFactory, beanName, type)); } return definition; } - private Object createValueObject(String beanName, Class beanType) { - ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(beanType, beanName); - ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(this.beanFactory); - try { - return binder.bindOrCreate(bean); - } - catch (Exception ex) { - throw new ConfigurationPropertiesBindException(bean, ex); - } - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java new file mode 100644 index 0000000000..32710fd922 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java @@ -0,0 +1,95 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import java.lang.reflect.Executable; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod; +import org.springframework.javapoet.CodeBlock; + +/** + * {@link BeanRegistrationAotProcessor} for immutable configuration properties. + * + * @author Stephane Nicoll + * @see ConstructorBindingValueSupplier + */ +class ConfigurationPropertiesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (!isImmutableConfigurationPropertiesBeanDefinition(registeredBean.getMergedBeanDefinition())) { + return null; + } + return BeanRegistrationAotContribution.ofBeanRegistrationCodeFragmentsCustomizer( + (codeFragments) -> new ConfigurationPropertiesBeanRegistrationCodeFragments(codeFragments, + registeredBean)); + + } + + private boolean isImmutableConfigurationPropertiesBeanDefinition(BeanDefinition beanDefinition) { + return beanDefinition.hasAttribute(BindMethod.class.getName()) + && BindMethod.VALUE_OBJECT.equals(beanDefinition.getAttribute(BindMethod.class.getName())); + } + + private static class ConfigurationPropertiesBeanRegistrationCodeFragments extends BeanRegistrationCodeFragments { + + private static final String REGISTERED_BEAN_PARAMETER_NAME = "registeredBean"; + + private final RegisteredBean registeredBean; + + ConfigurationPropertiesBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, + RegisteredBean registeredBean) { + super(codeFragments); + this.registeredBean = registeredBean; + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, + boolean allowDirectSupplierShortcut) { + GeneratedMethod method = beanRegistrationCode.getMethodGenerator().generateMethod("get", "instance") + .using((builder) -> { + Class beanClass = this.registeredBean.getBeanClass(); + builder.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()); + builder.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + builder.returns(beanClass); + builder.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER_NAME); + builder.addStatement("$T beanFactory = registeredBean.getBeanFactory()", BeanFactory.class); + builder.addStatement("$T beanName = registeredBean.getBeanName()", String.class); + builder.addStatement("$T beanClass = registeredBean.getBeanClass()", Class.class); + builder.addStatement("return ($T) $T.createValueObject(beanFactory, beanName, beanClass)", + beanClass, ConstructorBindingValueSupplier.class); + }); + return CodeBlock.of("$T.of($T::$L)", InstanceSupplier.class, beanRegistrationCode.getClassName(), + method.getName()); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBindingValueSupplier.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBindingValueSupplier.java new file mode 100644 index 0000000000..73a0236e42 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBindingValueSupplier.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import org.springframework.beans.factory.BeanFactory; + +/** + * Helper class to programmatically bind configuration properties that use constructor + * injection. + * + * @author Stephane Nicoll + * @since 6.0 + * @see ConstructorBinding + */ +public abstract class ConstructorBindingValueSupplier { + + /** + * Return an immutable {@link ConfigurationProperties} instance for the specified + * {@code beanType}. + * @param beanFactory the bean factory to use + * @param beanName the name of the bean + * @param beanType the type of the bean + * @return a new instance + */ + public static Object createValueObject(BeanFactory beanFactory, String beanName, Class beanType) { + ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(beanType, beanName); + ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(beanFactory); + try { + return binder.bindOrCreate(bean); + } + catch (Exception ex) { + throw new ConfigurationPropertiesBindException(bean, ex); + } + } + +} 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 173e096ecf..47dee9feeb 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 @@ -2,4 +2,7 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.boot.logging.logback.LogbackRuntimeHintsRegistrar,\ org.springframework.boot.WebApplicationType.WebApplicationTypeRuntimeHintsRegistrar org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ -org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor \ No newline at end of file +org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor + +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java new file mode 100644 index 0000000000..adfa31a5d2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java @@ -0,0 +1,102 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.aot.AotFactoriesLoader; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesBeanRegistrationAotProcessor}. + * + * @author Stephane Nicoll + */ +class ConfigurationPropertiesBeanRegistrationAotProcessorTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final ConfigurationPropertiesBeanRegistrationAotProcessor processor = new ConfigurationPropertiesBeanRegistrationAotProcessor(); + + @Test + void configurationPropertiesBeanRegistrationAotProcessorIsRegistered() { + assertThat(new AotFactoriesLoader(new DefaultListableBeanFactory()).load(BeanRegistrationAotProcessor.class)) + .anyMatch(ConfigurationPropertiesBeanRegistrationAotProcessor.class::isInstance); + } + + @Test + void processAheadOfTimeWithNoConfigurationPropertiesBean() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + BeanRegistrationAotContribution contribution = this.processor + .processAheadOfTime(RegisteredBean.of(this.beanFactory, "test")); + assertThat(contribution).isNull(); + } + + @Test + void processAheadOfTimeWithJavaBeanConfigurationPropertiesBean() { + BeanRegistrationAotContribution contribution = process(JavaBeanSampleBean.class); + assertThat(contribution).isNull(); + } + + @Test + void processAheadOfTimeWithValueObjectConfigurationPropertiesBean() { + BeanRegistrationAotContribution contribution = process(ValueObjectSampleBean.class); + assertThat(contribution).isNotNull(); + } + + private BeanRegistrationAotContribution process(Class type) { + ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(this.beanFactory); + beanRegistrar.register(type); + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, + this.beanFactory.getBeanDefinitionNames()[0]); + return this.processor.processAheadOfTime(registeredBean); + } + + @ConfigurationProperties("test") + public static class JavaBeanSampleBean { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + @ConfigurationProperties("test") + public static class ValueObjectSampleBean { + + private final String name; + + ValueObjectSampleBean(String name) { + this.name = name; + } + + } + +}