Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
Expand Down Expand Up @@ -125,18 +126,38 @@ public boolean rebind(Class type) {
private boolean rebind(String name, ApplicationContext appContext) {
try {
Object bean = appContext.getBean(name);
Object target = bean;
boolean proxied = false;
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
target = ProxyUtils.getTargetObject(bean);
proxied = true;
}
if (bean != null) {
if (target != null) {
// TODO: determine a more general approach to fix this.
// see
// https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName()) || getNeverRefreshable().contains(name)) {
if (getNeverRefreshable().contains(target.getClass().getName())
|| getNeverRefreshable().contains(name)) {
return false; // ignore
}
appContext.getAutowireCapableBeanFactory().destroyBean(bean);
appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
if (proxied && bean instanceof Advised advised) {
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new proxy branch assumes the proxy wraps a concrete target instance. If ProxyUtils.getTargetObject(bean) returns the proxy itself (e.g., proxies backed by a TargetSource that returns null, such as interface proxies created via ProxyFactory/EmptyTargetSource), then target.getClass() will be the proxy class and createBean(target.getClass()) will fail. Also, unconditionally replacing the proxy TargetSource can break non-singleton proxies (e.g., other scoped proxies). Consider restricting this path to proxies backed by a SingletonTargetSource (and/or only when target != bean and AopUtils.isAopProxy(target) is false), otherwise fall back to the existing destroy/initialize behavior.

Suggested change
if (proxied && bean instanceof Advised advised) {
if (proxied && bean instanceof Advised advised
&& advised.getTargetSource() instanceof org.springframework.aop.target.SingletonTargetSource
&& target != bean && !AopUtils.isAopProxy(target)) {

Copilot uses AI. Check for mistakes.
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
if (AopUtils.isAopProxy(freshBean)) {
freshBean = ProxyUtils.getTargetObject(freshBean);
}
try {
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
}
catch (Exception ex) {
appContext.getAutowireCapableBeanFactory().destroyBean(freshBean);
throw ex;
}
appContext.getAutowireCapableBeanFactory().destroyBean(target);
}
Comment on lines +143 to +156
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createBean(target.getClass()) will run the full BeanPostProcessor chain, including auto-proxying. In an AOP-enabled context this can return another AOP proxy, and setting that proxy as the target of the existing proxy can lead to nested proxies (e.g., advice executing twice) and unexpected behavior. Consider unwrapping freshBean to its ultimate target (or otherwise ensuring the replacement target is not itself proxied) before wrapping it in the SingletonTargetSource.

Copilot uses AI. Check for mistakes.
else {
appContext.getAutowireCapableBeanFactory().destroyBean(target);
appContext.getAutowireCapableBeanFactory().initializeBean(target, name);
}
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2012-present 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
*
* https://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.cloud.context.properties;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinderFieldInitializerIntegrationTests.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.test.annotation.DirtiesContext;

import static org.assertj.core.api.BDDAssertions.then;

/**
* Tests that field initializers in {@code @ConfigurationProperties} beans are restored
* when properties are removed and the bean is rebound via a proxy.
*
* @see <a href=
* "https://github.com/spring-cloud/spring-cloud-commons/issues/1616">gh-1616</a>
*/
@SpringBootTest(classes = TestConfiguration.class, properties = "my.name=overridden")
public class ConfigurationPropertiesRebinderFieldInitializerIntegrationTests {

@Autowired
private TestProperties properties;

@Autowired
private ConfigurationPropertiesRebinder rebinder;

@Autowired
private ConfigurableEnvironment environment;

@Test
@DirtiesContext
public void fieldInitializerRestoredAfterPropertyRemoval() {
// Initially the property overrides the field initializer
then(this.properties.getName()).isEqualTo("overridden");
then(this.properties.getTimeout()).isEqualTo(30);

// Override timeout as well
TestPropertyValues.of("my.timeout=60").applyTo(this.environment);
this.rebinder.rebind();
then(this.properties.getTimeout()).isEqualTo(60);

// Remove all property sources that contain our overrides
MutablePropertySources sources = this.environment.getPropertySources();
sources.forEach(ps -> {
if (ps.containsProperty("my.name") || ps.containsProperty("my.timeout")) {
sources.remove(ps.getName());
}
});

this.rebinder.rebind();

// Field initializers should be restored
then(this.properties.getName()).isEqualTo("default-name");
then(this.properties.getTimeout()).isEqualTo(30);
}

@Test
@DirtiesContext
public void rebindStillWorksWithNewValues() {
then(this.properties.getName()).isEqualTo("overridden");

TestPropertyValues.of("my.name=updated").applyTo(this.environment);
this.rebinder.rebind();

then(this.properties.getName()).isEqualTo("updated");
}

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@Import({ TestInterceptor.class, RefreshConfiguration.RebinderConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, AopAutoConfiguration.class })
protected static class TestConfiguration {

@Bean
protected TestProperties testProperties() {
return new TestProperties();
}

}

@Aspect
protected static class TestInterceptor {

@Before("execution(* *..TestProperties.*(..))")
public void before() {
// Triggers AOP proxy creation for TestProperties
}

}

// Hack out a protected inner class for testing
protected static class RefreshConfiguration extends RefreshAutoConfiguration {

@Configuration(proxyBeanMethods = false)
protected static class RebinderConfiguration extends ConfigurationPropertiesRebinderAutoConfiguration {

public RebinderConfiguration(ApplicationContext context) {
super(context);
}

}

}

@ConfigurationProperties("my")
protected static class TestProperties {

private String name = "default-name";

private int timeout = 30;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public int getTimeout() {
return this.timeout;
}

public void setTimeout(int timeout) {
this.timeout = timeout;
}

}

}
Loading