Skip to content

Commit af850a8

Browse files
committed
Merge pull request #42073 from nosan
* pr/42073: Polish "Add consistent scope support for ConfigurationProperties beans" Add consistent scope support for ConfigurationProperties beans Closes gh-42073
2 parents be47355 + 23d76a0 commit af850a8

File tree

6 files changed

+161
-51
lines changed

6 files changed

+161
-51
lines changed

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@
1616

1717
package org.springframework.boot.context.properties;
1818

19+
import org.springframework.aop.scope.ScopedProxyUtils;
1920
import org.springframework.beans.factory.BeanFactory;
2021
import org.springframework.beans.factory.ListableBeanFactory;
21-
import org.springframework.beans.factory.config.BeanDefinition;
22+
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
23+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
24+
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
2225
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
23-
import org.springframework.beans.factory.support.RootBeanDefinition;
26+
import org.springframework.beans.factory.support.GenericBeanDefinition;
2427
import org.springframework.boot.context.properties.bind.BindMethod;
28+
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
29+
import org.springframework.context.annotation.ScopeMetadata;
30+
import org.springframework.context.annotation.ScopeMetadataResolver;
31+
import org.springframework.context.annotation.ScopedProxyMode;
2532
import org.springframework.core.annotation.MergedAnnotation;
2633
import org.springframework.core.annotation.MergedAnnotations;
2734
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
@@ -38,6 +45,8 @@
3845
*/
3946
final class ConfigurationPropertiesBeanRegistrar {
4047

48+
private static final ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
49+
4150
private final BeanDefinitionRegistry registry;
4251

4352
private final BeanFactory beanFactory;
@@ -75,17 +84,30 @@ private void registerBeanDefinition(String beanName, Class<?> type,
7584
MergedAnnotation<ConfigurationProperties> annotation) {
7685
Assert.state(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName()
7786
+ " annotation found on '" + type.getName() + "'.");
78-
this.registry.registerBeanDefinition(beanName, createBeanDefinition(beanName, type));
87+
BeanDefinitionReaderUtils.registerBeanDefinition(createBeanDefinition(beanName, type), this.registry);
7988
}
8089

81-
private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
90+
private BeanDefinitionHolder createBeanDefinition(String beanName, Class<?> type) {
91+
GenericBeanDefinition definition = new AnnotatedGenericBeanDefinition(type);
8292
BindMethod bindMethod = ConfigurationPropertiesBean.deduceBindMethod(type);
83-
RootBeanDefinition definition = new RootBeanDefinition(type);
8493
BindMethodAttribute.set(definition, bindMethod);
8594
if (bindMethod == BindMethod.VALUE_OBJECT) {
8695
definition.setInstanceSupplier(() -> ConstructorBound.from(this.beanFactory, beanName, type));
8796
}
88-
return definition;
97+
ScopeMetadata metadata = scopeMetadataResolver.resolveScopeMetadata(definition);
98+
definition.setScope(metadata.getScopeName());
99+
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definition, beanName);
100+
return applyScopedProxyMode(metadata, definitionHolder, this.registry);
101+
}
102+
103+
static BeanDefinitionHolder applyScopedProxyMode(ScopeMetadata metadata, BeanDefinitionHolder definition,
104+
BeanDefinitionRegistry registry) {
105+
ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
106+
if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
107+
return definition;
108+
}
109+
boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
110+
return ScopedProxyUtils.createScopedProxy(definition, registry, proxyTargetClass);
89111
}
90112

91113
}

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

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,12 +20,14 @@
2020

2121
import org.junit.jupiter.api.Test;
2222

23+
import org.springframework.aop.scope.ScopedProxyFactoryBean;
2324
import org.springframework.beans.factory.config.BeanDefinition;
2425
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2526
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2627
import org.springframework.beans.factory.support.GenericBeanDefinition;
27-
import org.springframework.beans.factory.support.RootBeanDefinition;
2828
import org.springframework.boot.context.properties.bind.BindMethod;
29+
import org.springframework.context.annotation.Scope;
30+
import org.springframework.context.annotation.ScopedProxyMode;
2931

