@ConfigurationProperties自动刷新配置原理

20 4 月, 2023 454点热度 0人点赞 0条评论

在nacos自动配置刷新中,当我们使用spring-cloud做微服务开发的时候,主要通过两种方式自动刷新配置:

  • @RefreshScope
  • 将配置放在类中,并通过@ConfigurationProperties配置

这边文章主要介绍@ConfigurationProperties注解工作原理,以及解释自动刷新配置的工作机制。

1. 示例展示

在开始讲解具体原理之前,还是以一个示例开始,不会显得那么突兀。项目结构以及关键文件配置如下:

configuratioproperties spring nacos refresh

configuratioproperties spring nacos refresh

这里可能显的有点多余,不过抱着学习的态度还是需要有的,主要可以避免再去查询和匹配spring-cloud-alibaba版本的配对以及部分新学的小伙伴看不太懂的尴尬。如果你已经知道怎么使用,则跳过示例展示部分,直接进入正题。

parent pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.spring.cloud.alibaba.learn</groupId>
  <artifactId>spring-cloud-alibaba</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>spring-cloud-alibaba</name>
  <url>http://maven.apache.org</url>


  <modules>
    <module>spring-cloud-alibaba-nacos</module>
  </modules>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2021.0.5.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2021.0.5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.6.13</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

child pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.spring.cloud.alibaba.learn</groupId>
    <artifactId>spring-cloud-alibaba</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>

  <groupId>org.example</groupId>
  <artifactId>spring-cloud-alibaba-nacos</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-cloud-alibaba-nacos</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

ConfigProperties

@Data
@ConfigurationProperties(prefix = "config")
public class ConfigProperties {

    private String name;
    private Integer age;
    private String className;
}

SpringApplicationNacos

@SpringBootApplication
public class SpringApplicationNacos {
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringApplicationNacos.class, args);
        System.out.println(applicationContext);
//        ConfigurableEnvironment environment = applicationContext.getEnvironment();
//        System.out.println(environment.getProperty("config.name"));
//        System.out.println(environment.getProperty("config.app"));
//        System.out.println(environment.getProperty("config.bootstrap"));

        ConfigProperties configProperties = applicationContext.getBean(ConfigProperties.class);
        System.out.println(configProperties);
        System.out.println(Arrays.toString(applicationContext.getBeanNamesForType(ConfigProperties.class)));

        new ScheduledThreadPoolExecutor(1)
                .scheduleAtFixedRate(() -> {
                    System.out.println(configProperties);
                }, 0, 5, TimeUnit.SECONDS);
    }
}

bootstrap.yml

config:
  name: bootstrap
  bootstrap: app-boot
spring:
  cloud:
    nacos:
      config:
        server-addr: 192.168.56.101:8848
        namespace: 48dfde1f-308d-4a79-b1ee-62b2a3ff5320
        file-extension: yml
        username: nacos
        password: nacos
        group: dev
  application:
    name: spring-nacos

在以上的示例中,我认为有几点需要大家注意:

  • ConfigProperties配置使用了@ConfigurationProperties注解,该注解会配置前缀prefix, 让后将满足前缀的配置封装到当前类中
  • ConfigConfiguration类中,表明了@ConfigurationProperties标注的类有两种引入方式:
    • @EnableConfigurationProperties注解引入配置类
    • 创建bean方法,并在方法上标注@Bean实现注入

2. Bean的处理

看到这里就会有个疑问: Spring是如何处理被@ConfigurationProperties标注的bean的呢?spring的bean加载流程,不在这篇文章范围内。在Spring的扩展机制中,BeanPostProcessor是作为主要的扩展实现,因此我们以此为入口,找到对应的扩展类.

ConfigurationPropertiesBindingPostProcessor

