聊聊spring项目中如何动态刷新bean

news2025/1/22 17:44:38

前言

前阵子和朋友聊天,他手头上有个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的粗糙版本实现

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-bean-refresh

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/942909.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AArch64内存模型

概述 本指南介绍了 Armv8‑A 和 Armv9‑A 中的内存属性/特性。首先解释内存的属性从何而来&#xff0c;以及如何将其分配给内存区域。然后介绍不同的属性&#xff0c;以及内存访问顺序的基础知识。 这些信息对于底层&#xff08;例如启动代码或驱动程序&#xff09;开发人员都…

windows上奇怪的dump指向

1. CPP_EXCEPTION_e06d7363_pcl_filters.dll 百度后发现 e06d7363可能是杀毒软件把pcl的依赖库当成病毒&#xff0c;加到白名单即可。 修复未知软件异常错误代码0xe06d7363-回忆主机

Python实现自动关键词提取

随着互联网的发展&#xff0c;越来越多的人喜欢在网络上阅读小说。本文将通过详细示例&#xff0c;向您介绍如何使用Python编写爬虫程序来获取网络小说&#xff0c;并利用自然语言处理技术实现自动文摘和关键词提取功能。 1. 网络小说数据抓取 首先&#xff0c;请确保已安装必…

2023-08-29 LeetCode(带因子的二叉树)

2023-08-29每日一题 一、题目编号 823. 带因子的二叉树二、题目链接 点击跳转到题目位置 三、题目描述 给出一个含有不重复整数元素的数组 arr &#xff0c;每个整数 arr[i] 均大于 1。 用这些整数来构建二叉树&#xff0c;每个整数可以使用任意次数。其中&#xff1a;每…

Android 之 传感器专题 (2) —— 方向传感器

本节引言&#xff1a; 在上一节中我们中我们对传感器的一些基本概念进行了学习&#xff0c;以及学习了使用传感器的套路&#xff0c; 本节给大家带来的传感器是方向传感器的用法&#xff0c;好的&#xff0c;开始本节内容~ 1.三维坐标系的概念&#xff1a; 在Android平台中&a…

60个令人兴奋的ThreeJS网站示例

60个令人兴奋的ThreeJS网站示例 Three.js是一个JavaScript库&#xff0c;它使在Web上创建3D图形比直接使用WebGL容易得多。Three.js是网络上最受欢迎的3D JavaScript库&#xff0c;很容易上手。因此&#xff0c;在这篇文章中&#xff0c;我将展示一些创意网站的例子&#xff0…

数据分析师初级—中级—高级,每个阶段都需要学习什么?

先你需要看下这张图&#xff0c;这是一张数据分析师能力体系图&#xff1a; 通过图片&#xff0c;我们可以比较清晰的看到这三个阶段的数据分析师在各方面能力的差别了&#xff0c;那下面我们就来具体侃侃他们的区别。 初级水平 什么是初学者&#xff1f;如果解析学和数据科…

基于大语言模型知识问答应用落地实践 – 知识库构建(下)

上篇介绍了构建知识库的大体流程和一些优化经验细节&#xff0c;但并没有结合一个具体的场景给出更细节的实战经验以及相关的一些 benchmark 等&#xff0c;所以本文将会切入到一个具体场景进行讨论。 目标场景&#xff1a;对于 PubMed 医疗学术数据中的 1w 篇文章进行知识库构…

Autoware.universe部署04:universe传感器ROS2驱动

文章目录 一、激光雷达驱动二、IMU驱动2.1 上位机配置4.2 IMU校准4.3 安装ROS驱动 三、CAN驱动四、相机驱动4.1 安装驱动4.2 修改相机参数 五、GNSS驱动 本文介绍了 Autoware.universe 各个传感器ROS2驱动&#xff0c;本系列其他文章&#xff1a; Autoware.universe部署01&…

tensorrtx部署yolov5 6.0

文章目录 一. yolov5 v6.0训练模型二.训练好的yolov5模型转tensorrt引擎 一. yolov5 v6.0训练模型 官网下载yolov5 v6.0代码 下载官方预训练好的模型 安装yolov5所需要的库文件&#xff0c;requirements.txt在下载好的yolov5源代码中有 pip install -r C:\Users\10001540…

