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
pull/31277/head
Stephane Nicoll 2 years ago
parent 3f0c14187a
commit ac16432fad

@ -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);
}
}
}

@ -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());
}
}
}

@ -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);
}
}
}

@ -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
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor

@ -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;
}
}
}
Loading…
Cancel
Save