『Naocs 2.x』(九) SpringCloud 是如何实现配置动态刷新的?

7,300 阅读5分钟

前言

前段时间探究了,Nacos 配置变更时,如何与 Spring Boot 项目同步的。

这次我们继续来看,Spring Boot 项目收到更新后的配置,是如何刷新到项目中的。

spring-cloud-context

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
    <version>2.2.8.RELEASE</version>
</dependency>

我们常说,Spring Cloud 是基于 Spring Boot,为什么有这种说法?

就是因为 Spring Cloud 本质上,是对 Spring Boot 做了一些增强和新的特性。

例如,我们本节要了解的内容,配置中心的配置动态刷新,就基于spring-cloud-context实现的一个特性,允许在运行时动态刷新 bean。

@RefreshScope 说起

从使用中,我们了解到,若想一个类能够有动态刷新的效果,需使用类注解@RefreshScope

所以,让我们从这个注解开始看起。

 
/**
* 将@Bean定义放入refresh scope便捷注释。
* 以这种方式注释的 Bean 可以在运行时刷新,任何使用它们的组件将在下一次方法调用时获得一个新实例,完全初始化并注 入所有依赖项
* @author Dave Syer
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
    /**
     * @see Scope#proxyMode()
     * @return proxy mode
     */
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

@RefreshScope 实际是对 @Scope的包装,指定了代理类型为 TARGET_CLASS

此类型代表此类型,创建一个基于类的代理(使用 CGLIB)。

注释中也写的清楚,将在下一次方法调用时获得一个新实例,完全初始化并注 入所有依赖项

这里使用的代理类,以及代理类中获取新实例的逻辑在:

CglibAopProxy # DynamicAdvisedIntercepto r# intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)

这就意味着,如果我们把更新的配置塞进去,下一次调用重新初始化实例的时候,就能够加载新的配置了。

关于Scope,在最开始使用 xml配置的时候,我们就使用过这玩意儿,可以配置 bean 的作用域。不过多描述了。

Scope、GenericScope、RefreshScope

这三个类,是 Spring Cloud 中实现配置动态刷新的关键类。

image-20211230122855426

我们先来看 Scope 的抽象方法:

public interface Scope {
 
    // 获取真正的对象。
    // 和 ObjectFactory 的机制是一样的。
    Object get(String name, ObjectFactory<?> objectFactory);
 
    // 移除对象
    @Nullable
    Object remove(String name);
 
    // 注册某个 bean 销毁时的回调方法。
    void registerDestructionCallback(String name, Runnable callback);
 
    // .... 省略无关方法
}

这些抽象方法,在GenericScope中有通用实现,RefreshScope则是针对动态刷新Bean多了一些逻辑。 下面我们摘录 GenericScope 的 get()destroy()方法,了解一下逻辑,这两个方法比较重要:

public class GenericScope implements Scope, BeanFactoryPostProcessor,BeanDefinitionRegistryPostProcessor, DisposableBean {
 
// 这个方法很重要
// 被 @RefreshScope 注解的类,都会被 cglib 代理。
// 代理类每次调用方法,最终都会最终先调用这个方法,获取目标类。然后再执行方法。
// this.cache.put 的效果是 如果 name 存在,就返回已存在的值;如果 name 不存在,就存入新值。
public Object get(String name, ObjectFactory<?> objectFactory) {
    BeanLifecycleWrapper value = this.cache.put(name,
            new BeanLifecycleWrapper(name, objectFactory));
    this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
    try {
        // 第一次执行此方法,内部会走创建 bean 的逻辑。
        return value.getBean();
    }
    catch (RuntimeException e) {
        this.errors.put(name, e);
        throw e;
    }
}
 
// 销毁 bean,就是从 cache 中移除name 。
protected boolean destroy(String name) {
    BeanLifecycleWrapper wrapper = this.cache.remove(name);
    if (wrapper != null) {
        Lock lock = this.locks.get(wrapper.getName()).writeLock();
        lock.lock();
        try {
            wrapper.destroy();
        }
        finally {
            lock.unlock();
        }
        this.errors.remove(name);
        return true;
    }
    return false;
}
}

然后我们继续看 RefreshScope,我们也只摘录关注的方法:

