详解spring项目中如何动态刷新bean
作者:linyb极客之路
前言
前阵子和朋友聊天,他手头上有个spring单体项目,每次数据库配置变更,他都要重启项目,让配置生效。他就想说有没有什么办法,不重启项目,又可以让配置生效。当时我就跟他说,可以用配置中心,他的意思是因为是维护类项目,不想再额外引入一个配置中心,增加运维成本。后边跟他讨论了一个方案,可以实现一个监听配置文件变化的程序,当监听到文件变化,进行相应的变更操作。具体流程如下
在这些步骤,比较麻烦就是如何动态刷新bean,因为朋友是spring项目,今天就来聊下在spring项目中如何实现bean的动态刷新
实现思路
了解spring的朋友,应该知道spring的单例bean是缓存在singletonObjects这个map里面,所以可以通过变更singletonObjects来实现bean的刷新。
我们可以通过调用removeSingleton和addSingleton这两个方法来实现,但是这种实现方式的缺点就是会改变bean的生命周期,会导致原来的一些增强功能失效,比如AOP。
但spring作为一个极其优秀的框架,他提供了让我们自己管理bean的扩展点。这个扩展点就是通过指定scope,来达到自己管理bean的效果
实现步骤
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; } }
以上步骤就是实现自定义scope管理bean的过程,下面我们以一个配置变更实现bean刷新例子,来演示以上步骤
示例
1、创建属性配置文件
在项目src/main/rescoures目录下创建属性配置文件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、实现配置监听
注: 利用hutool的WatchMonitor或者apache common io的文件监听即可实现
以apache common io为例
a、 业务pom文件引入common-io gav
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${common-io.version}</version> </dependency>
b、 自定义文件变化监听器
@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、 启动文件监听器
@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()); //创建文件观察器 FileAlterationObserver observer = new FileAlterationObserver( monitorDir, FileFilterUtils.and( FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter(ext))); observer.addListener(new ConfigPropertyFileAlterationListener(context)); //创建文件变化监听器 FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer); //开始监听 monitor.start(); } }
4、监听文件变化,并实现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
此时发现控制台会输出我们的日志信息
通过浏览器再访问
发现内容已经发生变化
附录:自定义scope方法触发时机
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; }
触发时机就是在调用getBean时触发
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); } }
触发时机实在调用destroyScopedBean方法
总结
如果对spring cloud RefreshScope有研究的话,就会发现上述的实现方式,就是RefreshScope的粗糙版本实现
以上就是详解spring项目中如何动态刷新bean的详细内容,更多关于spring动态刷新bean的资料请关注脚本之家其它相关文章!