likes
comments
collection
share

【BlossomConfig】SpringBoot如何实现配置的管控?

作者站长头像
站长
· 阅读数 10

网关项目源码 RPC项目源码 配置中心项目源码

ConfigurableEnvironment

ConfigurableEnvironment 是 Spring Framework 中的一个接口,它继承自 Environment 接口。这个接口的主要职责是提供对环境配置的可编程接口,允许在应用运行期间动态调整环境和配置属性。 主要功能

  1. 属性源管理:ConfigurableEnvironment 允许你添加、移除或重新排序属性源(PropertySources)。属性源是配置数据的来源,比如properties文件、环境变量、命令行参数等。
  2. 激活和禁用配置文件:支持基于不同的环境激活或禁用特定的配置文件(如 application-dev.yml, application-prod.yml 等)。
  3. 属性解析:提供了解析属性值的方法,支持占位符解析(例如 ${property.name})和类型转换。
  4. 配置覆盖:能够覆盖现有的属性源中的配置,实现动态配置更新。

基于上面的点,我们知道,在开发我们自己的配置中心时,ConfigurableEnvironment 可以提供以下帮助:

  1. 动态配置更新:通过修改 ConfigurableEnvironment 中的属性源,可以实现配置的动态加载和更新,无需重启应用。
  2. 环境适配:可根据不同的环境(开发、测试、生产)动态调整配置,支持多环境管理。
  3. 配置的灵活管理:可以灵活地添加或移除配置来源,如将配置中心作为一个新的属性源集成到应用中。
  4. 统一配置接入点:通过 ConfigurableEnvironment,可以将来自不同来源的配置统一管理和访问,提高配置管理的一致性和可维护性。 在自研配置中心的集成中,可以通过实现一个 EnvironmentPostProcessor,在应用启动阶段动态添加自定义的属性源:
public class CustomConfigCenterPostProcessor implements EnvironmentPostProcessor {
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 添加自定义的属性源到环境中
        Map<String, Object> customConfig = loadConfigFromCustomConfigCenter();
        environment.getPropertySources().addLast(new MapPropertySource("customConfig", customConfig));
    }

    private Map<String, Object> loadConfigFromCustomConfigCenter() {
        // 加载配置中心的配置
        // ...
    }
}


package blossom.project.config.core;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class ConfigurableEnvironmentTest
{

    @Autowired
    private ConfigurableEnvironment environment;

    // 添加或更新配置项
    public void addOrUpdateProperty(String key, String value) {
        MutablePropertySources propertySources = environment.getPropertySources();
        Map<String, Object> properties = new HashMap<>();
        properties.put(key, value);

        PropertySource<?> propertySource = new MapPropertySource("customPropertySource", properties);

        if (propertySources.contains("customPropertySource")) {
            // 更新现有的属性源
            ((MapPropertySource) propertySources.get("customPropertySource")).getSource().putAll(properties);
        } else {
            // 添加新的属性源
            propertySources.addLast(propertySource);
        }
    }

    // 获取配置项
    public String getProperty(String key) {
        return environment.getProperty(key);
    }

    // 删除配置项
    public void removeProperty(String key) {
        MutablePropertySources propertySources = environment.getPropertySources();
        if (propertySources.contains("customPropertySource")) {
            MapPropertySource propertySource = (MapPropertySource) propertySources.get("customPropertySource");
            propertySource.getSource().remove(key);
        }
    }

    // 列出所有配置项
    public Map<String, Object> listProperties() {
        Map<String, Object> properties = new HashMap<>();
        environment.getPropertySources().forEach(propertySource -> {
            if (propertySource instanceof MapPropertySource) {
                properties.putAll(((MapPropertySource) propertySource).getSource());
            }
        });
        return properties;
    }
}

在这个例子中,loadConfigFromCustomConfigCenter 方法负责从自研配置中心加载配置,然后将其作为一个新的属性源添加到环境中。这样,应用就可以像访问其他任何属性源一样访问这些配置了。 因此,通过上面的例子,我们就已经知道了如何在项目运行过程中实现配置文件的增删改查功能了。 接下来我们需要解决第二个问题,如何动态修改@Value以及@ConfigurationProperties等注解修饰的属性的对应的值? 这里首先了解一个前提: 对于 @Value 注解,我们可能需要手动刷新相关的Bean,因为 @Value 注解通常在Bean初始化时解析一次,之后不会自动更新。这可以通过实现自定义的更新逻辑来完成。 对于 @ConfigurationProperties 注解,如果配置的Bean是一个 @Component,并且配置项是在运行时可变的(如使用了 @RefreshScope),那么当环境中的属性更新后,这些配置项将被自动更新。 所以,我们重点需要解决的就是@Value注解的解析问题。

