Skip to content

Commit f033016

Browse files
wilkinsonaphilwebb
authored andcommitted
Update configuration properties support to allow the `@Component` annotation to be used on `@ConfigurationProperties` beans as long as they are mutable. This restores the behavior of Spring Boot 2.1 for mutable beans whilst still allowing us to enforce the stricter rules for immutable value object configuration properties. Closes gh-18138
1 parent 4c0cf8f commit f033016

File tree

8 files changed

+28
-288
lines changed

8 files changed

+28
-288
lines changed

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,8 @@ In this setup one, and only one constructor must be defined with the list of pro
903903

904904
Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property.
905905

906+
NOTE: To use constructor binding the class must not be annotated with `@Component` and must be enabled using `@EnableConfigurationProperties` or configuration property scanning instead.
907+
906908

907909

908910
[[boot-features-external-config-enabling]]
@@ -933,20 +935,15 @@ In these cases, you can specify the list of types to process on any `@Configurat
933935

934936
[NOTE]
935937
====
936-
When the `@ConfigurationProperties` bean is registered using scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified name of the bean.
938+
When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified name of the bean.
937939
If the annotation does not provide any prefix, only the fully qualified name of the bean is used.
938940
939941
The bean name in the example above is `acme-com.example.AcmeProperties`.
940942
====
941943

942944
We recommend that `@ConfigurationProperties` only deal with the environment and, in particular, does not inject other beans from the context.
943-
In particular, it is not possible to inject other beans using the constructor as this would trigger the constructor binder that only deals with the environment.
944-
945-
For corner cases, setter injection can be used or any of the `*Aware` interfaces provided
946-
by the framework (such as `EnvironmentAware` if you need access to the `Environment`).
947-
948-
NOTE: Annotating a `@ConfigurationProperties` type with `@Component` will result in two beans of the same type if the type is also scanned as part of classpath scanning.
949-
If you want to register the bean yourself using `@Component`, consider disabling scanning of `@ConfigurationProperties`.
945+
For corner cases, setter injection can be used or any of the `*Aware` interfaces provided by the framework (such as `EnvironmentAware` if you need access to the `Environment`).
946+
If you still want to inject other beans using the constructor, the configuration properties bean must be annotated with `@Component` and use JavaBean-based property binding.
950947

951948

952949

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2929
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
3030
import org.springframework.core.annotation.AnnotationAttributes;
31-
import org.springframework.core.annotation.MergedAnnotation;
3231
import org.springframework.core.annotation.MergedAnnotations;
32+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3333
import org.springframework.core.env.Environment;
3434
import org.springframework.core.io.ResourceLoader;
3535
import org.springframework.core.type.AnnotationMetadata;
@@ -99,21 +99,18 @@ private void scan(ConfigurableListableBeanFactory beanFactory, BeanDefinitionReg
9999
String beanClassName = candidate.getBeanClassName();
100100
try {
101101
Class<?> type = ClassUtils.forName(beanClassName, null);
102-
validateScanConfiguration(type);
103-
ConfigurationPropertiesBeanDefinitionRegistrar.register(registry, beanFactory, type);
102+
if (!isComponent(type)) {
103+
ConfigurationPropertiesBeanDefinitionRegistrar.register(registry, beanFactory, type);
104+
}
104105
}
105106
catch (ClassNotFoundException ex) {
106107
// Ignore
107108
}
108109
}
109110
}
110111

111-
private void validateScanConfiguration(Class<?> type) {
112-
MergedAnnotation<Component> component = MergedAnnotations
113-
.from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(Component.class);
114-
if (component.isPresent()) {
115-
throw new InvalidConfigurationPropertiesException(type, component.getRoot().getType());
116-
}
112+
private boolean isComponent(Class<?> type) {
113+
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class);
117114
}
118115

119116
@Override

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/InvalidConfigurationPropertiesException.java

Lines changed: 0 additions & 50 deletions
This file was deleted.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/InvalidConfigurationPropertiesFailureAnalyzer.java

Lines changed: 0 additions & 80 deletions
This file was deleted.

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@
2323
import org.springframework.beans.factory.config.BeanDefinition;
2424
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2525
import org.springframework.beans.factory.support.GenericBeanDefinition;
26-
import org.springframework.boot.context.properties.scan.invalid.c.InvalidConfiguration;
27-
import org.springframework.boot.context.properties.scan.invalid.d.OtherInvalidConfiguration;
26+
import org.springframework.boot.context.properties.scan.combined.c.CombinedConfiguration;
27+
import org.springframework.boot.context.properties.scan.combined.d.OtherCombinedConfiguration;
2828
import org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration;
2929
import org.springframework.core.type.AnnotationMetadata;
3030
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
3131
import org.springframework.mock.env.MockEnvironment;
3232

3333
import static org.assertj.core.api.Assertions.assertThat;
34-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3534

3635
/**
3736
* Tests for {@link ConfigurationPropertiesScanRegistrar}.
@@ -99,38 +98,33 @@ void scanWhenBasePackagesAndBasePackcageClassesProvidedShouldUseThat() throws IO
9998
}
10099

101100
@Test
102-
void scanWhenComponentAnnotationPresentShouldThrowException() {
101+
void scanWhenComponentAnnotationPresentShouldSkipType() throws IOException {
103102
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
104103
beanFactory.setAllowBeanDefinitionOverriding(false);
105-
assertThatExceptionOfType(InvalidConfigurationPropertiesException.class)
106-
.isThrownBy(() -> this.registrar
107-
.registerBeanDefinitions(getAnnotationMetadata(InvalidScanConfiguration.class), beanFactory))
108-
.withMessageContaining(
109-
"Found @Component and @ConfigurationProperties on org.springframework.boot.context.properties.scan.invalid.c.InvalidConfiguration$MyProperties.");
104+
this.registrar.registerBeanDefinitions(getAnnotationMetadata(CombinedScanConfiguration.class), beanFactory);
105+
assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(0);
110106
}
111107

112108
@Test
113-
void scanWhenOtherComponentAnnotationPresentShouldThrowException() {
109+
void scanWhenOtherComponentAnnotationPresentShouldSkipType() throws IOException {
114110
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
115111
beanFactory.setAllowBeanDefinitionOverriding(false);
116-
assertThatExceptionOfType(InvalidConfigurationPropertiesException.class)
117-
.isThrownBy(() -> this.registrar.registerBeanDefinitions(
118-
getAnnotationMetadata(OtherInvalidScanConfiguration.class), beanFactory))
119-
.withMessageContaining(
120-
"Found @RestController and @ConfigurationProperties on org.springframework.boot.context.properties.scan.invalid.d.OtherInvalidConfiguration$MyControllerProperties.");
112+
this.registrar.registerBeanDefinitions(getAnnotationMetadata(OtherCombinedScanConfiguration.class),
113+
beanFactory);
114+
assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(0);
121115
}
122116

123117
private AnnotationMetadata getAnnotationMetadata(Class<?> source) throws IOException {
124118
return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()).getAnnotationMetadata();
125119
}
126120

127-
@ConfigurationPropertiesScan(basePackageClasses = InvalidConfiguration.class)
128-
static class InvalidScanConfiguration {
121+
@ConfigurationPropertiesScan(basePackageClasses = CombinedConfiguration.class)
122+
static class CombinedScanConfiguration {
129123

130124
}
131125

132-
@ConfigurationPropertiesScan(basePackageClasses = OtherInvalidConfiguration.class)
133-
static class OtherInvalidScanConfiguration {
126+
@ConfigurationPropertiesScan(basePackageClasses = OtherCombinedConfiguration.class)
127+
static class OtherCombinedScanConfiguration {
134128

135129
}
136130

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.boot.context.properties.scan.invalid.c;
16+
package org.springframework.boot.context.properties.scan.combined.c;
1717

1818
import org.springframework.boot.context.properties.ConfigurationProperties;
1919
import org.springframework.stereotype.Component;
2020

2121
/**
2222
* @author Madhura Bhave
2323
*/
24-
public class InvalidConfiguration {
24+
public class CombinedConfiguration {
2525

2626
@Component
2727
@ConfigurationProperties(prefix = "b")
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.boot.context.properties.scan.invalid.d;
17+
package org.springframework.boot.context.properties.scan.combined.d;
1818

1919
import org.springframework.boot.context.properties.ConfigurationProperties;
2020
import org.springframework.web.bind.annotation.RestController;
2121

2222
/**
2323
* @author Madhura Bhave
2424
*/
25-
public class OtherInvalidConfiguration {
25+
public class OtherCombinedConfiguration {
2626

2727
@RestController
2828
@ConfigurationProperties(prefix = "c")

0 commit comments

Comments
 (0)