Spring Cloud: principio de actualización dinámica @RefreshScope

URL original: Spring Cloud: el principio de actualización dinámica de @RefreshScope_IT Knives Out Blog-CSDN Blog

Introducción

Este artículo presenta el principio de actualización dinámica de @RefreshScope de Spring Cloud.

Resumen de principios

Los alcances de Spring incluyen: único (caso único), prototipo (casos múltiples), etc. (Para obtener más detalles, consulte: Spring - Alcance de Bean (alcance) - Uso / explicación detallada_IT Knives Out Blog-CSDN Blog ).

SpringCloud ha agregado un nuevo alcance personalizado: actualización (puede entenderse como "actualización dinámica"), que cambia la forma en que se administran los Beans para que se puedan actualizar a través de una configuración externa (.yml o .properties) sin reiniciar. valores de configuración externalizados en caso de aplicación.

¿Cómo logra este alcance la carga en caliente? RefreshScope realiza principalmente las siguientes acciones:

  1. Las clases que deben actualizarse dinámicamente se anotan con la anotación @RefreshScope (administre el ciclo de vida del Bean por separado)
    1. La anotación @RefreshScope marca @Scope y tiene un atributo predeterminado: atributo ScopedProxyMode.TARGET_CLASS; La función de este atributo es crear otro proxy, que se utiliza para llamar al método get de GenericScope para obtener el objeto cada vez. se llama.
    2. Entonces, al crear un Bean, si hay @RefreshScope, se almacena en caché en un ScopeMap (por lo que el Bean con @RefreshScope en realidad crea un total de dos beans).
  2. Si las propiedades cambian
    1. Llame a ContextRefresher refresco() => RefreshScope refrescoAll() para limpieza de caché
    2. Envíe una notificación de evento de actualización y llame a destroy() de GenericScope para limpiar el caché. Aquí, borre los Beans en el ScopeMap anterior.
  3. la próxima vez que se utilice el objeto
    1. Llame al método GenericScope get(String name, ObjectFactory<?> objectFactory) para crear un nuevo objeto y utilice el último valor de configuración externalizado para inyectarlo en la clase para lograr el efecto de carga en caliente de nuevos valores.

Obtención de beans gestionados

Anotación @RefreshScope

Si un Bean quiere cargar automáticamente la configuración, debe estar anotado con @RefreshScope. Mire esta anotación.

@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 tiene un atributo: proxyMode=ScopedProxyMode.TARGET_CLASS, que se utiliza para el proxy dinámico AOP.

@Scope("refresh") está marcado arriba, cambiando el alcance del Bean para actualizar. Agregaremos la anotación @SpringBootApplication (con un @ComponentScan dentro) a la clase de inicio SpringBoot. Escaneará los beans registrados a través de la anotación en el paquete. Cuando escanee los beans con la anotación @RefreshScope, cambiará el alcance de su BeanDefinition para actualizar.

Diferenciar alcances al crear beans

Al crear un Bean, irá al método doGetBean de BeanFactory para crear el Bean. Diferentes ámbitos tienen diferentes métodos de creación: el método doGetBean de AbstractBeanFactory:

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);
        }
        //...
    }
}
  1. A excepción de los singleton y los beans prototipo, otros ámbitos son manejados por objetos Scope.
  2. El proceso específico de creación de un bean lo realiza el IOC, pero el bean se obtiene a través del objeto Scope.

El objeto Scope obtenido aquí por alcance.get es RefreshScope. Se puede ver que la creación de un Bean todavía se realiza mediante IOC (método createBean), pero la obtención del Bean se obtiene mediante el método get del objeto RefreshScope, y su método get se implementa en la clase principal GenericScope.

Actualizar alcanceGetBean

RefreshScope hereda la clase GenericScope y finalmente llama al método get de GenericScope:

public class GenericScope
    implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {

    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;
        }
    }
    
    // 其他代码
}

poner en caché

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);
}

El objeto ScopeCache aquí es un 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;
    }
}

Aquí, el Bean se empaqueta en un objeto y se almacena en caché en un Mapa. ​​Si obtienes Bean la próxima vez, seguirá siendo el antiguo BeanWrapper.

Volviendo al método get de Scope, el siguiente paso es llamar al método getBean de 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;
}

Se puede ver que la variable de bean en BeanWrapper es el bean real. Si el primer get está definitivamente vacío, se llamará al método createBean de BeanFactory para crear el bean y se guardará después de su creación.

Se puede ver que RefreshScope administra el ciclo de vida del Bean con Scope = Refresh.

Recrear RefreshBean

Después de que el centro de configuración modifica la configuración, hay dos formas de actualizar dinámicamente el valor de configuración del Bean (SpringCloud-Bus y Nacos se implementan de esta manera):

  1. Publicar un evento RefreshEvent en el contexto
  2. Acceso HTTP/actualización de este EndPoint

Independientemente del método, se llamará al método ContextRefresher#refresh().

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

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

Actualizar plan

Generalmente usamos @Value y @ConfigurationProperties para obtener el valor de la variable de configuración. La capa inferior en IOC es obtener el valor de la propiedad a través del objeto de entorno del contexto y luego escribirlo en el objeto Bean mediante la reflexión durante el IOC.

Si actualizamos el valor de la propiedad en el entorno, volvemos a crear el RefreshBean y luego realizamos la inyección de dependencia anterior, se puede completar la carga en caliente de la configuración y el valor de la variable @Value se puede cargar como el último.

Actualizar el objeto Entorno y reinyectar las dependencias mencionadas aquí es lo que hacen los dos métodos en actualizar() anteriores:

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

Actualizar objeto de entorno

Presente brevemente cómo actualizar el valor de la propiedad en el entorno.

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;
}

El foco es el método addConfigFilesToEnvironment (actualizar entorno)

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 {
                    //....
                }
            }
        }
    }
    //....
}

Esta es la forma en que SpringBoot inicia el contexto y crea un nuevo contexto Spring. Debido a que Spring inicializará el Entorno en el contexto después del inicio y obtendrá la última configuración, aquí usamos el inicio de Spring para obtener el último objeto de Entorno y luego reemplazamos el valor de configuración en el objeto de Entorno en el contexto anterior.

Recrear RefreshBean

Después de las acciones anteriores, el valor de configuración en el contexto ya está actualizado. Volviendo al método de actualización de ContextRefresher, llamará al método de actualización de 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();
    // ...
}

¿Todavía recuerda la discusión sobre el almacenamiento en caché en "Adquisición de beans de administración" arriba? La variable de caché es un mapa que contiene instancias de RefreshBean. El mapa se borra directamente aquí.

Volviendo al proceso doGetBean de BeanFactory, la obtención de RefreshBean del contenedor IOC se realiza mediante el método get de 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;
}

En este momento, el contenedor IOC vuelve a crear RefreshBean y, a través de la función de inyección de dependencia de IOC, @Value es una nueva configuración. En este punto, la implementación de la función de recarga en caliente está básicamente completa.

Se puede ver que siempre que obtenga Bean del contenedor IOC, el RefreshBean que obtenga debe ser el Bean con el último valor de configuración.

Supongo que te gusta

Origin blog.csdn.net/feiying0canglang/article/details/129249938
Recomendado
Clasificación