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.