public class RefreshScope extends GenericScope implements ApplicationContextAware,ApplicationListener<ContextRefreshedEvent>, Ordered {
 
  // 刷新单个 bean
    public boolean refresh(String name) {
        if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
            name = SCOPED_TARGET_PREFIX + name;
        }
        if (super.destroy(name)) {
            this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
            return true;
        }
        return false;
    }
 
  // 刷新所有 bean
    public void refreshAll() {
        super.destroy();
        this.context.publishEvent(new RefreshScopeRefreshedEvent());
    }
}

这里我们看到,刷新方法,实际上就是执行destroy()

destroy()的内部逻辑,则是从 cache 中移除掉该 name,或者是清空cache

这样,我们下一次执行方法时,cache中没有对应的 bean,就会重新添加并初始化 bean。

ContextRefresher

至上,我们了解了,动态刷新 bean 的方法。

那么,动态刷新 Bean 是如何与配置更新结合起来的呢?答案便在 ContextRefresher类中。

public class ContextRefresher {
 
    // ...
 
    public synchronized Set<String> refresh() {
        // 刷新上下文配置环境
        Set<String> keys = refreshEnvironment();
        // 这里执行的就是 RefreshScope#refreshAll() 方法,即销毁缓存中的 Bean。
        this.scope.refreshAll();
        return keys;
    }
 
    public synchronized Set<String> refreshEnvironment() {
         // 收集原本的配置项 key
        Map<String, Object> before = extract(
                this.context.getEnvironment().getPropertySources());
        // 把新配置项,添加到上下文配置环境中
        addConfigFilesToEnvironment();
        // 匹配出修改的配置
        Set<String> keys = changes(before,
                extract(this.context.getEnvironment().getPropertySources())).keySet();
        this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
        return keys;
    }
 
    // ...
 
}

我们可以看到,这里的流程是:

  • 先刷新一遍上下文的配置环境,随即销毁缓存中的Bean 。
  • 下一次获取 bean 的时候,便会重新走一遍初始化 bean 的逻辑,也就会把新加载的配置项,注入到新的 Bean 中。

这便完成了配置刷新。 整体流程明白了, 我们再来细看一下 addConfigFilesToEnvironment()

ConfigurableApplicationContext addConfigFilesToEnvironment() {
        ConfigurableApplicationContext capture = null;
        try {
              // 从当前 Environment 复制一个新 Environment
            StandardEnvironment environment = copyEnvironment(
                    this.context.getEnvironment());
             // 构造 SpringApplication
            SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
                    .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
                    .environment(environment);
            builder.application()
                    .setListeners(Arrays.asList(new BootstrapApplicationListener(),
                            new ConfigFileApplicationListener()));
             // 运行 SpringApplication
             // 这里会走一遍 SpringApplication 启动的流程,在此这种,也就把新配置文件加载到 Environment 类中了。
            capture = builder.run();
            if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
                environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
            }
             // 获取原上下文的属性源
            MutablePropertySources target = this.context.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 {
            // ....
        }
        return capture;
}

那么至此,Spring Boot 配置动态刷新的机制,已经整体上梳理完了。

尝试使用 ContextRefresher

最后,我们来尝试使用一下 ContextRefresher 来刷新一下配置吧。

  1. 创建 Spring Boot Web 项目,另外引入依赖 spring-cloud-context

  2. 配置文件

    server:
      port: 9004
    spring:
      application:
        name: testOne
    damai:
      jj: jj
      xx: xx
    
  3. 接口

    @RestController
    @RequestMapping
    public class TestController{
     
        @Autowired
        ContextRefresher contextRefresher;
        @Autowired
        private TestObj testObj;
     
        @GetMapping("/test")
        public String test() {
            return testObj.getJj();
        }
     
        @GetMapping("/refresh")
        public void refresh() {
            Set<String> refresh = contextRefresher.refresh();
            System.out.println(Arrays.toString(refresh.toArray()));
        }
    }
    
  4. 启动,访问接口/test

    image-20220102180717287

  5. 修改配置文件,访问接口/refresh

    damai:
      jj: jj123
    
  6. 访问接口/test

    image-20220102180935333

如此,便完成了,配置的动态刷新。

结束。