postProcessBeforeInitialization()

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    // bind()方法主要是对当前的bean设置属性值,这里的主要实现是以Binder类型来完成
    // ConfigurationProperitesBean的目的是缓存被@ConfigurationProperties标注的bean, 因此通过该类
    // 可以拿到在spring容器中所有被@ConfigurationProperties标注的bean列表
    bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
    return bean;
}

bind()

private void bind(ConfigurationPropertiesBean bean) {
    // 判断bean是否为null,或者已经包含了绑定值的bean
    if (bean == null || hasBoundValueObject(bean.getName())) {
        return;
    }
    Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
            + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");
    try {
        // 执行属性绑定
        this.binder.bind(bean);
    }
    catch (Exception ex) {
        throw new ConfigurationPropertiesBindException(bean, ex);
    }
}

ConfiguraionPropertiesBean

上面讲到了该类会存储所有的@ConfigurationProperties注解标注的bean列表,因此我们这里就主要看下get()方法中代码逻辑:

get()

public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
    // 获取bean的工厂方法,当不是通过方法创建bean时,这里返回为空
    Method factoryMethod = findFactoryMethod(applicationContext, beanName);
    // 创建bean
    return create(beanName, bean, bean.getClass(), factoryMethod);
}

findFactoryMethod()

private static Method findFactoryMethod(ConfigurableListableBeanFactory beanFactory, String beanName) {
    // BeanFactory中是否包含BeanDefinition定义
    if (beanFactory.containsBeanDefinition(beanName)) {
        // 获取BeanDefinition定义对象
        BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
        if (beanDefinition instanceof RootBeanDefinition) {
            // 获取创建工厂方法
            Method resolvedFactoryMethod = ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod();
            if (resolvedFactoryMethod != null) {
                return resolvedFactoryMethod;
            }
        }
        // 如果没有包含创建工厂方法,从beanDefinition中查找工厂方法
        return findFactoryMethodUsingReflection(beanFactory, beanDefinition);
    }
    return null;
}

findFactoryMethodUsingReflection()

private static Method findFactoryMethodUsingReflection(ConfigurableListableBeanFactory beanFactory,
            BeanDefinition beanDefinition) {
        // 获取工厂方法名称
        String factoryMethodName = beanDefinition.getFactoryMethodName();
        // 获取工厂bean名称
        String factoryBeanName = beanDefinition.getFactoryBeanName();
        // 如果两者都为null,则表明不是通过工厂方法创建,则直接返回null
        if (factoryMethodName == null || factoryBeanName == null) {
            return null;
        }
        // 获取工厂class对象
        Class<?> factoryType = beanFactory.getType(factoryBeanName);
        // 判断是否为cglib代理类
        if (factoryType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
            factoryType = factoryType.getSuperclass();
        }
        AtomicReference<Method> factoryMethod = new AtomicReference<>();
        // 遍历工厂方法类中的所有方法,并判断方法名称是否一致,一致就作为方法返回
        ReflectionUtils.doWithMethods(factoryType, (method) -> {
            if (method.getName().equals(factoryMethodName)) {
                factoryMethod.set(method);
            }
        });
        return factoryMethod.get();
    }

create()

private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
    // 从当前bean Class上获取@ConfigurationProperties注解
    ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
    // 如果注解为空,则返回
    if (annotation == null) {
        return null;
    }
    // 从type上获取@Validated注解信息
    Validated validated = findAnnotation(instance, type, factory, Validated.class);
    // 如果validated注解不为空,则放入两个注解。否则放入@ConfigurationProperties注解
    Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
            : new Annotation[] { annotation };
    // 如果是通过工厂方法创建,则按照工厂方法解析bean类型,否则按照type的实际类型解析
    ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
            : ResolvableType.forClass(type);
    // 创建Bindable对象
    Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
    if (instance != null) {
        // 主要判断instance是否存在,存在时会返回新的Bindable对象
        bindTarget = bindTarget.withExistingValue(instance);
    }
    // 创建ConfigurationPropertiesBean对象
    return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
}

