Skip to content

Recreate proxied @ConfigurationProperties beans on rebind#1662

Open
seonghyeoklee wants to merge 3 commits intospring-cloud:mainfrom
seonghyeoklee:fix/rebind-restore-field-initializers
Open

Recreate proxied @ConfigurationProperties beans on rebind#1662
seonghyeoklee wants to merge 3 commits intospring-cloud:mainfrom
seonghyeoklee:fix/rebind-restore-field-initializers

Conversation

@seonghyeoklee
Copy link
Copy Markdown

Summary

When a @ConfigurationProperties bean is wrapped in an AOP proxy and properties are removed from the Environment, calling rebind() previously left field initializers unrestored (they became null / 0 / false instead of their declared default values).

This change modifies ConfigurationPropertiesRebinder.rebind() so that when the bean is an AOP proxy, a fresh instance is created via createBean() and the proxy's TargetSource is replaced, ensuring field initializers are properly restored.

Non-proxied beans continue to use the existing destroyBean() + initializeBean() behavior for backwards compatibility.

Fixes gh-1616

Changes

  • ConfigurationPropertiesRebinder: detect proxied beans and replace the proxy target with a freshly created instance
  • Added ConfigurationPropertiesRebinderFieldInitializerIntegrationTests to verify field initializer restoration

@ryanjbaxter
Copy link
Copy Markdown
Contributor

Can you sign the commits so the DCO passes?

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 spring-cloudgh-1616

Signed-off-by: seonghyeoklee <dltjdgur327@gmail.com>
@seonghyeoklee seonghyeoklee force-pushed the fix/rebind-restore-field-initializers branch from f8fab9b to 49a9ec2 Compare March 12, 2026 01:10
@seonghyeoklee
Copy link
Copy Markdown
Author

Hi @ryanjbaxter, I've updated the commit with Signed-off-by and the DCO check is now passing. Could you take a look when you get a chance? Thanks!

@ryanjbaxter
Copy link
Copy Markdown
Contributor

Can you merge in the latest changes from main so the build will pass?

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes ConfigurationPropertiesRebinder.rebind() behavior for AOP-proxied @ConfigurationProperties beans so that when properties are removed from the Environment, rebinding restores Java field initializer defaults (instead of leaving primitives/refs at JVM defaults). It does so by recreating the proxied target instance and swapping the proxy’s TargetSource, and adds an integration test to verify the regression.

Changes:

  • Update ConfigurationPropertiesRebinder to detect AOP proxies and replace the proxy’s target with a newly created instance on rebind.
  • Keep existing destroyBean() + initializeBean() behavior for non-proxied beans for compatibility.
  • Add integration tests covering field-initializer restoration after property removal for proxied beans.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java Rebind now recreates and swaps the target instance when the bean is AOP-proxied.
spring-cloud-context/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderFieldInitializerIntegrationTests.java New integration test validating field-initializer defaults are restored after property removal + rebind through a proxy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +147
if (proxied && bean instanceof Advised advised) {
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
}
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.
Comment on lines +143 to +148
appContext.getAutowireCapableBeanFactory().destroyBean(target);
if (proxied && bean instanceof Advised advised) {
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
}
else {
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.

The proxy-target replacement destroys the existing target before creating/switching to the fresh instance. If createBean(...) or setTargetSource(...) throws, the proxy can be left pointing at an already-destroyed target, potentially breaking the application after a failed rebind(). Consider creating the fresh instance first and only swapping/destroying once that succeeds (or ensuring rollback on failure).

Suggested change
appContext.getAutowireCapableBeanFactory().destroyBean(target);
if (proxied && bean instanceof Advised advised) {
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
}
else {
if (proxied && bean instanceof Advised advised) {
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass());
try {
advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean));
}
catch (Exception e) {
appContext.getAutowireCapableBeanFactory().destroyBean(freshBean);
throw e;
}
appContext.getAutowireCapableBeanFactory().destroyBean(target);
}
else {
appContext.getAutowireCapableBeanFactory().destroyBean(target);

Copilot uses AI. Check for mistakes.
@seonghyeoklee
Copy link
Copy Markdown
Author

Hi @ryanjbaxter, I've addressed the review feedback in c73c01c:

  1. Nested proxy prevention: freshBean is now unwrapped via ProxyUtils.getTargetObject() if createBean() returns an AOP proxy, avoiding double-proxied targets.
  2. Error safety: The fresh instance is created before destroying the old target. If setTargetSource() fails, the fresh bean is cleaned up and the original target remains intact.

Tests pass. Would appreciate a review when you get a chance. Thanks!

- Unwrap freshBean if createBean() returns an AOP proxy to avoid
  double-proxied targets where advice executes twice
- Create fresh instance before destroying old target so a failure
  in createBean()/setTargetSource() does not leave the proxy
  pointing at an already-destroyed target
- On setTargetSource failure, destroy freshBean before rethrowing

Signed-off-by: seonghyeoklee <dltjdgur327@gmail.com>
@seonghyeoklee seonghyeoklee force-pushed the fix/rebind-restore-field-initializers branch from c73c01c to 2daa617 Compare April 12, 2026 12:04
@ryanjbaxter ryanjbaxter requested a review from Copilot April 12, 2026 13:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConfigurationProperties initial values not restored on DELETE /actuator/env

5 participants