首页 > 其他分享 >@RefreshScope实现动态刷新配置原理

@RefreshScope实现动态刷新配置原理

时间:2024-04-19 10:12:54浏览次数:26  
标签:String RefreshScope refresh GenericScope bean 刷新 动态 public

1 @RefreshScope介绍

在介绍@RefreshScope之前,先介绍作用域的概念:在spring ioc中存在5种BeanScope,即:

  1. singleton:每一个Spring IoC容器都拥有唯一的一个实例对象(默认作用域)
  2. prototype:一个BeanDefinition对应多个对象实例,每次取出的都是不同的对象
  3. request:每一个HTTP请求都有自己的Bean实例
  4. session:一个Bean的作用域为HTTPsession的生命周期
  5. global session:一个Bean的作用域为全局HTTPSession的生命周期

除此之外,SpringCloud新增了一个名为“refresh”的作用域,目的在于可以在不重启应用的情况下热加载外部配置(yml或properties)。

@RefreshScope注解包含一个枚举类型ScopedProxyMode的属性,默认为TARGET_CLASS即基于类的代理:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	@AliasFor(annotation = Scope.class)
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
public enum ScopedProxyMode {
    DEFAULT,					//不使用代理。(默认)
    NO,								// 不使用代理,等价于DEFAULT。
    INTERFACES,				// 使用基于接口的代理
    TARGET_CLASS;			// 使用基于类的代理(cglib)

    private ScopedProxyMode() {
    }
}

2 @RefreshScope原理

@RefreshScope的实现依赖@Scope注解,其中包含两个属性value 和 proxyMode:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
    @AliasFor("scopeName")
    String value() default "";

    @AliasFor("value")
    String scopeName() default "";

    ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}

反过头来看@RefreshScope就是一个scopeName="refresh"的@Scope注解,

这个代理模式,则是会在生成bean时同时生成名为scopedTarget.beanName的bean,之后的访问通过代理对象来访问,每次访问都会创建一个新的对象。

接着来在介绍RefreshScope类前,先看下其顶级接口Scope接口,其中重点看get方法,get方法在其抽象实现类GenericScope中实现(如下图),实现方式是由GenericScope内部对加了@RefreshScope注解的对象wrapper进行缓存。

public interface Scope {
    Object get(String name, ObjectFactory<?> objectFactory);

    @Nullable
    Object remove(String name);

    void registerDestructionCallback(String name, Runnable callback);

    @Nullable
    Object resolveContextualObject(String key);

    @Nullable
    String getConversationId();
}

首先看GenericScope#get方法:

image-20240416142901538

GenericScope.BeanLifecycleWrapperCache中cache由ScopeCache实现,具体实现细节不深究,缓存中key为beanName,value为bean的生命周期wrapper

image-20240412175139504 image-20240412175450450

重点看GenericScope.BeanLifecycleWrapper#getBean方法,因为调用此方法时,已经将锁存入内部的锁缓存,判断wrapper中是否持有bean,没有的话创建新的bean并存入wrap再返回。从这里的逻辑不难看出,wrapperCache的作用就在于getBean时先从缓存里获取,如果不存在再创建新的bean并放入缓存中。

所以动态刷新就是在配置发生变化时,清除缓存,再重新创建的过程。

再看GenericScope.destroy

image-20240412182009853 image-20240416143126783

接着看GenericScope.get方法中的value.getBean方法,调用wrapper的getBean,而objectFactory.getObject最终调用的是beanFactory.getBean()

image-20240412182757500

ok,看下AbstractBeanFactory#doGetBean中的scope处理逻辑:

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
       // 省略....
                if (mbd.isSingleton()) {
                    // 省略....
                } else if (mbd.isPrototype()) {
                   // 省略....
                } else {
                    String scopeName = mbd.getScope();
                    if (!StringUtils.hasLength(scopeName)) {
                        throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
                    }

                    Scope scope = (Scope)this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                    }

                    try {
                        Object scopedInstance = scope.get(beanName, () -> {
                            this.beforePrototypeCreation(beanName);
                            Object var4;
                            try {
                                var4 = this.createBean(beanName, mbd, args);
                            } finally {
                                this.afterPrototypeCreation(beanName);
                            }
                            return var4;
                        });
                        beanInstance = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    } catch (IllegalStateException var30) {
                        throw new ScopeNotActiveException(beanName, scopeName, var30);
                    }
                }
            } catch (BeansException var32) {
                beanCreation.tag("exception", var32.getClass().toString());
                beanCreation.tag("message", String.valueOf(var32.getMessage()));
                this.cleanupAfterBeanCreationFailure(beanName);
                throw var32;
            } finally {
                beanCreation.end();
            }
        }

        return this.adaptBeanInstance(name, beanInstance, requiredType);
}

else逻辑就是对应refresh作用域的逻辑,重点看scope.get方法(就是GenericScope实现的scope.get,思考1:这里的genericScope从何而来),逻辑是如果缓存中没有,就创建新的bean。此时大家应该对@RefreshScope的实现逻辑有了一定的认知。

