SpringBoot应用篇@Value配置自动刷新能力扩展实践| 8月更文挑战

简介: 在我们的日常开发中,使用@Value来绑定配置属于非常常见的基础操作,但是这个配置注入是一次性的,简单来说就是配置一旦赋值,则不会再修改;通常来讲,这个并没有什么问题,基础的 SpringBoot 项目的配置也基本不存在配置变更,如果有使用过 SpringCloudConfig 的小伙伴,会知道@Value可以绑定远程配置,并支持动态刷新接下来本文将通过一个实例来演示下,如何让@Value注解支持配置刷新;本文将涉及到以下知识点

image.png


在我们的日常开发中,使用@Value来绑定配置属于非常常见的基础操作,但是这个配置注入是一次性的,简单来说就是配置一旦赋值,则不会再修改; 通常来讲,这个并没有什么问题,基础的 SpringBoot 项目的配置也基本不存在配置变更,如果有使用过 SpringCloudConfig 的小伙伴,会知道@Value可以绑定远程配置,并支持动态刷新

接下来本文将通过一个实例来演示下,如何让@Value注解支持配置刷新;本文将涉及到以下知识点


  • BeanPostProcessorAdapter + 自定义注解:获取支持自动刷新的配置类
  • MapPropertySource:实现配置动态变更


I. 项目环境



1. 项目依赖


本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个 web 服务用于测试


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
复制代码


II. 配置动态刷新支持



1. 思路介绍


要支持配合的动态刷新,重点在于下面两点


  • 如何修改Environment中的配置源
  • 配置变更之后,如何通知到相关的类同步更新


2. 修改配置


相信很多小伙伴都不会去修改Environment中的数据源,突然冒出一个让我来修改配置源的数据,还是有点懵的,这里推荐之前分享过一篇博文 SpringBoot 基础篇之自定义配置源的使用姿势


当我们知道如何去自定义配置源之后,再来修改数据源,就会有一点思路了


定义一个配置文件application-dynamic.yml

xhh:
  dynamic:
    name: 一灰灰blog
复制代码


然后在主配置文件中使用它

spring:
  profiles:
    active: dynamic
复制代码


使用配置的 java config

@Data
@Component
public class RefreshConfigProperties {
    @Value("${xhh.dynamic.name}")
    private String name;
    @Value("${xhh.dynamic.age:18}")
    private Integer age;
    @Value("hello ${xhh.dynamic.other:test}")
    private String other;
}
复制代码


接下来进入修改配置的正题

@Autowired
ConfigurableEnvironment environment;
// --- 配置修改
String name = "applicationConfig: [classpath:/application-dynamic.yml]";
MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
Map<String, Object> source = propertySource.getSource();
Map<String, Object> map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, value);
environment.getPropertySources().replace(name, new MapPropertySource(name, map));
复制代码


上面的实现中,有几个疑问点


  • name 如何找到的?
  • debug...
  • 配置变更
  • 注意修改配置是新建了一个 Map,然后将旧的配置拷贝到新的 Map,然后再执行替换;并不能直接进行修改,有兴趣的小伙伴可以实测一下为什么


3. 配置同步


上面虽然是实现了配置的修改,但是对于使用@Value注解修饰的变量,已经被赋值了,如何能感知到配置的变更,并同步刷新呢?


这里就又可以拆分两块


  • 找到需要修改的配置
  • 修改事件同步


3.1 找出需要刷新的配置变量


我们这里额外增加了一个注解,用来修饰需要支持动态刷新的场景


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshValue {
}
复制代码


接下来我们就是找出有上面这个注解的类,然后支持这些类中@Value注解绑定的变量动态刷新


关于这个就有很多实现方式了,我们这里选择BeanPostProcessor,bean 创建完毕之后,借助反射来获取@Value绑定的变量,并缓存起来


