From 430571b37b808c42b950c14778695a541adc45e9 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 7 Mar 2019 13:48:42 +0100 Subject: [PATCH] Perform binding at creation time if possible Previously, environment binding always happened in a post processor once the bean has been created. Constructor binding requires to perform the binding at creating time so this commit performs binding at creation time if possible. When this happens, a special `ConfigurationPropertiesBeanDefinition` is created with a supplier that invokes the binder. To avoid a case where a bean is processed twice, the post-processor now ignores any bean that has already been bound to the environment. Closes gh-8762 Co-authored-by: Madhura Bhave --- .../endpoint/jmx/JmxEndpointProperties.java | 2 + .../session/SessionProperties.java | 4 +- ...ConfigurationPropertiesBeanDefinition.java | 74 +++++++ .../ConfigurationPropertiesBindException.java | 11 +- .../ConfigurationPropertiesBinder.java | 37 +++- ...urationPropertiesBindingPostProcessor.java | 26 ++- ...opertiesBindingPostProcessorRegistrar.java | 21 +- ...ConfigurationPropertiesImportSelector.java | 39 +++- .../boot/context/properties/bind/Binder.java | 6 +- .../ConfigurationPropertiesTests.java | 111 ++++++++++ ...gurationPropertiesImportSelectorTests.java | 195 ++++++++++++++++++ .../properties/bind/JavaBeanBinderTests.java | 2 +- 12 files changed, 491 insertions(+), 37 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelectorTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointProperties.java index 9d0b691bf3..5cb5c25c3a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointProperties.java @@ -20,6 +20,7 @@ import java.util.LinkedHashSet; import java.util.Properties; import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; @@ -46,6 +47,7 @@ public class JmxEndpointProperties { */ private final Properties staticNames = new Properties(); + @Autowired public JmxEndpointProperties(Environment environment) { String defaultDomain = environment.getProperty("spring.jmx.default-domain"); if (StringUtils.hasText(defaultDomain)) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java index 6f9cebf67a..64e7f2049f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -25,6 +25,7 @@ import java.util.Set; import javax.annotation.PostConstruct; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; @@ -58,6 +59,7 @@ public class SessionProperties { private final ServerProperties serverProperties; + @Autowired public SessionProperties(ObjectProvider serverProperties) { this.serverProperties = serverProperties.getIfUnique(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java new file mode 100644 index 0000000000..498012331f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 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 + * + * http://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.annotation.Annotation; +import java.util.function.Supplier; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.annotation.Validated; + +/** + * {@link BeanDefinition} that is used for registering {@link ConfigurationProperties} + * beans that are bound at creation time. + * + * @author Stephane Nicoll + * @author Madhura Bhave + */ +final class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition { + + static ConfigurationPropertiesBeanDefinition from( + ConfigurableListableBeanFactory beanFactory, String beanName, Class type) { + ConfigurationPropertiesBeanDefinition beanDefinition = new ConfigurationPropertiesBeanDefinition(); + beanDefinition.setBeanClass(type); + beanDefinition.setInstanceSupplier(createBean(beanFactory, beanName, type)); + return beanDefinition; + } + + private static Supplier createBean(ConfigurableListableBeanFactory beanFactory, + String beanName, Class type) { + return () -> { + ConfigurationProperties annotation = getAnnotation(type, + ConfigurationProperties.class); + Validated validated = getAnnotation(type, Validated.class); + Annotation[] annotations = (validated != null) + ? new Annotation[] { annotation, validated } + : new Annotation[] { annotation }; + Bindable bindable = Bindable.of(type).withAnnotations(annotations); + ConfigurationPropertiesBinder binder = beanFactory.getBean( + ConfigurationPropertiesBinder.BEAN_NAME, + ConfigurationPropertiesBinder.class); + try { + return binder.bind(bindable).orElseCreate(type); + } + catch (Exception ex) { + throw new ConfigurationPropertiesBindException(beanName, type, annotation, + ex); + } + }; + } + + private static A getAnnotation(Class type, + Class annotationType) { + return AnnotationUtils.findAnnotation(type, annotationType); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java index c1ef345336..7564d97779 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java @@ -33,10 +33,10 @@ public class ConfigurationPropertiesBindException extends BeanCreationException private final ConfigurationProperties annotation; - ConfigurationPropertiesBindException(String beanName, Object bean, + ConfigurationPropertiesBindException(String beanName, Class beanType, ConfigurationProperties annotation, Exception cause) { - super(beanName, getMessage(bean, annotation), cause); - this.beanType = bean.getClass(); + super(beanName, getMessage(beanType, annotation), cause); + this.beanType = beanType; this.annotation = annotation; } @@ -56,10 +56,11 @@ public class ConfigurationPropertiesBindException extends BeanCreationException return this.annotation; } - private static String getMessage(Object bean, ConfigurationProperties annotation) { + private static String getMessage(Class beanType, + ConfigurationProperties annotation) { StringBuilder message = new StringBuilder(); message.append("Could not bind properties to '"); - message.append(ClassUtils.getShortName(bean.getClass())).append("' : "); + message.append(ClassUtils.getShortName(beanType)).append("' : "); message.append("prefix=").append(annotation.prefix()); message.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); message.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index d16dc05ff4..562812ec91 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -21,8 +21,10 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; +import org.springframework.beans.BeansException; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; @@ -34,6 +36,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; @@ -48,39 +51,51 @@ import org.springframework.validation.annotation.Validated; * @author Stephane Nicoll * @author Phillip Webb */ -class ConfigurationPropertiesBinder { +class ConfigurationPropertiesBinder implements ApplicationContextAware { - private final ApplicationContext applicationContext; + /** + * The bean name that this binder is registered with. + */ + static final String BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinder"; - private final PropertySources propertySources; + private final String validatorBeanName; - private final Validator configurationPropertiesValidator; + private ApplicationContext applicationContext; - private final boolean jsr303Present; + private PropertySources propertySources; + + private Validator configurationPropertiesValidator; + + private boolean jsr303Present; private volatile Validator jsr303Validator; private volatile Binder binder; - ConfigurationPropertiesBinder(ApplicationContext applicationContext, - String validatorBeanName) { + ConfigurationPropertiesBinder(String validatorBeanName) { + this.validatorBeanName = validatorBeanName; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { this.applicationContext = applicationContext; this.propertySources = new PropertySourcesDeducer(applicationContext) .getPropertySources(); this.configurationPropertiesValidator = getConfigurationPropertiesValidator( - applicationContext, validatorBeanName); + applicationContext, this.validatorBeanName); this.jsr303Present = ConfigurationPropertiesJsr303Validator .isJsr303Present(applicationContext); } - public void bind(Bindable target) { + public BindResult bind(Bindable target) { ConfigurationProperties annotation = target .getAnnotation(ConfigurationProperties.class); Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target); List validators = getValidators(target); BindHandler bindHandler = getBindHandler(annotation, validators); - getBinder().bind(annotation.prefix(), target, bindHandler); + return getBinder().bind(annotation.prefix(), target, bindHandler); } private Validator getConfigurationPropertiesValidator( diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index ff0a5a5fb9..40f9775c6f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -21,7 +21,9 @@ import java.lang.reflect.Method; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -53,8 +55,11 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc /** * The bean name of the configuration properties validator. + * @deprecated see + * {@link ConfigurationPropertiesBindingPostProcessorRegistrar#VALIDATOR_BEAN_NAME} */ - public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; + @Deprecated + public static final String VALIDATOR_BEAN_NAME = ConfigurationPropertiesBindingPostProcessorRegistrar.VALIDATOR_BEAN_NAME; private ConfigurationBeanFactoryMetadata beanFactoryMetadata; @@ -75,8 +80,9 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc this.beanFactoryMetadata = this.applicationContext.getBean( ConfigurationBeanFactoryMetadata.BEAN_NAME, ConfigurationBeanFactoryMetadata.class); - this.configurationPropertiesBinder = new ConfigurationPropertiesBinder( - this.applicationContext, VALIDATOR_BEAN_NAME); + this.configurationPropertiesBinder = this.applicationContext.getBean( + ConfigurationPropertiesBinder.BEAN_NAME, + ConfigurationPropertiesBinder.class); } @Override @@ -89,12 +95,18 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc throws BeansException { ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class); - if (annotation != null) { + if (annotation != null && !hasBeenBound(beanName)) { bind(bean, beanName, annotation); } return bean; } + private boolean hasBeenBound(String beanName) { + BeanDefinition beanDefinition = ((BeanDefinitionRegistry) this.applicationContext + .getAutowireCapableBeanFactory()).getBeanDefinition(beanName); + return beanDefinition instanceof ConfigurationPropertiesBeanDefinition; + } + private void bind(Object bean, String beanName, ConfigurationProperties annotation) { ResolvableType type = getBeanType(bean, beanName); Validated validated = getAnnotation(bean, beanName, Validated.class); @@ -107,8 +119,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc this.configurationPropertiesBinder.bind(target); } catch (Exception ex) { - throw new ConfigurationPropertiesBindException(beanName, bean, annotation, - ex); + throw new ConfigurationPropertiesBindException(beanName, bean.getClass(), + annotation, ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java index d876563504..1cef372171 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -32,9 +32,17 @@ import org.springframework.core.type.AnnotationMetadata; public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar { + /** + * The bean name of the configuration properties validator. + */ + public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; + @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME)) { + registerConfigurationPropertiesBinder(registry); + } if (!registry.containsBeanDefinition( ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) { registerConfigurationPropertiesBindingPostProcessor(registry); @@ -42,6 +50,16 @@ public class ConfigurationPropertiesBindingPostProcessorRegistrar } } + private void registerConfigurationPropertiesBinder(BeanDefinitionRegistry registry) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBinder.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + definition.getConstructorArgumentValues().addIndexedArgumentValue(0, + VALIDATOR_BEAN_NAME); + registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, + definition); + } + private void registerConfigurationPropertiesBindingPostProcessor( BeanDefinitionRegistry registry) { GenericBeanDefinition definition = new GenericBeanDefinition(); @@ -49,7 +67,6 @@ public class ConfigurationPropertiesBindingPostProcessorRegistrar definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); registry.registerBeanDefinition( ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition); - } private void registerConfigurationBeanFactoryMetadata( diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java index a0b8dc1443..09d4e29949 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -16,12 +16,15 @@ package org.springframework.boot.context.properties; +import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; @@ -89,7 +92,7 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { ConfigurableListableBeanFactory beanFactory, Class type) { String name = getName(type); if (!containsBeanDefinition(beanFactory, name)) { - registerBeanDefinition(registry, name, type); + registerBeanDefinition(registry, beanFactory, name, type); } } @@ -114,12 +117,11 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { return false; } - private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, - Class type) { + private void registerBeanDefinition(BeanDefinitionRegistry registry, + ConfigurableListableBeanFactory beanFactory, String name, Class type) { assertHasAnnotation(type); - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(type); - registry.registerBeanDefinition(name, definition); + registry.registerBeanDefinition(name, + createBeanDefinition(beanFactory, name, type)); } private void assertHasAnnotation(Class type) { @@ -129,6 +131,29 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { + " annotation found on '" + type.getName() + "'."); } + private BeanDefinition createBeanDefinition( + ConfigurableListableBeanFactory beanFactory, String name, Class type) { + if (canBindAtCreationTime(type)) { + return ConfigurationPropertiesBeanDefinition.from(beanFactory, name, + type); + } + else { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(type); + return definition; + } + } + + private boolean canBindAtCreationTime(Class type) { + Constructor[] constructors = type.getDeclaredConstructors(); + boolean autowiredPresent = Arrays.stream(constructors).anyMatch( + (c) -> AnnotationUtils.findAnnotation(c, Autowired.class) != null); + if (autowiredPresent) { + return false; + } + return (constructors.length == 1 && constructors[0].getParameterCount() > 0); + } + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 50ca79dcb5..dadea24f22 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -254,7 +254,8 @@ public class Binder { private Object bindObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) { ConfigurationProperty property = findProperty(name, context); - if (property == null && containsNoDescendantOf(context.getSources(), name)) { + if (property == null && containsNoDescendantOf(context.getSources(), name) + && context.depth != 0) { return null; } AggregateBinder aggregateBinder = getAggregateBinder(target, context); @@ -330,8 +331,7 @@ public class Binder { private Object bindBean(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) { - if (containsNoDescendantOf(context.getSources(), name) - || isUnbindableBean(name, target, context)) { + if (isUnbindableBean(name, target, context)) { return null; } Class type = target.getType().resolve(Object.class); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 971d8d54c2..3da426a42b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -41,6 +41,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -805,6 +806,46 @@ public class ConfigurationPropertiesTests { assertThat(x.get(1).getB()).isEqualTo(1); } + @Test + public void loadWhenConfigurationPropertiesInjectsAnotherBeanShouldNotFail() { + load(OtherInjectPropertiesConfiguration.class); + } + + @Test + public void loadWhenBindingToConstructorParametersShouldBind() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + Map source = new HashMap<>(); + source.put("test.foo", "baz"); + source.put("test.bar", "5"); + sources.addLast(new MapPropertySource("test", source)); + load(ConstructorParameterConfiguration.class); + ConstructorParameterProperties bean = this.context + .getBean(ConstructorParameterProperties.class); + assertThat(bean.getFoo()).isEqualTo("baz"); + assertThat(bean.getBar()).isEqualTo(5); + } + + @Test + public void loadWhenBindingToConstructorParametersWithDefaultValuesShouldBind() { + load(ConstructorParameterConfiguration.class); + ConstructorParameterProperties bean = this.context + .getBean(ConstructorParameterProperties.class); + assertThat(bean.getFoo()).isEqualTo("hello"); + assertThat(bean.getBar()).isEqualTo(0); + } + + @Test + public void loadWhenBindingToConstructorParametersShouldValidate() { + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> load(ConstructorParameterValidationConfiguration.class)) + .satisfies((ex) -> { + assertThat(ex).hasCauseInstanceOf(BindException.class); + assertThat(ex.getCause()) + .hasCauseExactlyInstanceOf(BindValidationException.class); + }); + } + private AnnotationConfigApplicationContext load(Class configuration, String... inlinedProperties) { return load(new Class[] { configuration }, inlinedProperties); @@ -1768,6 +1809,76 @@ public class ConfigurationPropertiesTests { } + @ConfigurationProperties(prefix = "test") + static class OtherInjectedProperties { + + private final DataSizeProperties dataSizeProperties; + + @Autowired + OtherInjectedProperties(ObjectProvider dataSizeProperties) { + this.dataSizeProperties = dataSizeProperties.getIfUnique(); + } + + } + + @Configuration + @EnableConfigurationProperties(OtherInjectedProperties.class) + static class OtherInjectPropertiesConfiguration { + + } + + @ConfigurationProperties(prefix = "test") + @Validated + static class ConstructorParameterProperties { + + @NotEmpty + private final String foo; + + private final int bar; + + ConstructorParameterProperties( + @ConfigurationPropertyDefaultValue("hello") String foo, int bar) { + this.foo = foo; + this.bar = bar; + } + + public String getFoo() { + return this.foo; + } + + public int getBar() { + return this.bar; + } + + } + + @ConfigurationProperties(prefix = "test") + @Validated + static class ConstructorParameterValidatedProperties { + + @NotEmpty + private final String foo; + + ConstructorParameterValidatedProperties(String foo) { + this.foo = foo; + } + + public String getFoo() { + return this.foo; + } + + } + + @EnableConfigurationProperties(ConstructorParameterProperties.class) + static class ConstructorParameterConfiguration { + + } + + @EnableConfigurationProperties(ConstructorParameterValidatedProperties.class) + static class ConstructorParameterValidationConfiguration { + + } + static class CustomPropertiesValidator implements Validator { @Override diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelectorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelectorTests.java new file mode 100644 index 0000000000..1320bcacfd --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelectorTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012-2019 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 + * + * http://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.io.IOException; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link EnableConfigurationPropertiesImportSelector}. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +public class EnableConfigurationPropertiesImportSelectorTests { + + private final EnableConfigurationPropertiesImportSelector importSelector = new EnableConfigurationPropertiesImportSelector(); + + private final EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar registrar = new EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar(); + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + public void selectImports() { + String[] imports = this.importSelector + .selectImports(mock(AnnotationMetadata.class)); + assertThat(imports).containsExactly( + EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar.class + .getName(), + ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName()); + } + + @Test + public void typeWithDefaultConstructorShouldRegisterGenericBeanDefinition() + throws Exception { + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(TestConfiguration.class), this.beanFactory); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( + "foo-org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelectorTests$FooProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + public void typeWithAutowiredOnConstructorShouldRegisterGenericBeanDefinition() + throws Exception { + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(TestConfiguration.class), this.beanFactory); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( + "bar-org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelectorTests$BarProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + public void typeWithOneConstructorWithParametersShouldRegisterConfigurationPropertiesBeanDefinition() + throws Exception { + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(TestConfiguration.class), this.beanFactory); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( + "baz-org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelectorTests$BazProperties"); + assertThat(beanDefinition) + .isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + } + + @Test + public void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() + throws Exception { + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(TestConfiguration.class), this.beanFactory); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( + "bing-org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelectorTests$BingProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + public void typeWithNoAnnotationShouldFail() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.registrar.registerBeanDefinitions( + getAnnotationMetadata(InvalidConfiguration.class), + this.beanFactory)) + .withMessageContaining("No ConfigurationProperties annotation found") + .withMessageContaining( + EnableConfigurationPropertiesImportSelectorTests.class.getName()); + } + + @Test + public void registrationWithDuplicatedTypeShouldRegisterSingleBeanDefinition() + throws IOException { + DefaultListableBeanFactory factory = spy(this.beanFactory); + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(DuplicateConfiguration.class), factory); + verify(factory, times(1)).registerBeanDefinition(anyString(), any()); + } + + @Test + public void registrationWithNoTypeShouldNotRegisterAnything() throws IOException { + DefaultListableBeanFactory factory = spy(this.beanFactory); + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(EmptyConfiguration.class), factory); + verifyZeroInteractions(factory); + } + + private AnnotationMetadata getAnnotationMetadata(Class source) throws IOException { + return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()) + .getAnnotationMetadata(); + } + + @EnableConfigurationProperties({ FooProperties.class, BarProperties.class, + BazProperties.class, BingProperties.class }) + static class TestConfiguration { + + } + + @EnableConfigurationProperties(EnableConfigurationPropertiesImportSelectorTests.class) + static class InvalidConfiguration { + + } + + @EnableConfigurationProperties({ FooProperties.class, FooProperties.class }) + static class DuplicateConfiguration { + + } + + @EnableConfigurationProperties + static class EmptyConfiguration { + + } + + @ConfigurationProperties(prefix = "foo") + static class FooProperties { + + } + + @ConfigurationProperties(prefix = "bar") + public static class BarProperties { + + @Autowired + public BarProperties(String foo) { + + } + + } + + @ConfigurationProperties(prefix = "baz") + public static class BazProperties { + + public BazProperties(String foo) { + + } + + } + + @ConfigurationProperties(prefix = "bing") + public static class BingProperties { + + public BingProperties() { + + } + + public BingProperties(String foo) { + + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index dba5c910d8..a69c699cd2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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.