詳解spring項(xiàng)目中如何動(dòng)態(tài)刷新bean
前言
前陣子和朋友聊天,他手頭上有個(gè)spring單體項(xiàng)目,每次數(shù)據(jù)庫配置變更,他都要重啟項(xiàng)目,讓配置生效。他就想說有沒有什么辦法,不重啟項(xiàng)目,又可以讓配置生效。當(dāng)時(shí)我就跟他說,可以用配置中心,他的意思是因?yàn)槭蔷S護(hù)類項(xiàng)目,不想再額外引入一個(gè)配置中心,增加運(yùn)維成本。后邊跟他討論了一個(gè)方案,可以實(shí)現(xiàn)一個(gè)監(jiān)聽配置文件變化的程序,當(dāng)監(jiān)聽到文件變化,進(jìn)行相應(yīng)的變更操作。具體流程如下

在這些步驟,比較麻煩就是如何動(dòng)態(tài)刷新bean,因?yàn)榕笥咽莝pring項(xiàng)目,今天就來聊下在spring項(xiàng)目中如何實(shí)現(xiàn)bean的動(dòng)態(tài)刷新
實(shí)現(xiàn)思路
了解spring的朋友,應(yīng)該知道spring的單例bean是緩存在singletonObjects這個(gè)map里面,所以可以通過變更singletonObjects來實(shí)現(xiàn)bean的刷新。
我們可以通過調(diào)用removeSingleton和addSingleton這兩個(gè)方法來實(shí)現(xiàn),但是這種實(shí)現(xiàn)方式的缺點(diǎn)就是會(huì)改變bean的生命周期,會(huì)導(dǎo)致原來的一些增強(qiáng)功能失效,比如AOP。
但spring作為一個(gè)極其優(yōu)秀的框架,他提供了讓我們自己管理bean的擴(kuò)展點(diǎn)。這個(gè)擴(kuò)展點(diǎn)就是通過指定scope,來達(dá)到自己管理bean的效果
實(shí)現(xiàn)步驟
1、自定義scope
public class RefreshBeanScope implements Scope {
private final Map<String,Object> beanMap = new ConcurrentHashMap<>(256);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if(beanMap.containsKey(name)){
return beanMap.get(name);
}
Object bean = objectFactory.getObject();
beanMap.put(name,bean);
return bean;
}
@Override
public Object remove(String name) {
return beanMap.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}2、自定義scope注冊(cè)
public class RefreshBeanScopeDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(SCOPE_NAME,new RefreshBeanScope());
}
}3、自定義scope注解(可選)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refreshBean")
@Documented
public @interface RefreshBeanScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}4、編寫自定義scope bean刷新邏輯
@RequiredArgsConstructor
public class RefreshBeanScopeHolder implements ApplicationContextAware {
private final DefaultListableBeanFactory beanFactory;
private ApplicationContext applicationContext;
public List<String> refreshBean(){
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
List<String> refreshBeanDefinitionNames = new ArrayList<>();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
if(SCOPE_NAME.equals(beanDefinition.getScope())){
beanFactory.destroyScopedBean(beanDefinitionName);
beanFactory.getBean(beanDefinitionName);
refreshBeanDefinitionNames.add(beanDefinitionName);
applicationContext.publishEvent(new RefreshBeanEvent(beanDefinitionName));
}
}
return Collections.unmodifiableList(refreshBeanDefinitionNames);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}以上步驟就是實(shí)現(xiàn)自定義scope管理bean的過程,下面我們以一個(gè)配置變更實(shí)現(xiàn)bean刷新例子,來演示以上步驟
示例
1、創(chuàng)建屬性配置文件
在項(xiàng)目src/main/rescoures目錄下創(chuàng)建屬性配置文件config/config.properties