ok,接下来看RefreshScope,他是GenericScope的继承类,这个类中暴露出一个比较重要的方法:refreshAll

@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

可以看到代码先去调用父类destroy方法清除缓存,接着发布RefreshScopeRefreshedEvent事件,因为getBean时是先从缓存中获取,如果没有再去创建新的Bean,所以这里清除缓存,就能做到下次获取Bean时拿到到新的Bean,实现刷新。

RefreshScope#refreshAll 方法又被ContextRefresher#refresh方法调用:

image-20240415152504909

ContextRefresher是spring中专门用来刷新RefreshScope的类,至此Springboot提供了用于刷新外部配置类的方法ContextRefresher#refresh,那么如何实现动态刷新呢?先来看下这个方法被谁调用:

  1. SpringBoot Actuator:RefreshEndpoint#refresh
  2. RefreshEventListener:RefreshEventListener#handle(RefreshEvent event)

其调用源码分别如下:

@Endpoint(id = "refresh")
public class RefreshEndpoint {

   private ContextRefresher contextRefresher;

   public RefreshEndpoint(ContextRefresher contextRefresher) {
      this.contextRefresher = contextRefresher;
   }

   @WriteOperation
   public Collection<String> refresh() {
      Set<String> keys = this.contextRefresher.refresh();
      return keys;
   }
}
public class RefreshEventListener implements SmartApplicationListener {

	private static Log log = LogFactory.getLog(RefreshEventListener.class);

	private ContextRefresher refresh;

	private AtomicBoolean ready = new AtomicBoolean(false);

	public RefreshEventListener(ContextRefresher refresh) {
		this.refresh = refresh;
	}
	// 省略其他方法...
	public void handle(ApplicationReadyEvent event) {
		this.ready.compareAndSet(false, true);
	}

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

所以我们可以在引入actuator后手动刷新:通过发起请求http://localhost:8080/actuator/refresh主动刷新,或者publish一个RefreshEvent事件,接下来会分别用Apollo和Nacos说明。

至此,我们可以得出调用链(用actuator举例):RefreshEndpoint#refresh -> ContextRefresher#refresh -> RefreshScope#refreshAll -> GenericScope.destroy

3 配置动态刷新应用举例

3.2 Nacos中

Nacos里定义了NacosContextRefresher,在其registerNacosListener方法中publish了RefreshEvent事件。

image-20240416172048455

3.2 Apollo中

apollo中的配置监听器注解:@ApolloConfigChangeListener,我们可以在用注解修饰的方法中刷新配置。思考2:这里的RefreshScope从何而来。

@Slf4j
@Configuration
public class ApolloChangeListener {
    @Autowired
    private RefreshScope refreshScope;

    @ApolloConfigChangeListener(value = "application.yml", interestedKeyPrefixes = "my.config.")
    public void refresh(ConfigChangeEvent changeEvent) {
        log.info("Changes for namespace " + changeEvent.getNamespace());
        for(String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
            log.info(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
        }
        // 刷新所有的bean
        refreshScope.refreshAll();
        // 刷新指定的bean
//        refreshScope.refresh("myConfigProperties");
    }
}

@ApolloConfigChangeListener注解的处理是通过拦截器拦截,并且创建配置监听,当配置发生变化时反射调用refreshAll方法刷新外部配置类。

/***
 * 方法处理
 * @param bean
 * @param beanName
 * @param method
 */
@Override
protected void processMethod(final Object bean, String beanName, final Method method) {
  //检查该方法是否有@ApolloConfigChangeListener注解
  ApolloConfigChangeListener annotation = AnnotationUtils
      .findAnnotation(method, ApolloConfigChangeListener.class);
  //没有就直接返回
  if (annotation == null) {
    return;
  }
  //获取参数类型集合
  Class<?>[] parameterTypes = method.getParameterTypes();
  Preconditions.checkArgument(parameterTypes.length == 1,
      "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
      method);
  Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
      "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
      method);
  ReflectionUtils.makeAccessible(method);
  //获取命名空间
  String[] namespaces = annotation.value();
  //获取要监听的key
  String[] annotatedInterestedKeys = annotation.interestedKeys();
  //获取要监听的key的前缀集合
  String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
  //创建监听
  ConfigChangeListener configChangeListener = new ConfigChangeListener() {
    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
      //执行方法调用
      ReflectionUtils.invokeMethod(method, bean, changeEvent);
    }
  };

  Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
  Set<String> interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null;

  // 给config设置listener
  for (String namespace : namespaces) {
    Config config = ConfigService.getConfig(namespace);
    //为每个命名空间添加configChangeListener,当每个命名空间发生变化的时候,都会触发该configChangeListener执行
    if (interestedKeys == null && interestedKeyPrefixes == null) {
      config.addChangeListener(configChangeListener);
    } else {
      config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
    }
  }
}

4 思考与补充

