-
Notifications
You must be signed in to change notification settings - Fork 735
Recreate proxied @ConfigurationProperties beans on rebind #1662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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) { | ||
| 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
|
||
| else { | ||
| appContext.getAutowireCapableBeanFactory().destroyBean(target); | ||
| appContext.getAutowireCapableBeanFactory().initializeBean(target, name); | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
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 aTargetSourcethat returnsnull, such as interface proxies created viaProxyFactory/EmptyTargetSource), thentarget.getClass()will be the proxy class andcreateBean(target.getClass())will fail. Also, unconditionally replacing the proxyTargetSourcecan break non-singleton proxies (e.g., other scoped proxies). Consider restricting this path to proxies backed by aSingletonTargetSource(and/or only whentarget != beanandAopUtils.isAopProxy(target)is false), otherwise fall back to the existing destroy/initialize behavior.