事件监听完成配置变更

这里我想到的思路是:结合 Environment 的动态更新特性和 Spring 的事件监听机制。 基本的思路是当配置发生变化时,触发一个事件,然后处理这个事件来更新相关的属性。这通常包括以下几个步骤:

  1. 监听配置变更事件:当配置源(例如您的配置中心)中的配置项发生变化时,需要有机制触发一个事件。
  2. 处理事件:当事件被触发时,获取变更的配置项,并更新 Environment 中相应的配置。
  3. 通知属性更新:在配置更新后,通知所有依赖这些配置的Bean,使它们重新加载新的配置值。 大概的实现代码如下:
  4. 首先编写一个事件,当前事件用来存储配置变更时候的信息。
@Component
public class ConfigChangeEvent extends ApplicationEvent {

    private final Map<String, Object> changedProperties;

    /**
     * 构造配置更改事件。
     *
     * @param source 事件源
     * @param changedProperties 包含变更的配置项及其值的映射
     */
    public ConfigChangeEvent(Object source, Map<String, Object> changedProperties) {
        super(source);
        this.changedProperties = Collections.unmodifiableMap(changedProperties);
    }

    /**
     * 获取发生变更的配置项及其值。
     *
     * @return 变更的配置项映射
     */
    public Map<String, Object> getChangedProperties() {
        return changedProperties;
    }
}
  1. 之后我们需要编写一个事件监听器,来监听配置变更的事件。

并且在这个监听器中,我们需要解析@Value注解,同时,扫描我们的Spring容器,得到所有存在有@Value注解的Field字段,然后判断这次变更事件中的心配置信息,是否存在这些字段的@Value注解对应的属性值,如果存在,我们就进行更新。

@Component
public class ConfigChangeListener implements ApplicationListener<ConfigChangeEvent> {

    private final ConfigurableApplicationContext applicationContext;

    public ConfigChangeListener(ConfigurableApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void onApplicationEvent(ConfigChangeEvent event) {
        // 获取发生变更的配置项
        Map<String, Object> changedProperties = event.getChangedProperties();

        // 更新应用上下文中所有Bean的@Value注解字段
        updateValueAnnotatedFields(changedProperties);
    }

    private void updateValueAnnotatedFields(Map<String, Object> changedProperties) {
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        String[] beanNames = beanFactory.getBeanDefinitionNames();

        for (String beanName : beanNames) {
            Object bean = beanFactory.getBean(beanName);
            Class<?> targetClass = bean.getClass();

            // 遍历所有字段,查找@Value注解
            ReflectionUtils.doWithFields(targetClass, field -> {
                Value valueAnnotation = field.getAnnotation(Value.class);
                if (valueAnnotation != null) {
                    String key = extractKeyFromValueAnnotation(valueAnnotation);
                    if (changedProperties.containsKey(key)) {
                        field.setAccessible(true);
                        Object newValue = changedProperties.get(key);
                        ReflectionUtils.setField(field, bean, newValue);
                    }
                }
            });
        }
    }

    private String extractKeyFromValueAnnotation(Value valueAnnotation) {
        // 提取和解析@Value注解的值,可能需要解析Spring表达式
        String value = valueAnnotation.value();
        // TODO: 实现对Spring表达式的解析,这里只处理简单情况
        return value.startsWith("${") && value.endsWith("}") ?
               value.substring(2, value.length() - 1) : value;
    }
}
  1. 最后,我们编写一个发布事件的生产者
@Component
public class ConfigChangePublisher {

    private final ApplicationEventPublisher eventPublisher;

    public ConfigChangePublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void publishConfigChange() {
        Map<String, Object> changedProperties = new HashMap<>();
        changedProperties.put("test.property", "updatedValue");

        ConfigChangeEvent event = new ConfigChangeEvent(this, changedProperties);
        eventPublisher.publishEvent(event);
    }
}
  1. 测试一下代码可用性
@SpringBootTest
public class ConfigChangeTest {

    @Autowired
    private TestConfig testConfig;

