Skip to content
Open
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 @@ -461,17 +461,46 @@ private void refreshChangedBeans(SpringBeanReloadContext context, Consumer<List<
}
}

private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";

private void resetConfigurationBeanDefinition(Set<String> configurationBeansToReload) {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
if (!beanFactory.containsBeanDefinition(beanName)) {
// already removed, e.g. a scoped proxy
continue;
}
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
String factoryBeanName = beanDefinition.getFactoryBeanName();
if (factoryBeanName != null && configurationBeansToReload.contains(factoryBeanName)) {
LOGGER.debug("the bean '{}' will be recreating because the factory bean '{}' is changed", beanName, factoryBeanName);
beanFactory.removeBeanDefinition(beanName);
removeScopedProxyDefinition(beanName);
}
}
}

/**
* A scoped proxy @Bean (e.g. @SessionScope) is registered as "scopedTarget.name" with the factory method and as
* "name" for the proxy. If only the target is removed, the proxy definition stays and the @Configuration class is
* not processed for this @Bean again, so "scopedTarget.name" is missing. Remove the proxy too.
*/
private void removeScopedProxyDefinition(String targetBeanName) {
// ScopedProxyUtils.TARGET_NAME_PREFIX, spring-aop is not a dependency of the plugin
if (!targetBeanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
return;
}
String proxyBeanName = targetBeanName.substring(SCOPED_TARGET_NAME_PREFIX.length());
if (!beanFactory.containsBeanDefinition(proxyBeanName)) {
return;
}
BeanDefinition proxyDefinition = beanFactory.getBeanDefinition(proxyBeanName);
PropertyValue targetBeanNameValue = proxyDefinition.getPropertyValues().getPropertyValue("targetBeanName");
if (targetBeanNameValue != null && targetBeanName.equals(targetBeanNameValue.getValue())) {
LOGGER.debug("the scoped proxy '{}' of bean '{}' will be recreated too", proxyBeanName, targetBeanName);
beanFactory.removeBeanDefinition(proxyBeanName);
}
}

private void refreshNewBean(SpringBeanReloadContext context) {
for (BeanDefinitionHolder beanDefinitionHolder : context.newScanBeanDefinitions) {
BeanDefinitionReaderUtils.registerBeanDefinition(beanDefinitionHolder, beanFactory);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxy;

import org.hotswap.agent.plugin.spring.BaseTestUtil;
import org.hotswap.agent.plugin.spring.ClassSwappingRule;
import org.hotswap.agent.plugin.spring.reload.SpringChangedAgent;
import org.hotswap.agent.plugin.spring.scopedproxybak.ScopedProxyConfigurationBak;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.concurrent.atomic.AtomicReference;

import static org.junit.Assert.assertEquals;

/**
* Change of a @Configuration class with a scoped proxy @Bean (#393).
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ScopedProxyTestConfiguration.class)
public class ScopedProxyBeanTest {

@Rule
public ClassSwappingRule swappingRule = new ClassSwappingRule();

@Autowired
private AbstractApplicationContext applicationContext;

@Before
public void before() {
BaseTestUtil.configMaxReloadTimes();
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
SpringChangedAgent.getInstance(beanFactory);
swappingRule.setBeanFactory(beanFactory);
}

@After
public void after() {
SpringChangedAgent.destroyBeanFactory((DefaultListableBeanFactory) applicationContext.getBeanFactory());
}

@Test
public void changeConfigurationWithScopedProxyBean() throws Exception {
assertEquals("aaaa", applicationContext.getBean(WebUser.class).web());
assertEquals("plain-a", applicationContext.getBean("plainBean"));
// a proxy kept by someone else, it looks up the target by name
WebCls oldProxy = applicationContext.getBean("webCls", WebCls.class);

swappingRule.swapClasses(ScopedProxyConfiguration.class, ScopedProxyConfigurationBak.class, 1);

// the scoped target was "No bean named 'scopedTarget.webCls' available" before
assertEquals("aaaa", applicationContext.getBean(WebUser.class).web());
assertEquals("aaaa", oldProxy.getWeb());
// a new scope (another thread here, a new session in a web application) uses the changed @Bean method
assertEquals("bbbb", inNewThread(() -> applicationContext.getBean(WebUser.class).web()));
assertEquals("plain-b", applicationContext.getBean("plainBean"));

// and back again
swappingRule.swapClasses(ScopedProxyConfiguration.class, ScopedProxyConfiguration.class, 2);
assertEquals("aaaa", applicationContext.getBean(WebUser.class).web());
assertEquals("aaaa", inNewThread(() -> applicationContext.getBean(WebUser.class).web()));
assertEquals("plain-a", applicationContext.getBean("plainBean"));
}

private static String inNewThread(java.util.concurrent.Callable<String> callable) throws Exception {
AtomicReference<Object> result = new AtomicReference<>();
Thread thread = new Thread(() -> {
try {
result.set(callable.call());
} catch (Throwable e) {
result.set(e);
}
});
thread.start();
thread.join();
if (result.get() instanceof Throwable) {
throw new AssertionError("failed in new thread", (Throwable) result.get());
}
return (String) result.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxy;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;

/**
* Like @SessionScope, with a thread scope instead of the session. Without @Qualifier: with it, the context does not
* start with the agent on Spring 5.3, see setCacheBeanMetadata(false) in SpringPlugin and #603.
*/
@Configuration
public class ScopedProxyConfiguration {
@Bean
@Scope(scopeName = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS)
public WebCls webCls() {
WebCls cls = new WebCls();
cls.setWeb("aaaa");
return cls;
}

@Bean
public String plainBean() {
return "plain-a";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxy;

import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.SimpleThreadScope;

@Configuration
@ComponentScan(basePackageClasses = WebUser.class)
public class ScopedProxyTestConfiguration {
@Bean
public static CustomScopeConfigurer threadScope() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new SimpleThreadScope());
return configurer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxy;

public class WebCls {
private String web;

public String getWeb() {
return web;
}

public void setWeb(String web) {
this.web = web;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class WebUser {
@Autowired
private WebCls webCls;

public String web() {
return webCls.getWeb();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2013-2026 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.spring.scopedproxybak;

import org.hotswap.agent.plugin.spring.scopedproxy.WebCls;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;

@Configuration
public class ScopedProxyConfigurationBak {
@Bean
@Scope(scopeName = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS)
public WebCls webCls() {
WebCls cls = new WebCls();
cls.setWeb("bbbb");
return cls;
}

@Bean
public String plainBean() {
return "plain-b";
}
}
Loading