创建方法最终是创建了ConfigurationPropertiesBean对象,并且带对象持有了bean对象的实例,然后通过bind()方法对属性值进行填充。最终是ConfigProperties对象能够包含完整的配置属性值。

3. 刷新配置处理

当我们看完了bean的处理逻辑之后,那么配置更新其实并不会自动的导致配置更新到对应的bean, 原因是因为spring中并不清楚配置与bean之间的关系,因此这里可以猜测,通过某种机制会触发Bean被对应的BeanPostProcessor类重新执行,然后将新的属性值填充到bean中。

nacos 监听配置变化

在spring启动过程中,会从nacos-server加载配置信息,当容器启动完毕后,会发送ApplicationReadyEvent事件,nacos框架中会监听该事件,并开启监听逻辑。

NacosContextRefresher

public void onApplicationEvent(ApplicationReadyEvent event) {
    // many Spring context
    if (this.ready.compareAndSet(false, true)) {
        this.registerNacosListenersForApplications();
    }
}

当接收到事件之后,当nacos监听还没有准备好时,将开启监听。

private void registerNacosListenersForApplications() {
    if (isRefreshEnabled()) {
        // 获取所有的nacos配置列表
        for (NacosPropertySource propertySource : NacosPropertySourceRepository
                .getAll()) {
            // 判断配置是否能够刷新
            if (!propertySource.isRefreshable()) {
                continue;
            }
            // 获取dataId
            String dataId = propertySource.getDataId();
            // 注册nacos监听器
            registerNacosListener(propertySource.getGroup(), dataId);
        }
    }
}

在处理事件的时候,会注册listener监听器,用于处理配置文件的变化逻辑。具体代码如下:

private void registerNacosListener(final String groupKey, final String dataKey) {
    // 获取key
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    // 存入listener映射,该listener为一个匿名内部类
    Listener listener = listenerMap.computeIfAbsent(key,
            lst -> new AbstractSharedListener() {
                @Override
                public void innerReceive(String dataId, String group,
                        String configInfo) {
                    // 增加刷新次数
                    refreshCountIncrement();
                    // 新增刷新记录到历史记录中
                    nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                    // 发布RefreshEvent事件
                    applicationContext.publishEvent(
                            new RefreshEvent(this, null, "Refresh Nacos config"));
                    if (log.isDebugEnabled()) {
                        log.debug(String.format(
                                "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
                                group, dataId, configInfo));
                    }
                }
            });
    try {
        // 将listener注册到ConfigService中
        configService.addListener(dataKey, groupKey, listener);
        log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,
                groupKey);
    }
    catch (NacosException e) {
        log.warn(String.format(
                "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
                groupKey), e);
    }
}

从以上代码可知,当配置发生更新时,会想容器中发送RefreshEvent事件,因此关注点在处理该事件的listener的逻辑。

事件的注册和监听的逻辑,不在本篇文章的范围内,可以查看源码对应的实现逻辑。

RefreshEventListener

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationReadyEvent) {
        handle((ApplicationReadyEvent) event);
    }
    // 处理刷新事件
    else if (event instanceof RefreshEvent) {
        handle((RefreshEvent) event);
    }
}
public void handle(RefreshEvent event) {
    if (this.ready.get()) { // don't handle events before app is ready
        log.debug("Event received " + event.getEventDesc());
        // 容器刷新
        Set<String> keys = this.refresh.refresh();
        log.info("Refresh keys changed: " + keys);
    }
}

该类在处理刷新事件的时候,最终是通过ContextRefresher完成刷新动作。

ContextRefresher

refresh()

public synchronized Set<String> refresh() {
    // 刷新Environment配置信息
    Set<String> keys = refreshEnvironment();
    // 刷新Scope,该Scope对应的RereshScope的实现,会导致bean重新创建,也是@Refresh实现的关键点
    this.scope.refreshAll();
    return keys;
}

