Merge branch '3.0.x'

Closes gh-35647
pull/35655/head
Andy Wilkinson 2 years ago
commit 127004b4c5

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,9 +24,10 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContrib
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/** /**
* {@link BeanFactoryInitializationAotProcessor} that contributes runtime hints for * {@link BeanFactoryInitializationAotProcessor} that contributes runtime hints for
@ -43,36 +44,38 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessor implements Be
public ConfigurationPropertiesReflectionHintsContribution processAheadOfTime( public ConfigurationPropertiesReflectionHintsContribution processAheadOfTime(
ConfigurableListableBeanFactory beanFactory) { ConfigurableListableBeanFactory beanFactory) {
String[] beanNames = beanFactory.getBeanNamesForAnnotation(ConfigurationProperties.class); String[] beanNames = beanFactory.getBeanNamesForAnnotation(ConfigurationProperties.class);
List<Class<?>> types = new ArrayList<>(); List<Bindable<?>> bindables = new ArrayList<>();
for (String beanName : beanNames) { for (String beanName : beanNames) {
Class<?> beanType = beanFactory.getType(beanName, false); Class<?> beanType = beanFactory.getType(beanName, false);
if (beanType != null) { if (beanType != null) {
types.add(ClassUtils.getUserClass(beanType)); BindMethod bindMethod = beanFactory.containsBeanDefinition(beanName)
? (BindMethod) beanFactory.getBeanDefinition(beanName).getAttribute(BindMethod.class.getName())
: null;
bindables.add(Bindable.of(ClassUtils.getUserClass(beanType))
.withBindMethod((bindMethod != null) ? bindMethod : BindMethod.JAVA_BEAN));
} }
} }
if (!CollectionUtils.isEmpty(types)) { return (!bindables.isEmpty()) ? new ConfigurationPropertiesReflectionHintsContribution(bindables) : null;
return new ConfigurationPropertiesReflectionHintsContribution(types);
}
return null;
} }
static final class ConfigurationPropertiesReflectionHintsContribution static final class ConfigurationPropertiesReflectionHintsContribution
implements BeanFactoryInitializationAotContribution { implements BeanFactoryInitializationAotContribution {
private final Iterable<Class<?>> types; private final List<Bindable<?>> bindables;
private ConfigurationPropertiesReflectionHintsContribution(Iterable<Class<?>> types) { private ConfigurationPropertiesReflectionHintsContribution(List<Bindable<?>> bindables) {
this.types = types; this.bindables = bindables;
} }
@Override @Override
public void applyTo(GenerationContext generationContext, public void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode) { BeanFactoryInitializationCode beanFactoryInitializationCode) {
BindableRuntimeHintsRegistrar.forTypes(this.types).registerHints(generationContext.getRuntimeHints()); BindableRuntimeHintsRegistrar.forBindables(this.bindables)
.registerHints(generationContext.getRuntimeHints());
} }
Iterable<Class<?>> getTypes() { Iterable<Bindable<?>> getBindables() {
return this.types; return this.bindables;
} }
} }

@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import kotlin.jvm.JvmClassMappingKt; import kotlin.jvm.JvmClassMappingKt;
@ -51,8 +52,8 @@ import org.springframework.util.ReflectionUtils;
* {@link RuntimeHintsRegistrar} that can be used to register {@link ReflectionHints} for * {@link RuntimeHintsRegistrar} that can be used to register {@link ReflectionHints} for
* {@link Bindable} types, discovering any nested type it may expose through a property. * {@link Bindable} types, discovering any nested type it may expose through a property.
* <p> * <p>
* This class can be used as a base-class, or instantiated using the {@code forTypes} * This class can be used as a base-class, or instantiated using the {@code forTypes} and
* factory methods. * {@code forBindables} factory methods.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Moritz Halbritter * @author Moritz Halbritter
@ -62,14 +63,23 @@ import org.springframework.util.ReflectionUtils;
*/ */
public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar { public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
private final Class<?>[] types; private final Bindable<?>[] bindables;
/** /**
* Create a new {@link BindableRuntimeHintsRegistrar} for the specified types. * Create a new {@link BindableRuntimeHintsRegistrar} for the specified types.
* @param types the types to process * @param types the types to process
*/ */
protected BindableRuntimeHintsRegistrar(Class<?>... types) { protected BindableRuntimeHintsRegistrar(Class<?>... types) {
this.types = types; this(Stream.of(types).map(Bindable::of).toArray(Bindable[]::new));
}
/**
* Create a new {@link BindableRuntimeHintsRegistrar} for the specified bindables.
* @param bindables the bindables to process
* @since 3.0.8
*/
protected BindableRuntimeHintsRegistrar(Bindable<?>... bindables) {
this.bindables = bindables;
} }
@Override @Override
@ -83,8 +93,8 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
*/ */
public void registerHints(RuntimeHints hints) { public void registerHints(RuntimeHints hints) {
Set<Class<?>> compiledWithoutParameters = new HashSet<>(); Set<Class<?>> compiledWithoutParameters = new HashSet<>();
for (Class<?> type : this.types) { for (Bindable<?> bindable : this.bindables) {
new Processor(type, compiledWithoutParameters).process(hints.reflection()); new Processor(bindable, compiledWithoutParameters).process(hints.reflection());
} }
if (!compiledWithoutParameters.isEmpty()) { if (!compiledWithoutParameters.isEmpty()) {
throw new MissingParametersCompilerArgumentException(compiledWithoutParameters); throw new MissingParametersCompilerArgumentException(compiledWithoutParameters);
@ -110,6 +120,27 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
return new BindableRuntimeHintsRegistrar(types); return new BindableRuntimeHintsRegistrar(types);
} }
/**
* Create a new {@link BindableRuntimeHintsRegistrar} for the specified bindables.
* @param bindables the bindables to process
* @return a new {@link BindableRuntimeHintsRegistrar} instance
* @since 3.0.8
*/
public static BindableRuntimeHintsRegistrar forBindables(Iterable<Bindable<?>> bindables) {
Assert.notNull(bindables, "Bindables must not be null");
return forBindables(StreamSupport.stream(bindables.spliterator(), false).toArray(Bindable[]::new));
}
/**
* Create a new {@link BindableRuntimeHintsRegistrar} for the specified bindables.
* @param bindables the bindables to process
* @return a new {@link BindableRuntimeHintsRegistrar} instance
* @since 3.0.8
*/
public static BindableRuntimeHintsRegistrar forBindables(Bindable<?>... bindables) {
return new BindableRuntimeHintsRegistrar(bindables);
}
/** /**
* Processor used to register the hints. * Processor used to register the hints.
*/ */
@ -136,15 +167,17 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
private final Set<Class<?>> compiledWithoutParameters; private final Set<Class<?>> compiledWithoutParameters;
Processor(Class<?> type, Set<Class<?>> compiledWithoutParameters) { Processor(Bindable<?> bindable, Set<Class<?>> compiledWithoutParameters) {
this(type, false, new HashSet<>(), compiledWithoutParameters); this(bindable, false, new HashSet<>(), compiledWithoutParameters);
} }
private Processor(Class<?> type, boolean nestedType, Set<Class<?>> seen, private Processor(Bindable<?> bindable, boolean nestedType, Set<Class<?>> seen,
Set<Class<?>> compiledWithoutParameters) { Set<Class<?>> compiledWithoutParameters) {
this.type = type; this.type = bindable.getType().getRawClass();
this.bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(Bindable.of(type), nestedType); this.bindConstructor = (bindable.getBindMethod() != BindMethod.JAVA_BEAN)
this.bean = JavaBeanBinder.BeanProperties.of(Bindable.of(type)); ? BindConstructorProvider.DEFAULT.getBindConstructor(bindable.getType().resolve(), nestedType)
: null;
this.bean = JavaBeanBinder.BeanProperties.of(bindable);
this.seen = seen; this.seen = seen;
this.compiledWithoutParameters = compiledWithoutParameters; this.compiledWithoutParameters = compiledWithoutParameters;
} }
@ -235,7 +268,7 @@ public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
} }
private void processNested(Class<?> type, ReflectionHints hints) { private void processNested(Class<?> type, ReflectionHints hints) {
new Processor(type, true, this.seen, this.compiledWithoutParameters).process(hints); new Processor(Bindable.of(type), true, this.seen, this.compiledWithoutParameters).process(hints);
} }
private Class<?> getComponentClass(ResolvableType type) { private Class<?> getComponentClass(ResolvableType type) {

@ -18,22 +18,26 @@ package org.springframework.boot.context.properties;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeHint;
import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.AotServices; import org.springframework.beans.factory.aot.AotServices;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor.ConfigurationPropertiesReflectionHintsContribution; import org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor.ConfigurationPropertiesReflectionHintsContribution;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link ConfigurationPropertiesBeanFactoryInitializationAotProcessor}. * Tests for {@link ConfigurationPropertiesBeanFactoryInitializationAotProcessor}.
@ -41,6 +45,7 @@ import static org.mockito.Mockito.mock;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Andy Wilkinson
*/ */
class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests { class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests {
@ -58,44 +63,192 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests {
} }
@Test @Test
void processManuallyRegisteredSingleton() { void manuallyRegisteredSingletonBindsAsJavaBean() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("test", new SampleProperties()); beanFactory.registerSingleton("test", new SampleProperties());
ConfigurationPropertiesReflectionHintsContribution contribution = process(beanFactory); ConfigurationPropertiesReflectionHintsContribution contribution = process(beanFactory);
assertThat(contribution.getTypes()).containsExactly(SampleProperties.class); assertThat(singleBindable(contribution)).hasBindMethod(BindMethod.JAVA_BEAN).hasType(SampleProperties.class);
assertThat(typeHints(contribution).map(TypeHint::getType)) assertThat(typeHints(contribution).map(TypeHint::getType))
.containsExactly(TypeReference.of(SampleProperties.class)); .containsExactly(TypeReference.of(SampleProperties.class));
} }
@Test @Test
void processDefinedBean() { void javaBeanConfigurationPropertiesBindAsJavaBean() {
ConfigurationPropertiesReflectionHintsContribution contribution = process(SampleProperties.class); ConfigurationPropertiesReflectionHintsContribution contribution = process(EnableJavaBeanProperties.class);
assertThat(contribution.getTypes()).containsExactly(SampleProperties.class); assertThat(singleBindable(contribution)).hasBindMethod(BindMethod.JAVA_BEAN).hasType(JavaBeanProperties.class);
assertThat(typeHints(contribution).map(TypeHint::getType)) assertThat(typeHints(contribution).map(TypeHint::getType))
.containsExactly(TypeReference.of(SampleProperties.class)); .containsExactly(TypeReference.of(JavaBeanProperties.class));
}
@Test
void constructorBindingConfigurationPropertiesBindAsValueObject() {
ConfigurationPropertiesReflectionHintsContribution contribution = process(
EnableConstructorBindingProperties.class);
assertThat(singleBindable(contribution)).hasBindMethod(BindMethod.VALUE_OBJECT)
.hasType(ConstructorBindingProperties.class);
assertThat(typeHints(contribution).map(TypeHint::getType))
.containsExactly(TypeReference.of(ConstructorBindingProperties.class));
} }
Stream<TypeHint> typeHints(ConfigurationPropertiesReflectionHintsContribution contribution) { @Test
GenerationContext generationContext = new TestGenerationContext(); void possibleConstructorBindingPropertiesDefinedThroughBeanMethodBindAsJavaBean() {
contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class)); ConfigurationPropertiesReflectionHintsContribution contribution = process(
PossibleConstructorBindingPropertiesBeanMethodConfiguration.class);
assertThat(singleBindable(contribution)).hasBindMethod(BindMethod.JAVA_BEAN)
.hasType(PossibleConstructorBindingProperties.class);
assertThat(typeHints(contribution).map(TypeHint::getType))
.containsExactly(TypeReference.of(PossibleConstructorBindingProperties.class));
}
@Test
void possibleConstructorBindingPropertiesDefinedThroughEnabledAnnotationBindAsValueObject() {
ConfigurationPropertiesReflectionHintsContribution contribution = process(
EnablePossibleConstructorBindingProperties.class);
assertThat(singleBindable(contribution)).hasBindMethod(BindMethod.VALUE_OBJECT)
.hasType(PossibleConstructorBindingProperties.class);
assertThat(typeHints(contribution).map(TypeHint::getType))
.containsExactly(TypeReference.of(PossibleConstructorBindingProperties.class));
}
private Stream<TypeHint> typeHints(ConfigurationPropertiesReflectionHintsContribution contribution) {
TestGenerationContext generationContext = new TestGenerationContext();
contribution.applyTo(generationContext, null);
return generationContext.getRuntimeHints().reflection().typeHints(); return generationContext.getRuntimeHints().reflection().typeHints();
} }
private ConfigurationPropertiesReflectionHintsContribution process(Class<?>... types) { private ConfigurationPropertiesReflectionHintsContribution process(Class<?> config) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(config)) {
for (Class<?> type : types) { return process(context.getBeanFactory());
beanFactory.registerBeanDefinition(type.getName(), new RootBeanDefinition(type));
} }
return process(beanFactory);
} }
private ConfigurationPropertiesReflectionHintsContribution process(ConfigurableListableBeanFactory beanFactory) { private ConfigurationPropertiesReflectionHintsContribution process(ConfigurableListableBeanFactory beanFactory) {
return this.processor.processAheadOfTime(beanFactory); return this.processor.processAheadOfTime(beanFactory);
} }
private BindableAssertProvider singleBindable(ConfigurationPropertiesReflectionHintsContribution contribution) {
assertThat(contribution.getBindables()).hasSize(1);
return new BindableAssertProvider(contribution.getBindables().iterator().next());
}
@ConfigurationProperties("test") @ConfigurationProperties("test")
static class SampleProperties { static class SampleProperties {
} }
@EnableConfigurationProperties(JavaBeanProperties.class)
static class EnableJavaBeanProperties {
}
@ConfigurationProperties("java-bean")
static class JavaBeanProperties {
private String value;
String getValue() {
return this.value;
}
void setValue(String value) {
this.value = value;
}
}
@EnableConfigurationProperties(ConstructorBindingProperties.class)
static class EnableConstructorBindingProperties {
}
@ConfigurationProperties("constructor-binding")
static class ConstructorBindingProperties {
private final String value;
ConstructorBindingProperties(String value) {
this.value = value;
}
String getValue() {
return this.value;
}
}
@Configuration(proxyBeanMethods = false)
static class PossibleConstructorBindingPropertiesBeanMethodConfiguration {
@Bean
@ConfigurationProperties(prefix = "bean-method")
PossibleConstructorBindingProperties possibleConstructorBindingProperties() {
return new PossibleConstructorBindingProperties("alpha");
}
}
@EnableConfigurationProperties(PossibleConstructorBindingProperties.class)
static class EnablePossibleConstructorBindingProperties {
}
@ConfigurationProperties("possible-constructor-binding")
static class PossibleConstructorBindingProperties {
private String value;
PossibleConstructorBindingProperties(String arg) {
}
String getValue() {
return this.value;
}
void setValue(String value) {
this.value = value;
}
}
static class BindableAssertProvider implements AssertProvider<BindableAssert> {
private final Bindable<?> bindable;
BindableAssertProvider(Bindable<?> bindable) {
this.bindable = bindable;
}
@Override
public BindableAssert assertThat() {
return new BindableAssert(this.bindable);
}
}
static class BindableAssert extends AbstractAssert<BindableAssert, Bindable<?>> {
BindableAssert(Bindable<?> bindable) {
super(bindable, BindableAssert.class);
}
BindableAssert hasBindMethod(BindMethod bindMethod) {
if (this.actual.getBindMethod() != bindMethod) {
throwAssertionError(
new BasicErrorMessageFactory("Expected %s to have bind method %s but bind method was %s",
this.actual, bindMethod, this.actual.getBindMethod()));
}
return this;
}
BindableAssert hasType(Class<?> type) {
if (!type.equals(this.actual.getType().resolve())) {
throwAssertionError(new BasicErrorMessageFactory("Expected %s to have type %s but type was %s",
this.actual, type, this.actual.getType().resolve()));
}
return this;
}
}
} }

@ -75,7 +75,7 @@ class BindableRuntimeHintsRegistrarTests {
@Test @Test
void registerHintsWhenNoClasses() { void registerHintsWhenNoClasses() {
RuntimeHints runtimeHints = new RuntimeHints(); RuntimeHints runtimeHints = new RuntimeHints();
BindableRuntimeHintsRegistrar registrar = new BindableRuntimeHintsRegistrar(); BindableRuntimeHintsRegistrar registrar = new BindableRuntimeHintsRegistrar(new Class<?>[0]);
registrar.registerHints(runtimeHints); registrar.registerHints(runtimeHints);
assertThat(runtimeHints.reflection().typeHints()).isEmpty(); assertThat(runtimeHints.reflection().typeHints()).isEmpty();
} }

Loading…
Cancel
Save