Принцип автоматического обновления @RefreshScope (3)

Spring

предисловие

Текст был включен в мой репозиторий GitHub, добро пожаловать, звезда:GitHub.com/bin39232820…
Лучшее время посадить дерево было десять лет назад, затем сейчас

болтовня

В прошлой статье был проанализирован принцип работы центра конфигурации Nacos, принцип работы клиента и принцип работы сервера, далее нам необходимо сотрудничать с аннотацией @RefreshScope для завершения нашей автоматической настройки

BeanScope

В SpringIOC известный BeanScope имеет синглтон (singleton) и прототип (prototype). Scope бина влияет на управление бином. Например, когда создается бин с Scope=singleton, IOC сохранит экземпляр в карте, чтобы гарантировать, что этот компонент A имеет один и только один экземпляр в контексте IOC. В SpringCloud добавлена ​​область области обновления, которая также изменяет управление bean-компонентами уникальным образом, чтобы его можно было обновить с помощью внешней конфигурации (. Значение внешней конфигурации.

Так как же этот прицел обеспечивает горячую загрузку? RefreshScope в основном выполняет следующие действия:

Управляйте жизненным циклом компонента отдельно При создании bean-компонента, если это RefreshScope, он кэшируется в специально управляемой ScopeMap, чтобы можно было управлять жизненным циклом bean-компонента, Scope которого Refresh. воссоздать бин После обновления внешней конфигурации будет запущено действие, которое очистит bean-компоненты в ScopeMap выше, таким образом, эти bean-компоненты будут воссозданы контейнером IOC и внедрены в класс с последним значением внешней конфигурации. для достижения горячей загрузки Эффект нового значения Давайте углубимся в исходный код, чтобы проверить наше утверждение выше.

Управление жизненным циклом RefreshBean

Прежде всего, если вам нужен bean-компонент, который может автоматически загружать значения конфигурации в горячем режиме, bean-компонент должен быть аннотирован с помощью @RefreshScope, а затем посмотрите, что делает эта аннотация:

@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 имеет атрибут proxyMode=ScopedProxyMode.TARGET_CLASS, который используется для динамического прокси AOP и будет упомянут позже.

Видно, что это составная аннотация, помеченная @Scope("refresh"), которая изменяет Scope bean-компонента на тип обновления и аннотирует класс BootStrap в SpringBoot с помощью @SpringBootApplication (внутри находится @ComponentScan) , просто Он будет сканировать управляемые аннотациями компоненты в пакете. Когда он сканирует компонент, аннотированный с помощью RefreshScope, он изменит область действия своего BeanDefinition на обновление. В чем польза?

При создании Bean он переходит к методу doGetBean BeanFactory для создания Bean.Различные области имеют разные методы создания:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

  //....

  // Create bean instance.
  // 单例Bean的创建
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  // 原型Bean的创建
  else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
		// ...
    try {
      prototypeInstance = createBean(beanName, mbd, args);
    }
    //...
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  }

  else {
    // 由上面的RefreshScope注解可以知道,这里scopeName=refresh
    String scopeName = mbd.getScope();
    // 获取Refresh的Scope对象
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // 让Scope对象去管理Bean
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }
    //...
  }
}
//...
}

//...
}

Здесь можно увидеть несколько вещей:

  • Bean-компоненты синглтона и прототипа жестко запрограммированы для отдельной обработки.
  • За исключением синглетонов и bean-компонентов-прототипов, другие Scope обрабатываются объектами Scope.
  • Конкретный процесс создания bean-компонента выполняется IOC, но bean-компонент получается через объект Scope.

Здесь объектом Scope, полученным с помощью scope.get, является RefreshScope.Видно, что создание bean-компонентов по-прежнему выполняется IOC (метод createBean), но для получения bean-компонентов используется метод get объекта RefreshScope. метод реализован в родительском классе GenericScope. :

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 将Bean缓存下来
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 创建Bean,只会创建一次,后面直接返回创建好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

Во-первых, Bean запаковывается и кэшируется здесь.

Здесь объектом Scope, полученным с помощью scope.get, является RefreshScope.Видно, что создание bean-компонентов по-прежнему выполняется IOC (метод createBean), но для получения bean-компонентов используется метод get объекта RefreshScope. метод реализован в родительском классе GenericScope. :

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 将Bean缓存下来
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 创建Bean,只会创建一次,后面直接返回创建好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

private final ScopeCache cache;
// 这里进入上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
  return (BeanLifecycleWrapper) this.cache.put(name, value);
}

Объект ScopeCache здесь на самом деле является HashMap:

public class StandardScopeCache implements ScopeCache {

  private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();

  //...

  public Object get(String name) {
    return this.cache.get(name);
  }

  // 如果不存在,才会put进去
  public Object put(String name, Object value) {
    // result若不等于null,表示缓存存在了,不会进行put操作
    Object result = this.cache.putIfAbsent(name, value);
    if (result != null) {
      // 直接返回旧对象
      return result;
    }
    // put成功,返回新对象
    return value;
  }
}

Вот чтобы упаковать боб в объект. На карте, в следующий раз, если вы получите BeaBean, или старый BeanWrapper. Вернуться к применению метода Получить, далее - звонить методом GetBean BeanWRapper:

// 实际Bean对象,缓存下来了
private Object bean;

