«Naocs 2.x» (9) Как SpringCloud реализует динамическое обновление конфигурации?

Spring Boot Spring Cloud

предисловие

Некоторое время назад я исследовал, как синхронизироваться с проектом Spring Boot при изменении конфигурации Nacos.

На этот раз давайте продолжим наблюдать за тем, как проект 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-компонентов. Но больше описания.

Область, Универсальная область, Обновить область

Эти три класса являются ключевыми для реализации динамического обновления конфигурации в 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-компонентов. Ниже мы приводим выдержку из GenericScopeget()и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Если в нем нет соответствующего компонента, компонент будет повторно добавлен и инициализирован.

ContextRefresher

Выше мы узнали, как динамически обновлять бины.

Итак, как динамический компонент обновления сочетается с обновлением конфигурации? ответ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;
    }
 
    // ...
 
}

Мы видим, что поток здесь:

  • Сначала обновите среду конфигурации контекста, а затем уничтожьте бины в кеше.
  • В следующий раз, когда компонент будет получен, логика инициализации компонента будет повторена, и вновь загруженные элементы конфигурации будут внедрены в новый компонент.

На этом обновление конфигурации завершено. Общий процесс понятен, давайте подробнее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 и добавьте дополнительные зависимости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

Таким образом, динамическое обновление конфигурации завершено.

Заканчивать.