Recreate proxied @ConfigurationProperties beans on rebind#1662
Recreate proxied @ConfigurationProperties beans on rebind#1662seonghyeoklee wants to merge 3 commits intospring-cloud:mainfrom
Conversation
|
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>
f8fab9b to
49a9ec2
Compare
|
Hi @ryanjbaxter, I've updated the commit with |
|
Can you merge in the latest changes from main so the build will pass? |
There was a problem hiding this comment.
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
ConfigurationPropertiesRebinderto 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.
| if (proxied && bean instanceof Advised advised) { | ||
| Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(target.getClass()); | ||
| advised.setTargetSource(new org.springframework.aop.target.SingletonTargetSource(freshBean)); | ||
| } |
There was a problem hiding this comment.
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.
| 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 { |
There was a problem hiding this comment.
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).
| 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); |
|
Hi @ryanjbaxter, I've addressed the review feedback in c73c01c:
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>
c73c01c to
2daa617
Compare
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
| 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)) { |
Summary
When a
@ConfigurationPropertiesbean is wrapped in an AOP proxy and properties are removed from theEnvironment, callingrebind()previously left field initializers unrestored (they becamenull/0/falseinstead of their declared default values).This change modifies
ConfigurationPropertiesRebinder.rebind()so that when the bean is an AOP proxy, a fresh instance is created viacreateBean()and the proxy'sTargetSourceis 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 instanceConfigurationPropertiesRebinderFieldInitializerIntegrationTeststo verify field initializer restoration