@Component
public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
    private Map<String, List<FieldPair>> mapper = new HashMap<>();
    private Environment environment;
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        processMetaValue(bean);
        return super.postProcessAfterInstantiation(bean, beanName);
    }
    /**
     * 这里主要的目的就是获取支持动态刷新的配置属性,然后缓存起来
     *
     * @param bean
     */
    private void processMetaValue(Object bean) {
        Class clz = bean.getClass();
        if (!clz.isAnnotationPresent(RefreshValue.class)) {
            return;
        }
        try {
            for (Field field : clz.getDeclaredFields()) {
                if (field.isAnnotationPresent(Value.class)) {
                    Value val = field.getAnnotation(Value.class);
                    List<String> keyList = pickPropertyKey(val.value(), 0);
                    for (String key : keyList) {
                        mapper.computeIfAbsent(key, (k) -> new ArrayList<>())
                                .add(new FieldPair(bean, field, val.value()));
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
    /**
     * 实现一个基础的配置文件参数动态刷新支持
     *
     * @param value
     * @return 提取key列表
     */
    private List<String> pickPropertyKey(String value, int begin) {
        int start = value.indexOf("${", begin) + 2;
        if (start < 2) {
            return new ArrayList<>();
        }
        int middle = value.indexOf(":", start);
        int end = value.indexOf("}", start);
        String key;
        if (middle > 0 && middle < end) {
            // 包含默认值
            key = value.substring(start, middle);
        } else {
            // 不包含默认值
            key = value.substring(start, end);
        }
        List<String> keys = pickPropertyKey(value, end);
        keys.add(key);
        return keys;
    }
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class FieldPair {
        private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",
                ":", true);
        Object bean;
        Field field;
        String value;
        public void updateValue(Environment environment) {
            boolean access = field.isAccessible();
            if (!access) {
                field.setAccessible(true);
            }
            String updateVal = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty);
            try {
                if (field.getType() == String.class) {
                    field.set(bean, updateVal);
                } else {
                    field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            field.setAccessible(access);
        }
    }
}
复制代码


上面的实现虽然有点长,但是核心逻辑就下面节点


  • processMetaValue():
  • 通过反射,捞取带有@Value注解的变量
  • pickPropertyKey()
  • 主要就是解析@Value注解中表达式,挑出变量名,用于缓存
  • 如: @value("hello ${name:xhh} ${now:111}
  • 解析之后,有两个变量,一个 name 一个 now
  • 缓存Map<String, List<FieldPair>>
  • 缓存的 key,为变量名
  • 缓存的 value,自定义类,主要用于反射修改配置值


3.2 修改事件同步


从命名也可以看出,我们这里选择事件机制来实现同步,直接借助 Spring Event 来完成

一个简单的自定义类事件类


public static class ConfigUpdateEvent extends ApplicationEvent {
    String key;
    public ConfigUpdateEvent(Object source, String key) {
        super(source);
        this.key = key;
    }
}
复制代码


消费也比较简单,直接将下面这段代码,放在上面的

AnoValueRefreshPostProcessor, 接收到变更事件,通过 key 从缓存中找到需要变更的 Field,然后依次执行刷新即可


@EventListener
public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
    List<FieldPair> list = mapper.get(configUpdateEvent.key);
    if (!CollectionUtils.isEmpty(list)) {
        list.forEach(f -> f.updateValue(environment));
    }
}
复制代码


4. 实例演示


最后将前面修改配置的代码块封装一下,提供一个接口,来验证下我们的配置刷新


@RestController
public class DynamicRest {
    @Autowired
    ApplicationContext applicationContext;
    @Autowired
    ConfigurableEnvironment environment;
    @Autowired
    RefreshConfigProperties refreshConfigProperties;
    @GetMapping(path = "dynamic/update")
    public RefreshConfigProperties updateEnvironment(String key, String value) {
        String name = "applicationConfig: [classpath:/application-dynamic.yml]";
        MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
        Map<String, Object> source = propertySource.getSource();
        Map<String, Object> map = new HashMap<>(source.size());
        map.putAll(source);
        map.put(key, value);
        environment.getPropertySources().replace(name, new MapPropertySource(name, map));
        applicationContext.publishEvent(new AnoValueRefreshPostProcessor.ConfigUpdateEvent(this, key));
        return refreshConfigProperties;
    }
}
复制代码

image.png


5.小结


本文主要通过简单的几步,对@Value进行了拓展,支持配置动态刷新,核心知识点下面三块:


  • 使用 BeanPostProcess 来扫描需要刷新的变量
  • 利用 Spring Event 事件机制来实现刷新同步感知
  • 至于配置的修改,则主要是MapPropertySource来实现配置的替换修改

请注意,上面的这个实现思路,与 Spring Cloud Config 是有差异的,很久之前写过一个配置刷新的博文,有兴趣的小伙伴可以看一下 SpringBoot 配置信息之配置刷新



相关文章
|
6天前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
23 0
|
19天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
95 62
|
10天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
23 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
9天前
|
Java 数据库连接
SpringBoot配置多数据源实战
第四届光学与机器视觉国际学术会议(ICOMV 2025) 2025 4th International Conference on Optics and Machine Vision
37 8
|
7天前
|
Java 数据库连接 数据库
springboot启动配置文件-bootstrap.yml常用基本配置
以上是一些常用的基本配置项,在实际应用中可能会根据需求有所变化。通过合理配置 `bootstrap.yml`文件,可以确保应用程序在启动阶段加载正确的配置,并顺利启动运行。
13 2
|
17天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
35 2
|
18天前
|
Java Spring 容器
SpringBoot读取配置文件的6种方式,包括:通过Environment、@PropertySource、@ConfigurationProperties、@Value读取配置信息
SpringBoot读取配置文件的6种方式,包括:通过Environment、@PropertySource、@ConfigurationProperties、@Value读取配置信息
43 3
|
Java 测试技术
springboot中使用@Value读取配置文件
一:配置文件 一般我们配制配置文件都是多套的。测试环境,生产环境。   一般   application.properties里面配置都是公共的不用动的配置,application-test.properties配置的就是测试环境所需要的配置,application-prod.properties就是生产环境所需要的配置。
5261 0
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
145 1
|
20天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。