    @Autowired
    private ConfigChangePublisher configChangePublisher;

    @Test
    public void testConfigChange() {
        assertEquals("default", testConfig.getTestProperty());

        configChangePublisher.publishConfigChange();

        // 这里可能需要一些时间来等待配置更新
        // 可能需要使用 Thread.sleep 或其他机制来等待
        assertEquals("updatedValue", testConfig.getTestProperty());
    }
}

我们对上面的代码进行debug运行,发现是可以通过的。 因此,我们就简单的完成了配置中心对配置变更时候的处理。

使用Scope来管控Bean的生命周期

当然,上面的方法其实就是通过遍历存在有@Value注解的属性,然后重新进行属性值的设置,这种方式用到了反射,因此在安全性和性能上有略有问题。 因此,我又继续思考了第二种解决方案。 从上面我们知道,@Value注解不能动态修改值的原因是因为当类初始化的时候,这个值就已经被设置上去了,所以我们运行时修改配置值,是不能修改@Value对应的值的。 那么解决思路也很简单,就是我们能不能重新初始化一下这个类? 也就是我们先将当前类销毁,然后重新创建一个当前的类就好了。 同时,为了保证不“误伤”其他的类,我们肯定得有一个机制来保证只刷新指定的类。 还记得SpringCloud中提供的@RefreshScope注解嘛? 当配置更改时,标有 @RefreshScope 的bean会被Spring Cloud Context特殊处理,这些bean在下一次访问时会被重新初始化,从而获取最新的配置值。 我们要做的,其实就是管理Bean的生命周期,使得我们可以控制bean的创建和销毁规则。 而如何实现上面的功能呢? 在Spring框架中,自定义作用域(Scope)允许你控制Bean的生命周期,即定义Bean的创建、存在和销毁的规则。Spring默认提供了几个内置作用域,如单例(singleton)、原型(prototype)、请求(request)、会话(session)和应用(application)作用域。但在某些特定场景下,这些默认作用域可能不足以满足需求,这时你就可以通过实现自定义作用域来扩展Spring的功能。 因此,我们首先需要实现一下Scope接口,首先介绍一下该接口内部的几个方法的含义。

  1. Object get(String name, ObjectFactory objectFactory) ● 作用:这是最关键的方法。当请求作用域内的Bean时,Spring容器会调用此方法。 ● 参数解释: ○ String name:Bean的名称。 ○ ObjectFactory objectFactory:如果请求的Bean在作用域内不存在,则使用这个工厂来创建新的Bean实例。 ● 行为:通常,此方法会检查是否已经为当前作用域创建了具有指定名称的Bean。如果是,返回现有的Bean;如果不是,使用objectFactory来创建新的Bean,并将其存储在作用域内以供后续使用。
  2. Object remove(String name) ● 作用:从当前作用域中移除具有指定名称的Bean。 ● 行为:此方法通常在Bean不再需要时调用,例如,当作用域结束时。移除操作通常涉及到清理资源和执行任何必要的销毁回调。
  3. void registerDestructionCallback(String name, Runnable callback) ● 作用:为作用域内的Bean注册一个销毁回调。 ● 参数解释: ○ String name:Bean的名称。 ○ Runnable callback:当Bean被销毁时需要执行的回调。 ● 行为:在Bean不再需要时,这些注册的回调将被调用。这是用于资源清理和其他销毁逻辑的地方。
  4. Object resolveContextualObject(String key) ● 作用:解析作用域相关的对象。这是一个高级功能,通常用于解析与当前作用域特定相关的对象。 ● 行为:例如,如果你的作用域与Web请求相关,此方法可能用于解析与当前HTTP请求相关的信息。
  5. String getConversationId() ● 作用:获取当前作用域的会话ID(如果有的话)。 ● 行为:这主要用于支持会话或对话类型的作用域,例如,在Web应用程序中跟踪用户会话。

这个Scope在开发中,我最经常用在多租户的开发。 假设我们创建了一个自定义作用域,比如“租户作用域”(tenant scope),用于多租户应用中。当一个租户的请求进入应用时,我们可能希望所有的Bean都是针对这个特定租户的。这时,我们的自定义作用域可以确保每个租户只看到自己的Bean实例。当租户的请求结束时,作用域内的Bean可以被清除或重置,为下一个租户请求做准备。 这里我简单的列举一下对Scope的代码实现。

public class MyRefreshScope implements Scope {

