詳解spring項目中如何動態(tài)刷新bean
前言
前陣子和朋友聊天,他手頭上有個spring單體項目,每次數(shù)據庫配置變更,他都要重啟項目,讓配置生效。他就想說有沒有什么辦法,不重啟項目,又可以讓配置生效。當時我就跟他說,可以用配置中心,他的意思是因為是維護類項目,不想再額外引入一個配置中心,增加運維成本。后邊跟他討論了一個方案,可以實現(xiàn)一個監(jiān)聽配置文件變化的程序,當監(jiān)聽到文件變化,進行相應的變更操作。具體流程如下
在這些步驟,比較麻煩就是如何動態(tài)刷新bean,因為朋友是spring項目,今天就來聊下在spring項目中如何實現(xiàn)bean的動態(tài)刷新
實現(xiàn)思路
了解spring的朋友,應該知道spring的單例bean是緩存在singletonObjects這個map里面,所以可以通過變更singletonObjects來實現(xiàn)bean的刷新。
我們可以通過調用removeSingleton和addSingleton這兩個方法來實現(xiàn),但是這種實現(xiàn)方式的缺點就是會改變bean的生命周期,會導致原來的一些增強功能失效,比如AOP。
但spring作為一個極其優(yōu)秀的框架,他提供了讓我們自己管理bean的擴展點。這個擴展點就是通過指定scope,來達到自己管理bean的效果
實現(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注冊
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; } }
以上步驟就是實現(xiàn)自定義scope管理bean的過程,下面我們以一個配置變更實現(xiàn)bean刷新例子,來演示以上步驟
示例
1、創(chuàng)建屬性配置文件
在項目src/main/rescoures目錄下創(chuàng)建屬性配置文件config/config.properties
并填入測試內容
test: name: zhangsan2222
2、將config.yml裝載進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、實現(xiàn)配置監(jiān)聽
注: 利用hutool的WatchMonitor或者apache common io的文件監(jiān)聽即可實現(xiàn)
以apache common io為例
a、 業(yè)務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、 啟動文件監(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(); //輪詢間隔時間 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)聽文件變化,并實現(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、測試
a、 編寫controller并將controller scope設置為我們自定義的scope
@RestController @RequestMapping("test") @RefreshBeanScope public class TestController { @Value("${test.name: }") private String name; @GetMapping("print") public String print(){ return name; } }
原來的test.name內容如下
test:
name: zhangsan2222
我們通過瀏覽器訪問
b、 此時我們不重啟服務器,并將test.name改為如下
test:
name: zhangsan3333
此時發(fā)現(xiàn)控制臺會輸出我們的日志信息
通過瀏覽器再訪問
發(fā)現(xiàn)內容已經發(fā)生變化
附錄:自定義scope方法觸發(fā)時機
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ā)時機就是在調用getBean時觸發(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ā)時機實在調用destroyScopedBean方法
總結
如果對spring cloud RefreshScope有研究的話,就會發(fā)現(xiàn)上述的實現(xiàn)方式,就是RefreshScope的粗糙版本實現(xiàn)
以上就是詳解spring項目中如何動態(tài)刷新bean的詳細內容,更多關于spring動態(tài)刷新bean的資料請關注腳本之家其它相關文章!
相關文章
基于SpringBoot+Mybatis實現(xiàn)Mysql分表
這篇文章主要為大家詳細介紹了基于SpringBoot+Mybatis實現(xiàn)Mysql分表的相關知識,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2025-04-04@Autowired注解注入的xxxMapper報錯問題及解決
這篇文章主要介紹了@Autowired注解注入的xxxMapper報錯問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Spring?cloud?OpenFeign中動態(tài)URl、動態(tài)傳遞接口地址代碼示例
openFeign是作為微服務之間調用的解決方案,每個微服務項目是必不可少的,下面這篇文章主要給大家介紹了關于Spring?cloud?OpenFeign中動態(tài)URl、動態(tài)傳遞接口地址的相關資料,需要的朋友可以參考下2024-02-02Java 1.8使用數(shù)組實現(xiàn)循環(huán)隊列
這篇文章主要為大家詳細介紹了Java 1.8使用數(shù)組實現(xiàn)循環(huán)隊列,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-10-10