前言
前阵子和敌人聊天,他手头上有个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")@Documentedpublic @interface RefreshBeanScope { /** * @see Scope#proxyMode() * @return proxy mode */ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;}
4、编写自定义scope bean刷新逻辑
@RequiredArgsConstructorpublic 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、 自定义文件变动监听器
@Slf4jpublic 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")@RefreshBeanScopepublic 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的毛糙版本实现
demo链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-bean-refresh