    private final Map<String, Object> scopeMap = new ConcurrentHashMap<>();

    private final Map<String, Runnable> destructionCallbacks = new ConcurrentHashMap<>();


    @Override
    public Object get(String beanName, ObjectFactory<?> objectFactory) {
        if (scopeMap.containsKey(beanName)){
            return scopeMap.get(beanName);
        }
        //不存在 那么就获取(获取的时候会在内部进行创建)
        Object object = objectFactory.getObject();
        scopeMap.put(beanName,object);
        return object;
    }

    @Override
    public Object remove(String beanName) {
        return scopeMap.remove(beanName);
    }
}

此时,我们还需要对Scope进行注册,来保证spring知道这个我们自己实现的scope。 有两种方法,先介绍第一种,这种比较直观。

@Data
@Configuration
public class ScopeRegistry implements BeanDefinitionRegistryPostProcessor {

    private BeanDefinitionRegistry beanDefinitionRegistry;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        beanFactory.registerScope("myRefreshScope", new MyRefreshScope());
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        this.beanDefinitionRegistry = beanDefinitionRegistry;
    }
}

如下是第二种,使用的是CustomScopeConfigurer。

@Configuration
public class TenantScopeConfig {

    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        Map<String, Object> scopes = new HashMap<>();
        scopes.put("tenant", new TenantScope());
        configurer.setScopes(scopes);
        return configurer;
    }
}

此时,我们就已经成功的完成了自定义的Scope定义。可以用它来管控我们的Bean了。 在我们的需要被管控的Bean上添加注解@Scope(scopeName = "myRefreshScope")

@Component
@Scope(scopeName = "myRefreshScope")
public class RebuildClass {
    @Value("${name1:defaultValue1}")
    private String name1;

    @Value("${name2:defaultValue2}")
    private String name2;

    private long hashCode = hashCode();

    @PostConstruct
    public void init() {
        System.out.println("first hashCode:"+hashCode);

        ScheduledExecutorService service = Executors.newScheduledThreadPool(1);

        service.schedule(()->{
            System.out.println(name1);
            System.out.println(name2);

            System.out.println("new hashCode:"+hashCode);
        }, 3,TimeUnit.SECONDS);

    }
}

此时,只要我们对bean进行销毁和创建,就会执行我们自定义Scope中的逻辑。

private void refreshValue() {
    String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(beanDefinitionName);
        if (REFRESH_SCOPE.equalsIgnoreCase(beanDefinition.getScope())){
            //如果存在当前scope属性 销毁
            applicationContext.getBeanFactory().destroyScopedBean(beanDefinitionName);
            //通过get方法就可以重建
            applicationContext.getBean(beanDefinitionName);
        }
    }
}

到此,我就已经介绍完毕了两种能在运行时动态修改@Value注解对应的值的方式了。 接下来的开发中,我们可以选取其中一种作为我们主要使用的方式。

了解完毕这些之后,还有一个非常重要的知识点我们需要去了解。 就是我们知道,配置中心比如Nacos启动的时候,其实他们的配置信息就已经添加到项目中去了,而上面我们的代码,@Value注解的值还是空的。 所以我们得了解一下spring的生命周期相关的知识,以及配置文件相关的知识,从而使得我们的项目启动的时候,也能优先从配置中心拿到配置之后,就能马上让这些配置生效,从而使得@Value注解能拿到配置中心中设置的值。 我们知道,配置中心的启动依赖bootstrap.yml文件,再项目启动的时候会读取当前配置文件中的配置信息用来加载配置中心的地址。 而配置中心地址加载完毕连接到配置中心之后,会通过http请求的方式/rpc请求/的方式,从配置中心中拿到所有的配置,并且将所有的配置添加到本地。这两步完成之后,我们的功能才完整。 因此,我们就必须了解上面的这些流程是如何实现的。 篇幅有限,对于bootstrap文件的解析,我们留到下一片文章。

什么是配置中心?以及如何实现一个配置中心?

SpringBoot如何实现配置的管控?

SpringCloud项目是如何对bootstrap配置文件进行加载的?

Nacos是如何实现配置文件的读取加载的?

开发配置中心前必须了解的前置知识

配置中心Server和Client端代码的编写

配置中心Core核心功能代码的编写

配置中心源码优化---本地缓存与读写锁