思考:RefreshScopeGenericScope以及被@RefreshScope修饰的类是什么时候注册到容器的?

1.RefreshScope通过RefreshAutoConfiguration这个类自动装配到容器:

image-20240416181335702

2.GenericScope通过BPP扩展点在容器启动后调用ConfigurableBeanFactory.registerScope注册到容器:

image-20240416184029079 image-20240416184129808

3.被@RefreshScope修饰的Bean,在容器启动时通过AnnotatedBeanDefinitionReader#doRegisterBean注册到IOC容器中,并设置其作用域为refresh。

image-20240418182918674

本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。

标签:String,RefreshScope,refresh,GenericScope,bean,刷新,动态,public
From: https://www.cnblogs.com/zhaobo1997/p/18145195

相关文章

  • java动态代理模式
    Java动态代理模式是Java编程语言中的一种设计模式,它提供了一种在运行时动态创建代理对象的方式。这个模式主要用于实现AOP(面向切面编程)的概念,允许开发者在不修改原有业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理、权限验证等。在Java中,动态代理模式主要依赖于java.l......
  • Trino418版本动态加载catalog不需要重启集群修改思路及实现2
       原来没事的时候改了一个这样的功能,当时也没有仔细研究,后来也没继续弄。详细可以参考 https://www.cnblogs.com/liuzx8888/p/17635913.html当时有1个问题:新增数据源需要每一个节点都去调取API注册,这样非常麻烦,最近闲下来又研究了一下,在原先的基础上做了一些改造。具体流......
  • 洛谷题单指南-动态规划1-P1115 最大子段和
    原题链接:https://www.luogu.com.cn/problem/P1115题意解读:计算最大字段和,典型dp问题。解题思路:设a[]表示所有整数,f[i]表示以第i个数结束的最大字段和当f[i-1]>=0时,f[i]=f[i-1]+a[i]否则,f[i]=a[i]因此,递归式为f[i]=max(a[i],f[i-1]+a[i])注意整数可能为负,ans初始......
  • Pyecharts制作动态GDP柱状图
    学习使用pyecharts制作动态柱状图使用csv模块进行csv数据文件处理importcsvfrompyecharts.chartsimportBar,Timelinefrompyecharts.optionsimport*frompyecharts.globalsimportThemeTypedefdealCSVFile():"""读取处理csv数据文件:retu......
  • 洛谷题单指南-动态规划1-P1434 [SHOI2002] 滑雪
    原题链接:https://www.luogu.com.cn/problem/P1434题意解读:计算能滑行的最长距离。解题思路:设dp(i,j)表示从i,j可以滑行的最大距离对于4个方向i,j可以到达的点,ni,nj,如果可以滑过去(ni,ni所在点高度更低)则dp(i,j)=max(dp(i,j),1+dp(ni,nj))为了便于搜索4个方向的各条路径,......
  • EAS_DEP添加动态控件,在代码中获取DEP扩展控件
    1.在编辑界面onload的方法前置事件添加脚本//把动态控件传递到代码中varcomponents=newjava.util.HashMap();components.put("prmtassureAmountAccount",pluginCtx.getKDBizPromptBox("prmtassureAmountAccount"));components.put("prmtassureInterestAccount",......
  • 洛谷题单指南-动态规划1-P2196 [NOIP1996 提高组] 挖地雷
    原题链接:https://www.luogu.com.cn/problem/P2196题意解读:求一条路径,使得所有点地雷数之和最大。解题思路:1、DFS先建图,再从1~n点分别进行一次DFS,记录过程中地雷总数最大的,并且同时记录遍历的顺序。数据量不大,直接就可以过。100分代码:#include<bits/stdc++.h>usingnamespa......
  • 洛谷题单指南-动态规划1-P1216 [USACO1.5] [IOI1994]数字三角形 Number Triangles
    原题链接:https://www.luogu.com.cn/problem/P1216题意解读:计算数字三角形最高点到最后一行路径之和最大值,典型线性DP。解题思路:设a[i][j]表示数字三角形的值,设dp[i][j]表示从最高点到第i行第j列路径之和的最大值,由于每一步可以走到左下方的点也可以到达右下方的点,所以dp[i][......
  • java多线程 读取list--动态读取list
    java多线程读取list--动态读取list的案例 本次介绍,我使用的是synchronized同步代码块的关键字来读取list,在写java多线程时,一定要注意synchronized关键字的有效范围。ps:如果synchronized关键字的代码块范围太大,可能会导致优先获取到cpu资源的第一个线程在满足条件的情......
  • 动态规划、回溯、BFS、二分、滑动窗口总结
    动态规划动态规划的核心问题:重叠子问题,最优子结构,状态转移方程动态规划与记忆递归的区别:记忆递归为自顶而上的递归剪枝,动态规划为自底向上的循环迭代;正确的状态转移方程+dp[]数组:确定状态(原问题和子问题中变化的变量)->确定dp数组的定义dp[i]->确定当前状态的'选择'并确定......