public Object getBean() {
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

Видно, что переменная компонента бобов в BeanWAPPER - это фактический фасоль. Если первое получение определенно пустое, метод Createbean Cannfactory будет призван создать боб, и он будет сохранен после того, как он будет сохранен.

Видно, что RefreshScope управляет жизненным циклом бинов с Scope=Refresh.

Воссоздать RefreshBean

Когда центр конфигурации обновляет конфигурацию, есть два способа динамического обновления значения переменной конфигурации бина (это почти то же самое для SpringCloud-Bus или Nacos):

  • Опубликовать RefreshEvent в контексте
  • Http-доступ/обновление этой конечной точки

Независимо от того, какой метод обновления режима, в конечном счете, вызывает ContextRefresher этого класса, поэтому мы должны проанализировать вход в принципы расположения тепловой нагрузки:

// 这就是我们上面一直分析的Scope对象(实际上可以看作一个保存refreshBean的Map)
private RefreshScope scope;

public synchronized Set<String> refresh() {
  // 更新上下文中Environment外部化配置值
  Set<String> keys = refreshEnvironment();
  // 调用scope对象的refreshAll方法
  this.scope.refreshAll();
  return keys;
}

Обычно мы используем @value, @configurationproperties для получения значения переменной конфигурации, а нижний уровень находится в IOC для получения значения свойства через контекстный объект Environment, а затем полагаемся на внедрение в отражающий набор для объекта bean.

Итак, если мы обновим значение свойства в среде, затем заново создадим RefreshBean и снова выполним вышеуказанную инъекцию зависимостей, сможем ли мы завершить горячую загрузку конфигурации? Значение переменной @Value может быть загружено как последнее.

Здесь обновление объекта Environment и повторное внедрение зависимостей делают то же, что и два вышеуказанных метода:

  • Set keys = refreshEnvironment();
  • this.scope.refreshAll();

Обновить объект среды

Ниже приведено краткое описание того, как обновить значение свойства в среде.

public synchronized Set<String> refreshEnvironment() {
  // 获取刷新配置前的配置信息,对比用
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  // 刷新Environment
  addConfigFilesToEnvironment();
  // 这里上下文的Environment已经是新的值了
  // 进行新旧对比,结果返回有变化的值
  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对象,copy一份
    StandardEnvironment environment = copyEnvironment(
      this.context.getEnvironment());
    // SpringBoot启动类builder,准备新做一个Spring上下文启动
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
      // banner和web都关闭,因为只是想单纯利用新的Spring上下文构造一个新的Environment
      .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
      // 传入我们刚刚copy的Environment实例
      .environment(environment);
    // 启动上下文
    capture = builder.run();
    // 这个时候,通过上下文SpringIOC的启动,刚刚Environment对象就变成带有最新配置值的Environment了
    // 获取旧的外部化配置列表
    MutablePropertySources target = this.context.getEnvironment()
      .getPropertySources();
    String targetName = null;
    // 遍历这个最新的Environment外部化配置列表
    for (PropertySource<?> source : environment.getPropertySources()) {
      String name = source.getName();
      if (target.contains(name)) {
        targetName = name;
      }
      // 某些配置源不做替换,读者自行查看源码
      // 一般的配置源都会进入if语句
      if (!this.standardSources.contains(name)) {
        if (target.contains(name)) {
          // 用新的配置替换旧的配置
          target.replace(name, source);
        }
        else {
          //....
        }
      }
    }
  }
  //....
}

Видно, что в конечном счете это метод контекста запуска SpringBoot.Создается новый контекст Spring, потому что Spring будет инициализировать Environment в контексте после запуска и получит последнюю конфигурацию.Поэтому запуск SpringBoot здесь используется для получения последней конфигурации Назначение объекта Environment. Затем замените значение конфигурации в объекте Environment в старом контексте.

Воссоздать RefreshBean

После вышеуказанных действий по обновлению объекта Environment значения конфигурации в контексте уже актуальны. Идея восходит к методу обновления ContextRefresher, а затем будет вызываться метод refreshAll объекта Scope:

public void refreshAll() {
  // 销毁Bean
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  // 缓存清空
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  // ...
}

Помните обсуждение кэширования в разделе об управлении жизненным циклом RefreshBean выше, переменная cache — это карта, содержащая экземпляр RefreshBean, и карта очищается непосредственно здесь.

Идея восходит к процессу doGetBean BeanFactory, а получение RefreshBean из IOC-контейнера осуществляется методом get RefreshScope:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 在这里是新的BeanLifecycleWrapper实例调用getBean方法
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

public Object getBean() {
  // 由于是新的BeanLifecycleWrapper实例,这里一定为null
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // 调用IOC容器的createBean,再创建一个Bean出来
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

Видно, что RefreshBean воссоздается контейнером IOC в это время.После функции внедрения зависимостей IOC, @Value является новым значением конфигурации. На этом реализация функции горячей загрузки в основном закончена.

Согласно вышеуказанному анализу, мы видим, что до тех пор, пока мы получаем GetBean из контейнера IOC каждый раз, полученный ремня обновления должна быть бобов с последним значением конфигурации.

конец

До сих пор мы поняли и завершили.Сяо Люлю фактически усвоил общую идею.Многие детали не так ясны.Подготовьтесь к продолжению изучения в будущем.

ежедневные комплименты

Хорошо всем, вышеизложенное является полным содержанием этой статьи. Люди, которые могут видеть это здесь, всенастоящий порошок.

Творить нелегко. Ваша поддержка и признание — самая большая мотивация для моего творчества. Увидимся в следующей статье.

Six Meridians Excalibur | Text [Original] Если в этом блоге есть какие-то ошибки, прошу покритиковать и посоветовать, буду очень признателен!