基于S参数的稳定性分析

目录 1、一些常数的定义2、负载稳定性区域3、结束语 1、一些常数的定义 在基于S参数讨论网络的稳定性之前&#xff0c;首先定义几组常数&#xff0c;如下&#xff1a; 无条件稳定&#xff1a;对于任意的|ГL|<1&#xff0c;|ГS|<1&#xff0c;则一定有|Гin|<1&am…

Linux基础(一)

1.操作系统概念 人与计算机交流的中介 管理和控制计算机中硬件和软件资源 处于上层应用程序和底层硬件之间的软件平台 2.操作系统组成 内核&#xff1a;直接控制管理硬件 内核直接识别计算机二进制语言 解释器&#xff1a;把c c java python等语言解释成二进制&#xff…

闲人闲谈PS之四十六——网络生产全流程

惯例闲话&#xff1a;下半年已开始块行情似乎又是一波大涨&#xff0c;很多朋友委托我介绍PS顾问&#xff0c;很多朋友已经上了能源系统项目&#xff0c;这就造成装备制造的PS又是极度紧缺&#xff0c;rate也还可以&#xff0c;搞的自己也有点心痒痒。这种逆势大涨&#xff0c;…

Python读取Windows注册表的实战代码

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

聚观早报|我国网民规模达10.79亿人;比亚迪冲进人形机器人赛道

【聚观365】8月29日消息 我国网民规模达10.79亿人 比亚迪冲进人形机器人赛道 新岚图FREE上市一周订单突破1.1万辆 SpaceX龙飞船成功与国际空间站对接 三星电子将在德国和日本举办晶圆代工论坛 我国网民规模达10.79亿人 中国互联网络信息中心&#xff08;CNNIC&#xff09…

【2023中国算力大会】《中国综合算力指数(2023年)》出炉,宁夏“资源环境”位列全国第1,“算力”跃入Top10

2023年8月18日-19日&#xff0c;2023中国算力大会在宁夏银川举行&#xff0c;本届大会以“算领新产业潮流 力赋高质量发展”为主题&#xff0c;打造“主题论坛、成果展示、产业推介、先锋引领”四大核心内容&#xff0c;全面展示算力产业发展最新成果&#xff0c;为产业各方搭建…

Android 蓝牙开发(一)

蓝牙简介 蓝牙&#xff08;Bluetooth&#xff09;是一种无线技术标准&#xff0c;能够在短距离内实现设备之间的数据交换和通信。蓝牙技术最初由瑞典爱立信公司于1994年开发&#xff0c;其名称源自丹麦国王哈拉尔布吕特的译名“Harald Bluetooth”&#xff0c;他曾统一了斯堪的…

R语言空气污染数据的地理空间可视化和分析:颗粒物2.5(PM2.5)和空气质量指数(AQI)...

原文链接&#xff1a;http://tecdat.cn/?p23800 由于空气污染对公众健康的不利影响&#xff0c;人们一直非常关注。世界各国的环境部门都通过各种方法&#xff08;例如地面观测网络&#xff09;来监测和评估空气污染问题&#xff08;点击文末“阅读原文”获取完整代码数据&…

Spring Security存在认证绕过漏洞 CVE-2021-22096

文章目录 0.前言1.参考文档2.基础介绍漏洞影响范围&#xff1a;官方说明&#xff1a;修复版本&#xff1a;漏洞利用步骤&#xff1a;修复方式&#xff1a; 3.解决方案 0.前言 背景&#xff1a;项目被扫到Spring Boot 的漏洞&#xff0c;严格的说应该是Spring Security 组件的漏…

前端面试基础面试题——1

总结了一些基础的面试题 如果大家有兴趣的话可以关注留意一下 今后会不断更新一些面试题 1.JavaScript 中的 AJAX 原理及应用。 2.什么是闭包?请简单描述一下闭包的特点与应用场景。 3.请简述 HTTPS 与 HTTP 的区别&#xff0c;如何保证 HTTPS 的安全性? 4.请简述…