这段代码有个点,不在这篇文章中介绍,但是缺失一个单独的知识点,就是Scope的工作,因为该类也是@RefreshScope的实现的重要类,这里的scope.refreshAll()方法会导致被@RefreshScope标记的bean重新被创建,这个会在后面的文章中单独介绍。

因此,在刷新的动作中,主要是刷新Environment对象,具体查看刷新的源码.

refreshEnvironment()

public synchronized Set<String> refreshEnvironment() {
    // 获取标准配置之前的resources配置列表,
    // systemProperties, systemEnvironment, servletContextInitParams, servletConfigInitParams, configurationProperties, jndiProperties
    // 这里需要注意,因为resources具有优先级,因此低优先级的相同配置最终会被高优先级的配置覆盖
    Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
    // 更新环境配置
    updateEnvironment();
    // 对比前后的配置列表,并获取发生变更的配置key
    Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
    // 发布环境配置发生变更事件
    this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
    return keys;
}

LegacyContextRefresher

updateEnvironment()

/* For testing. */ ConfigurableApplicationContext addConfigFilesToEnvironment() {
    ConfigurableApplicationContext capture = null;
    try {
        // 创建并复制Environment对象,复制完成的Environment对象中,主要包括了:
        // commandLineArgs和defaultProperties两种配置信息,
        // 并且也会复制activeProfiles和defaultProfiles两个配置信息,这样就组成了新的Environment对象
        StandardEnvironment environment = copyEnvironment(getContext().getEnvironment());

        // 设置刷新环境参数配置,并将配置信息加入到当前的Environment中,并命名为:refreshArgs
        Map<String, Object> map = new HashMap<>();
        map.put("spring.jmx.enabled", false);
        map.put("spring.main.sources", "");
        // gh-678 without this apps with this property set to REACTIVE or SERVLET fail
        map.put("spring.main.web-application-type", "NONE");
        map.put(BOOTSTRAP_ENABLED_PROPERTY, Boolean.TRUE.toString());
        environment.getPropertySources().addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));

        // 构建SpringApplication对象,这里的SpringApplication的构建,限制了本身SpringApplication的完整功能,
        // 因此只是一个缩减版
        SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class).bannerMode(Banner.Mode.OFF)
                .web(WebApplicationType.NONE).environment(environment);
        // 在SpringApplicaion创建的时候,会采用SPI机制加载ApplicationListener列表,这里
        // 将默认的配置信息覆盖到,而只是设置了会影响Environment这部分的Listener, 因此
        // 这里的SpringApplication的启动只会影响Environment中的值
        builder.application().setListeners(
                Arrays.asList(new BootstrapApplicationListener(), new BootstrapConfigFileApplicationListener()));
        capture = builder.run();

        // 移除refreshArgs资源配置信息
        if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
            environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
        }

        // 获取刷新前的配置资源列表,并使用现有的资源列表替换已有的资源列表配置
        // 如果配置在已有资源列表中不存在,则加入到资源列表首部,具有高优先级
        MutablePropertySources target = getContext().getEnvironment().getPropertySources();
        String targetName = null;
        for (PropertySource<?> source : environment.getPropertySources()) {
            String name = source.getName();
            if (target.contains(name)) {
                targetName = name;
            }
            if (!this.standardSources.contains(name)) {
                if (target.contains(name)) {
                    target.replace(name, source);
                }
                else {
                    if (targetName != null) {
                        target.addAfter(targetName, source);
                        // update targetName to preserve ordering
                        targetName = name;
                    }
                    else {
                        // targetName was null so we are at the start of the list
                        target.addFirst(source);
                        targetName = name;
                    }
                }
            }
        }
    }
    finally {
        ConfigurableApplicationContext closeable = capture;
        while (closeable != null) {
            try {
                closeable.close();
            }
            catch (Exception e) {
                // Ignore;
            }
            if (closeable.getParent() instanceof ConfigurableApplicationContext) {
                closeable = (ConfigurableApplicationContext) closeable.getParent();
            }
            else {
                break;
            }
        }
    }
    return capture;
}

