Skip to content

Commit 49a9ec2

Browse files
committed
Recreate proxied @ConfigurationProperties beans on rebind
When a @ConfigurationProperties bean is wrapped in an AOP proxy, recreate the target instance via createBean() instead of re-initializing the existing one. This ensures field initializers are restored when properties are removed from the Environment. Fixes gh-1616 Signed-off-by: seonghyeoklee <dltjdgur327@gmail.com>
1 parent 4451900 commit 49a9ec2

2 files changed

Lines changed: 178 additions & 5 deletions

File tree

spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Set;
2222
import java.util.concurrent.ConcurrentHashMap;
2323

24+
import org.springframework.aop.framework.Advised;
2425
import org.springframework.aop.scope.ScopedProxyUtils;
2526
import org.springframework.aop.support.AopUtils;
2627
import org.springframework.beans.BeansException;
@@ -125,18 +126,28 @@ public boolean rebind(Class type) {
125126
private boolean rebind(String name, ApplicationContext appContext) {
126127
try {
127128
Object bean = appContext.getBean(name);
129+
Object target = bean;
130+
boolean proxied = false;
128131
if (AopUtils.isAopProxy(bean)) {
129-
bean = ProxyUtils.getTargetObject(bean);
132+
target = ProxyUtils.getTargetObject(bean);
133+
proxied = true;
130134
}
131-
if (bean != null) {
135+
if (target != null) {
132136
// TODO: determine a more general approach to fix this.
133137
// see
134138
// https://github.com/spring-cloud/spring-cloud-commons/issues/571
135-
if (getNeverRefreshable().contains(bean.getClass().getName()) || getNeverRefreshable().contains(name)) {
139+
if (getNeverRefreshable().contains(target.getClass().getName())
140+
|| getNeverRefreshable().contains(name)) {
136141
return false; // ignore
137142
}
138-
appContext.getAutowireCapableBeanFactory().destroyBean(bean);
139-
appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
143+
appContext.getAutowireCapableBeanFactory().destroyBean(target);
144+
if (proxied && bean instanceof Advised advised) {
145+
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
146+
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
147+
}
148+
else {
149+
appContext.getAutowireCapableBeanFactory().initializeBean(target, name);
150+
}
140151
return true;
141152
}
142153
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.context.properties;
18+
19+
import org.aspectj.lang.annotation.Aspect;
20+
import org.aspectj.lang.annotation.Before;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
25+
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
26+
import org.springframework.boot.context.properties.ConfigurationProperties;
27+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
28+
import org.springframework.boot.test.context.SpringBootTest;
29+
import org.springframework.boot.test.util.TestPropertyValues;
30+
import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration;
31+
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
32+
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinderFieldInitializerIntegrationTests.TestConfiguration;
33+
import org.springframework.context.ApplicationContext;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.context.annotation.Import;
37+
import org.springframework.core.env.ConfigurableEnvironment;
38+
import org.springframework.core.env.MutablePropertySources;
39+
import org.springframework.test.annotation.DirtiesContext;
40+
41+
import static org.assertj.core.api.BDDAssertions.then;
42+
43+
/**
44+
* Tests that field initializers in {@code @ConfigurationProperties} beans are restored
45+
* when properties are removed and the bean is rebound via a proxy.
46+
*
47+
* @see <a href=
48+
* "https://github.com/spring-cloud/spring-cloud-commons/issues/1616">gh-1616</a>
49+
*/
50+
@SpringBootTest(classes = TestConfiguration.class, properties = "my.name=overridden")
51+
public class ConfigurationPropertiesRebinderFieldInitializerIntegrationTests {
52+
53+
@Autowired
54+
private TestProperties properties;
55+
56+
@Autowired
57+
private ConfigurationPropertiesRebinder rebinder;
58+
59+
@Autowired
60+
private ConfigurableEnvironment environment;
61+
62+
@Test
63+
@DirtiesContext
64+
public void fieldInitializerRestoredAfterPropertyRemoval() {
65+
// Initially the property overrides the field initializer
66+
then(this.properties.getName()).isEqualTo("overridden");
67+
then(this.properties.getTimeout()).isEqualTo(30);
68+
69+
// Override timeout as well
70+
TestPropertyValues.of("my.timeout=60").applyTo(this.environment);
71+
this.rebinder.rebind();
72+
then(this.properties.getTimeout()).isEqualTo(60);
73+
74+
// Remove all property sources that contain our overrides
75+
MutablePropertySources sources = this.environment.getPropertySources();
76+
sources.forEach(ps -> {
77+
if (ps.containsProperty("my.name") || ps.containsProperty("my.timeout")) {
78+
sources.remove(ps.getName());
79+
}
80+
});
81+
82+
this.rebinder.rebind();
83+
84+
// Field initializers should be restored
85+
then(this.properties.getName()).isEqualTo("default-name");
86+
then(this.properties.getTimeout()).isEqualTo(30);
87+
}
88+
89+
@Test
90+
@DirtiesContext
91+
public void rebindStillWorksWithNewValues() {
92+
then(this.properties.getName()).isEqualTo("overridden");
93+
94+
TestPropertyValues.of("my.name=updated").applyTo(this.environment);
95+
this.rebinder.rebind();
96+
97+
then(this.properties.getName()).isEqualTo("updated");
98+
}
99+
100+
@Configuration(proxyBeanMethods = false)
101+
@EnableConfigurationProperties
102+
@Import({ TestInterceptor.class, RefreshConfiguration.RebinderConfiguration.class,
103+
PropertyPlaceholderAutoConfiguration.class, AopAutoConfiguration.class })
104+
protected static class TestConfiguration {
105+
106+
@Bean
107+
protected TestProperties testProperties() {
108+
return new TestProperties();
109+
}
110+
111+
}
112+
113+
@Aspect
114+
protected static class TestInterceptor {
115+
116+
@Before("execution(* *..TestProperties.*(..))")
117+
public void before() {
118+
// Triggers AOP proxy creation for TestProperties
119+
}
120+
121+
}
122+
123+
// Hack out a protected inner class for testing
124+
protected static class RefreshConfiguration extends RefreshAutoConfiguration {
125+
126+
@Configuration(proxyBeanMethods = false)
127+
protected static class RebinderConfiguration extends ConfigurationPropertiesRebinderAutoConfiguration {
128+
129+
public RebinderConfiguration(ApplicationContext context) {
130+
super(context);
131+
}
132+
133+
}
134+
135+
}
136+
137+
@ConfigurationProperties("my")
138+
protected static class TestProperties {
139+
140+
private String name = "default-name";
141+
142+
private int timeout = 30;
143+
144+
public String getName() {
145+
return this.name;
146+
}
147+
148+
public void setName(String name) {
149+
this.name = name;
150+
}
151+
152+
public int getTimeout() {
153+
return this.timeout;
154+
}
155+
156+
public void setTimeout(int timeout) {
157+
this.timeout = timeout;
158+
}
159+
160+
}
161+
162+
}

0 commit comments

Comments
 (0)