3032
import static org.assertj.core.api.Assertions.assertThat;
3133
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -75,20 +77,53 @@ void registerWhenValueObjectRegistersValueObjectBeanDefinition() {
7577
String beanName = "valuecp-" + ValueObjectConfigurationProperties.class.getName();
7678
this.registrar.register(ValueObjectConfigurationProperties.class);
7779
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
78-
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
80+
assertThat(definition).satisfies(hasBindMethodAttribute(BindMethod.VALUE_OBJECT));
7981
}
8082

8183
@Test
8284
void registerWhenNotValueObjectRegistersRootBeanDefinitionWithJavaBeanBindMethod() {
8385
String beanName = MultiConstructorBeanConfigurationProperties.class.getName();
8486
this.registrar.register(MultiConstructorBeanConfigurationProperties.class);
8587
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
86-
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
88+
assertThat(definition).satisfies(hasBindMethodAttribute(BindMethod.JAVA_BEAN));
8789
}
8890

89-
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
91+
@Test
92+
void registerWhenNoScopeUsesSingleton() {
93+
String beanName = "beancp-" + BeanConfigurationProperties.class.getName();
94+
this.registrar.register(BeanConfigurationProperties.class);
95+
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
96+
assertThat(definition).isNotNull();
97+
assertThat(definition.getScope()).isEqualTo(BeanDefinition.SCOPE_SINGLETON);
98+
}
99+
100+
@Test
101+
void registerScopedBeanDefinition() {
102+
String beanName = "beancp-" + ScopedBeanConfigurationProperties.class.getName();
103+
this.registrar.register(ScopedBeanConfigurationProperties.class);
104+
BeanDefinition beanDefinition = this.registry.getBeanDefinition(beanName);
105+
assertThat(beanDefinition).isNotNull();
106+
assertThat(beanDefinition.getBeanClassName()).isEqualTo(ScopedBeanConfigurationProperties.class.getName());
107+
assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
108+
}
109+
110+
@Test
111+
void registerScopedBeanDefinitionWithProxyMode() {
112+
String beanName = "beancp-" + ProxyScopedBeanConfigurationProperties.class.getName();
113+
this.registrar.register(ProxyScopedBeanConfigurationProperties.class);
114+
BeanDefinition proxiedBeanDefinition = this.registry.getBeanDefinition(beanName);
115+
assertThat(proxiedBeanDefinition).isNotNull();
116+
assertThat(proxiedBeanDefinition.getBeanClassName()).isEqualTo(ScopedProxyFactoryBean.class.getName());
117+
String targetBeanName = (String) proxiedBeanDefinition.getPropertyValues().get("targetBeanName");
118+
assertThat(targetBeanName).isNotNull();
119+
BeanDefinition beanDefinition = this.registry.getBeanDefinition(targetBeanName);
120+
assertThat(beanDefinition).isNotNull();
121+
assertThat(beanDefinition.getBeanClassName()).isEqualTo(ProxyScopedBeanConfigurationProperties.class.getName());
122+
assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
123+
}
124+
125+
private Consumer<BeanDefinition> hasBindMethodAttribute(BindMethod bindMethod) {
90126
return (definition) -> {
91-
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
92127
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
93128
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
94129
};
@@ -99,6 +134,18 @@ static class BeanConfigurationProperties {
99134

100135
}
101136

137+
@ConfigurationProperties(prefix = "beancp")
138+
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
139+
static class ScopedBeanConfigurationProperties {
140+
141+
}
142+
143+
@ConfigurationProperties(prefix = "beancp")
144+
@Scope(scopeName = BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
145+
static class ProxyScopedBeanConfigurationProperties {
146+
147+
}
148+
102149
static class NoAnnotationConfigurationProperties {
103150

104151
}

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,13 +17,13 @@
1717
package org.springframework.boot.context.properties;
1818

1919
import java.io.IOException;
20+
import java.util.Locale;
2021
import java.util.function.Consumer;
2122

2223
import org.junit.jupiter.api.Test;
2324

2425
import org.springframework.beans.factory.config.BeanDefinition;
2526
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
26-
import org.springframework.beans.factory.support.RootBeanDefinition;
2727
import org.springframework.boot.context.properties.bind.BindMethod;
2828
import org.springframework.boot.context.properties.scan.combined.c.CombinedConfiguration;
2929
import org.springframework.boot.context.properties.scan.combined.d.OtherCombinedConfiguration;
@@ -56,9 +56,9 @@ void registerBeanDefinitionsShouldScanForConfigurationProperties() throws IOExce
5656
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties");
5757
BeanDefinition barDefinition = this.beanFactory.getBeanDefinition(
5858
"bar-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$BarProperties");
59-
assertThat(bingDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
60-
assertThat(fooDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
61-
assertThat(barDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
59+
assertThat(bingDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
60+
assertThat(fooDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
61+
assertThat(barDefinition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
6262
}
6363

6464
@Test
@@ -67,9 +67,12 @@ void scanWhenBeanDefinitionExistsShouldSkip() throws IOException {
6767
beanFactory.setAllowBeanDefinitionOverriding(false);
6868
this.registrar.registerBeanDefinitions(
6969
getAnnotationMetadata(ConfigurationPropertiesScanConfiguration.TestConfiguration.class), beanFactory);
70-
BeanDefinition fooDefinition = beanFactory.getBeanDefinition(
71-
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties");
72-
assertThat(fooDefinition).isExactlyInstanceOf(RootBeanDefinition.class);
70+
assertThat(beanFactory.containsBeanDefinition(
71+
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties"))
72+
.isTrue();
73+
assertThat(beanFactory.getBeanDefinitionNames())
74+
.filteredOn((name) -> name.toLowerCase(Locale.ENGLISH).contains("fooproperties"))
75+
.hasSize(1);
7376
}
7477

7578
@Test
@@ -88,11 +91,11 @@ void scanWhenBasePackagesAndBasePackageClassesProvidedShouldUseThat() throws IOE
8891
"b.first-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BFirstProperties");
8992
BeanDefinition bSecondDefinition = beanFactory.getBeanDefinition(
9093
"b.second-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BSecondProperties");
91-
assertThat(aDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
94+
assertThat(aDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
9295
// Constructor injection
93-
assertThat(bFirstDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
96+
assertThat(bFirstDefinition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
9497
// Post-processing injection
95-
assertThat(bSecondDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
98+
assertThat(bSecondDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
9699
}
97100

98101
@Test
@@ -112,9 +115,8 @@ void scanWhenOtherComponentAnnotationPresentShouldSkipType() throws IOException
112115
assertThat(beanFactory.getBeanDefinitionCount()).isZero();
113116
}
114117

115-
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
118+
private Consumer<BeanDefinition> hasBindMethod(BindMethod bindMethod) {
116119
return (definition) -> {
117-
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
118120
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
119121
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
120122
};

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.beans.factory.ObjectProvider;
4949
import org.springframework.beans.factory.annotation.Autowired;
5050
import org.springframework.beans.factory.annotation.Value;
51+
import org.springframework.beans.factory.config.BeanDefinition;
5152
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
5253
import org.springframework.beans.factory.support.AbstractBeanDefinition;
5354
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@@ -584,13 +585,21 @@ void loadWhenHasUnboundElementsFromSystemEnvironmentShouldNotThrowException() {
584585
}
585586

586587
@Test
587-
void loadShouldSupportRebindableConfigurationProperties() {
588-
// gh-9160
588+
void loadShouldSupportRebindableConfigurationPropertiesRegisteredAsBean() {
589+
testRebindableConfigurationProperties(PrototypePropertiesBeanConfiguration.class);
590+
}
591+
592+
@Test
593+
void loadShouldSupportRebindableConfigurationPropertiesRegisteredUsingRegistrar() {
594+
testRebindableConfigurationProperties(PrototypePropertiesRegistrarConfiguration.class);
595+
}
596+
597+
void testRebindableConfigurationProperties(Class<?> configurationClass) {
589598
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
590599
Map<String, Object> source = new LinkedHashMap<>();
591600
source.put("example.one", "foo");
592601
sources.addFirst(new MapPropertySource("test-source", source));
593-
this.context.register(PrototypePropertiesConfiguration.class);
602+
this.context.register(configurationClass);
594603
this.context.refresh();
595604
PrototypeBean first = this.context.getBean(PrototypeBean.class);
596605
assertThat(first.getOne()).isEqualTo("foo");
@@ -602,12 +611,24 @@ void loadShouldSupportRebindableConfigurationProperties() {
602611
}
603612

604613
@Test
605-
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties() {
614+
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationPropertiesRegisteredAsBean() {
615+
testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
616+
PrototypePropertiesBeanConfiguration.class);
617+
}
618+
619+
@Test
620+
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationPropertiesRegisteredUsingRegistrar() {
621+
testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
622+
PrototypePropertiesRegistrarConfiguration.class);
623+
}
624+
625+
void testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
626+
Class<?> configurationClass) {
606627
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
607628
Map<String, Object> source = new LinkedHashMap<>();
608629
source.put("example.one", "foo");
609630
sources.addFirst(new MapPropertySource("test-source", source));
610-
this.context.register(PrototypePropertiesConfiguration.class);
631+
this.context.register(configurationClass);
611632
this.context.register(PropertySourcesPlaceholderConfigurer.class);
612633
this.context.refresh();
613634
PrototypeBean first = this.context.getBean(PrototypeBean.class);
@@ -1495,17 +1516,29 @@ static PropertySourcesPlaceholderConfigurer configurer2() {
14951516

14961517
@Configuration(proxyBeanMethods = false)
14971518
@EnableConfigurationProperties
1498-
static class PrototypePropertiesConfiguration {
1519+
static class PrototypePropertiesBeanConfiguration {
14991520

15001521
@Bean
1501-
@Scope("prototype")
1522+
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
15021523
@ConfigurationProperties("example")
15031524
PrototypeBean prototypeBean() {
15041525
return new PrototypeBean();
15051526
}
15061527

15071528
}
15081529

1530+
@Configuration(proxyBeanMethods = false)
1531+
@EnableConfigurationProperties(PrototypeBeanProperties.class)
1532+
static class PrototypePropertiesRegistrarConfiguration {
1533+
1534+
}
1535+
1536+
@ConfigurationProperties("example")
1537+
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
1538+
static class PrototypeBeanProperties extends PrototypeBean {
1539+
1540+
}
1541+
15091542
@EnableConfigurationProperties
15101543
@ConfigurationProperties(prefix = "test")
15111544
static class PropertiesWithResource {

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@
2323

2424
import org.springframework.beans.factory.config.BeanDefinition;
2525
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
26-
import org.springframework.beans.factory.support.RootBeanDefinition;
2726
import org.springframework.boot.context.properties.bind.BindMethod;
2827
import org.springframework.core.type.AnnotationMetadata;
2928

@@ -57,23 +56,23 @@ void typeWithDefaultConstructorShouldRegisterRootBeanDefinition() {
5756
register(TestConfiguration.class);
5857
BeanDefinition definition = this.beanFactory
5958
.getBeanDefinition("foo-" + getClass().getName() + "$FooProperties");
60-
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
59+
assertThat(definition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
6160
}
6261

6362
@Test
6463
void constructorBoundPropertiesShouldRegisterConfigurationPropertiesBeanDefinition() {
6564
register(TestConfiguration.class);
6665
BeanDefinition definition = this.beanFactory
6766
.getBeanDefinition("bar-" + getClass().getName() + "$BarProperties");
68-
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
67+
assertThat(definition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
6968
}
7069

7170
@Test
7271
void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() {
7372
register(TestConfiguration.class);
7473
BeanDefinition definition = this.beanFactory
7574
.getBeanDefinition("bing-" + getClass().getName() + "$BingProperties");
76-
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
75+
assertThat(definition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
7776
}
7877

7978
@Test
@@ -99,9 +98,8 @@ void registrationWithNoTypeShouldNotRegisterAnything() {
9998
}
10099
}
101100

102-
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
101+
private Consumer<BeanDefinition> hasBindMethod(BindMethod bindMethod) {
103102
return (definition) -> {
104-
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
105103
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
106104
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
107105
};

0 commit comments

Comments
 (0)