在配置信息刷新过程中,主要包含了以下几个要点:

  • 刷新配置会创建SpringApplicaion对象,并重新进行容器初始化。只是这个时候初始化并不会全部初始化,而是只是与Environment有关的处理类作为监听入口
  • 新的SpringApplication配置刷新并不包含application相关的配置文件。
  • 当在刷新配置之后,有新的配置源引入后,新的配置源会有较高的优先级
  • 在刷新之后获取到的环境配置信息会和现有的配置进行合并,相同名称的配置源会被新的配置替换。

4. 更新Bean配置

当环境配置更新并加载完毕之后,此时会向容器中发送EnvironmentChangeEvent事件通知,该事件通知告知环境配置发生变化,对应的Listener可以对这个事件做出响应。

ConfigurationPropertiesRebinder

public void onApplicationEvent(EnvironmentChangeEvent event) {
        if (this.applicationContext.equals(event.getSource())
                // Backwards compatible
                || event.getKeys().equals(event.getSource())) {
            rebind();
        }
    }

首先判断ApplicationContext是否发生变化,在满足条件之后,则执行rebind()方法。

rebind()

public void rebind() {
    // 清理所有的错误
    this.errors.clear();
    // 这里所有的bean名称是从ConfigurationPropertiesBeans中获取
    // 该类中存储了所有的被@ConfigurationProperties标注的bean列表
    for (String name : this.beans.getBeanNames()) {
        rebind(name);
    }
}
public boolean rebind(String name) {
    // 如果name表示的bean没有在ConfigurationPropertiesBeans,也就是说没有被@ConfigurationProperties标注
    // 那么就不应该处理对应的bean
    if (!this.beans.getBeanNames().contains(name)) {
        return false;
    }
    // 获取ApplicationContext对象
    ApplicationContext appContext = this.applicationContext;
    while (appContext != null) {
        // 判断是否包含了bean对象
        if (appContext.containsLocalBean(name)) {
            return rebind(name, appContext);
        }
        else {
            // 双亲委派规则,如果当前没有,则寻找父context
            appContext = appContext.getParent();
        }
    }
    return false;
}
private boolean rebind(String name, ApplicationContext appContext) {
    try {
        // 获取bean实例对象
        Object bean = appContext.getBean(name);
        // 如果是aop代理对象,则获取被代理对象
        if (AopUtils.isAopProxy(bean)) {
            bean = ProxyUtils.getTargetObject(bean);
        }
        if (bean != null) {
            // 判断当前的bean是否不需要刷新
            if (getNeverRefreshable().contains(bean.getClass().getName())) {
                return false; // ignore
            }
            // 执行bean的销毁方法,包含了销毁方法,销毁回调接口等。
            appContext.getAutowireCapableBeanFactory().destroyBean(bean);
            // 重新初始化bean, 这个时候会执行到ConfigurationPropertiesBindingPostProcessor中,重新对bean的属性赋值
            appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
            return true;
        }
    }
    catch (RuntimeException e) {
        this.errors.put(name, e);
        throw e;
    }
    catch (Exception e) {
        this.errors.put(name, e);
        throw new IllegalStateException("Cannot rebind to " + name, e);
    }
    return false;
}

因此,当spring在处理环境配置发生改变的时候,这个时候会包含两个步骤:

  • 执行bean的销毁程序,这个就包含了destory的方法,DisposableBean回调等
  • 再次初始化bean, 初始化bean并不是重新创建一个新的bean, 而是对其中的属性进行重新赋值

因此经过以上的步骤,实现了bean的配置动态更新。到此@ConfigurationProperties配置动态更新的源码解析完毕。

专注着

一个奋斗在编程路上的小伙

文章评论