并填入測(cè)試內(nèi)容
test: name: zhangsan2222
2、將config.yml裝載進(jìn)spring
public static void setConfig() {
String configLocation = getProjectPath() + "/src/main/resources/config/config.yml";
System.setProperty("spring.config.additional-location",configLocation);
}
public static String getProjectPath() {
String basePath = ConfigFileUtil.class.getResource("").getPath();
return basePath.substring(0, basePath.indexOf("/target"));
}3、實(shí)現(xiàn)配置監(jiān)聽
注: 利用hutool的WatchMonitor或者apache common io的文件監(jiān)聽即可實(shí)現(xiàn)
以apache common io為例
a、 業(yè)務(wù)pom文件引入common-io gav
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>b、 自定義文件變化監(jiān)聽器
@Slf4j
public class ConfigPropertyFileAlterationListener extends FileAlterationListenerAdaptor {
private ApplicationContext applicationContext;
public ConfigPropertyFileAlterationListener(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void onStart(FileAlterationObserver observer) {
super.onStart(observer);
}
@Override
public void onDirectoryCreate(File directory) {
super.onDirectoryCreate(directory);
}
@Override
public void onDirectoryChange(File directory) {
super.onDirectoryChange(directory);
}
@Override
public void onDirectoryDelete(File directory) {
super.onDirectoryDelete(directory);
}
@Override
public void onFileCreate(File file) {
super.onFileCreate(file);
}
@Override
public void onFileChange(File file) {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>> Monitor PropertyFile with path --> {}",file.getName());
refreshConfig(file);
}
@Override
public void onFileDelete(File file) {
super.onFileDelete(file);
}
@Override
public void onStop(FileAlterationObserver observer) {
super.onStop(observer);
}
}c、 啟動(dòng)文件監(jiān)聽器
@SneakyThrows
private static void monitorPropertyChange(FileMonitor fileMonitor, File file,ApplicationContext context){
if(fileMonitor.isFileScanEnabled()) {
String ext = "." + FilenameUtils.getExtension(file.getName());
String monitorDir = file.getParent();
//輪詢間隔時(shí)間
long interval = TimeUnit.SECONDS.toMillis(fileMonitor.getFileScanInterval());
//創(chuàng)建文件觀察器
FileAlterationObserver observer = new FileAlterationObserver(
monitorDir, FileFilterUtils.and(
FileFilterUtils.fileFileFilter(),
FileFilterUtils.suffixFileFilter(ext)));
observer.addListener(new ConfigPropertyFileAlterationListener(context));
//創(chuàng)建文件變化監(jiān)聽器
FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
//開始監(jiān)聽
monitor.start();
}
}4、監(jiān)聽文件變化,并實(shí)現(xiàn)PropertySource以及bean的刷新
@SneakyThrows
private void refreshConfig(File file){
ConfigurableEnvironment environment = applicationContext.getBean(ConfigurableEnvironment.class);
MutablePropertySources propertySources = environment.getPropertySources();
PropertySourceLoader propertySourceLoader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySourceList = propertySourceLoader.load(file.getAbsolutePath(), applicationContext.getResource("file:"+file.getAbsolutePath()));
for (PropertySource<?> propertySource : propertySources) {
if(propertySource.getName().contains(file.getName())){
propertySources.replace(propertySource.getName(),propertySourceList.get(0));
}
}
RefreshBeanScopeHolder refreshBeanScopeHolder = applicationContext.getBean(RefreshBeanScopeHolder.class);
List<String> strings = refreshBeanScopeHolder.refreshBean();
log.info(">>>>>>>>>>>>>>> refresh Bean :{}",strings);
}5、測(cè)試
a、 編寫controller并將controller scope設(shè)置為我們自定義的scope
@RestController
@RequestMapping("test")
@RefreshBeanScope
public class TestController {
@Value("${test.name: }")
private String name;
@GetMapping("print")
public String print(){
return name;
}
}原來的test.name內(nèi)容如下
test:
name: zhangsan2222
我們通過瀏覽器訪問

b、 此時(shí)我們不重啟服務(wù)器,并將test.name改為如下
test:
name: zhangsan3333
此時(shí)發(fā)現(xiàn)控制臺(tái)會(huì)輸出我們的日志信息

通過瀏覽器再訪問

發(fā)現(xiàn)內(nèi)容已經(jīng)發(fā)生變化
附錄:自定義scope方法觸發(fā)時(shí)機(jī)
1、scope get方法
// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}觸發(fā)時(shí)機(jī)就是在調(diào)用getBean時(shí)觸發(fā)
2、scope remove方法
@Override
public void destroyScopedBean(String beanName) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (mbd.isSingleton() || mbd.isPrototype()) {
throw new IllegalArgumentException(
"Bean name '" + beanName + "' does not correspond to an object in a mutable scope");
}
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope SPI registered for scope name '" + scopeName + "'");
}
Object bean = scope.remove(beanName);
if (bean != null) {
destroyBean(beanName, bean, mbd);
}
}觸發(fā)時(shí)機(jī)實(shí)在調(diào)用destroyScopedBean方法
總結(jié)
如果對(duì)spring cloud RefreshScope有研究的話,就會(huì)發(fā)現(xiàn)上述的實(shí)現(xiàn)方式,就是RefreshScope的粗糙版本實(shí)現(xiàn)
以上就是詳解spring項(xiàng)目中如何動(dòng)態(tài)刷新bean的詳細(xì)內(nèi)容,更多關(guān)于spring動(dòng)態(tài)刷新bean的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot如何通過@Value,@ConfigurationProperties獲取配置
這篇文章主要介紹了springboot如何通過@Value,@ConfigurationProperties獲取配置,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Java編程實(shí)現(xiàn)深度優(yōu)先遍歷與連通分量代碼示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)深度優(yōu)先遍歷與連通分量代碼示例,2017-11-11
出現(xiàn)次數(shù)超過一半(50%)的數(shù)
給出n個(gè)數(shù),需要我們找出出現(xiàn)次數(shù)超過一半的數(shù),下面小編給大家分享下我的實(shí)現(xiàn)思路及關(guān)鍵代碼,感興趣的朋友一起學(xué)習(xí)吧2016-07-07
通過端口1433連接到主機(jī)127.0.0.1的 TCP/IP 連接失敗,錯(cuò)誤:“connect timed out”的解
這篇文章主要介紹了通過端口1433連接到主機(jī)127.0.0.1的 TCP/IP 連接失敗,錯(cuò)誤:“connect timed out”的解決方法,需要的朋友可以參考下2015-08-08
Spring中@RestController注解的使用實(shí)現(xiàn)
@RestController是SpringMVC中用于構(gòu)建RESTful Web服務(wù)的核心注解,結(jié)合@Controller和@ResponseBody功能,自動(dòng)將方法返回值序列化為JSON/XML,感興趣的可以了解一下2025-08-08
Java?Stream比較兩個(gè)List的差異并取出不同的對(duì)象四種方法
今天開發(fā)一個(gè)需求時(shí)要對(duì)A和B兩個(gè)List集合遍歷,并比較出集合A有,而集合B沒有的值,下面這篇文章主要給大家介紹了關(guān)于Java?Stream比較兩個(gè)List的差異并取出不同對(duì)象的四種方法,需要的朋友可以參考下2024-01-01
Spring?WebFlux怎么進(jìn)行異常處理源碼解析
這篇文章主要為大家介紹了Spring?WebFlux怎么進(jìn)行異常處理源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08

