2021-09-28

前言

在SpringIOC中,我们熟知的BeanScope有单例(singleton)、原型(prototype), Bean的Scope影响了Bean的创建方式,例如创建Scope=singleton的Bean时,IOC会保存实例在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。SpringCloud新增了一个refresh范围的scope,同样用了一种独特的方式改变了Bean的创建方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。

那么这个scope是如何做到热加载的呢?RefreshScope主要做了以下动作:

  • 单独管理Bean生命周期
    • 创建Bean的时候如果是RefreshScope就缓存在一个专门管理的ScopeMap中,这样就可以管理Scope是Refresh的Bean的生命周期了
  • 重新创建Bean
    • 外部化配置刷新之后,会触发一个动作,这个动作将上面的ScopeMap中的Bean清空,这样,这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果

下面我们深入源码,来验证我们上述的讲法。

1. 管理RefreshBean的生命周期

首先,若想要一个Bean可以自动热加载配置值,这个Bean要被打上@RefreshScope注解,那么就看看这个注解做了什么:


   
   
     
     
     
     
  1. @Target({ ElementType.TYPE, ElementType.METHOD })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Scope("refresh")
  4. @Documented
  5. public @interface RefreshScope {
  6. /**
  7. * @see Scope#proxyMode()
  8. * @return proxy mode
  9. */
  10. ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
  11. }

可以发现RefreshScope有一个属性 proxyMode=ScopedProxyMode.TARGET_CLASS,这个是AOP动态代理用,之后会再来提这个

可以看出其是一个复合注解,被标注了 @Scope("refresh") ,其将Bean的Scope变为refresh这个类型,在SpringBoot中BootStrap类上打上@SpringBootApplication注解(里面是一个@ComponentScan),就会扫描包中的注解驱动Bean,扫描到打上RefreshScope注解的Bean的时候,就会将其的BeanDefinition的scope变为refresh,这有什么用呢?

创建一个Bean的时候,会去BeanFactory的doGetBean方法创建Bean,不同scope有不同的创建方式:


   
   
     
     
     
     
  1. protected T doGetBean(final String name, @Nullable final Class requiredType,
  2. @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
  3. //....
  4. // Create bean instance.
  5. // 单例Bean的创建
  6. if (mbd.isSingleton()) {
  7. sharedInstance = getSingleton(beanName, () -> {
  8. try {
  9. return createBean(beanName, mbd, args);
  10. }
  11. //...
  12. });
  13. bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  14. }
  15. // 原型Bean的创建
  16. else if (mbd.isPrototype()) {
  17. // It's a prototype -> create a new instance.
  18. // ...
  19. try {
  20. prototypeInstance = createBean(beanName, mbd, args);
  21. }
  22. //...
  23. bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  24. }
  25. else {
  26. // 由上面的RefreshScope注解可以知道,这里scopeName=refresh
  27. String scopeName = mbd.getScope();
  28. // 获取Refresh的Scope对象
  29. final Scope scope = this.scopes.get(scopeName);
  30. if (scope == null) {
  31. throw new IllegalStateException( "No Scope registered for scope name '" + scopeName + "'");
  32. }
  33. try {
  34. // 让Scope对象去管理Bean
  35. Object scopedInstance = scope.get(beanName, () -> {
  36. beforePrototypeCreation(beanName);
  37. try {
  38. return createBean(beanName, mbd, args);
  39. }
  40. finally {
  41. afterPrototypeCreation(beanName);
  42. }
  43. });
  44. bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
  45. }
  46. //...
  47. }
  48. }
  49. //...
  50. }
  51. //...
  52. }

这里可以看到几件事情:

  • 单例和原型scope的Bean是硬编码单独处理的
  • 除了单例和原型Bean,其他Scope是由Scope对象处理的
  • 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象

这里scope.get获取的Scope对象为RefreshScope,可以看到,创建Bean还是由IOC来做(createBean方法),但是获取Bean,都由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现:


   
   
     
     
     
     
  1. public Object get(String name, ObjectFactory objectFactory) {
  2. // 将Bean缓存下来
  3. BeanLifecycleWrapper value = this.cache.put(name,
  4. new BeanLifecycleWrapper(name, objectFactory));
  5. this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  6. try {
  7. // 创建Bean,只会创建一次,后面直接返回创建好的Bean
  8. return value.getBean();
  9. }
  10. catch (RuntimeException e) {
  11. this.errors.put(name, e);
  12. throw e;
  13. }
  14. }

首先这里将Bean包装起来缓存下来


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

这里的ScopeCache对象其实就是一个HashMap:


   
   
     
     
     
     
  1. public class StandardScopeCache implements ScopeCache {
  2. private final ConcurrentMap cache = new ConcurrentHashMap();
  3. //...
  4. public Object get(String name) {
  5. return this.cache.get(name);
  6. }
  7. // 如果不存在,才会put进去
  8. public Object put(String name, Object value) {
  9. // result若不等于null,表示缓存存在了,不会进行put操作
  10. Object result = this.cache.putIfAbsent(name, value);
  11. if (result != null) {
  12. // 直接返回旧对象
  13. return result;
  14. }
  15. // put成功,返回新对象
  16. return value;
  17. }
  18. }
  • 1

这里就是将Bean包装成一个对象,缓存在一个Map中,下次如果再GetBean,还是那个旧的BeanWrapper。回到Scope的get方法,接下来就是调用BeanWrapper的getBean方法:


   
   
     
     
     
     
  1. // 实际Bean对象,缓存下来了
  2. private Object bean;
  3. public Object getBean() {
  4. if ( this.bean == null) {
  5. synchronized ( this.name) {
  6. if ( this.bean == null) {
  7. this.bean = this.objectFactory.getObject();
  8. }
  9. }
  10. }
  11. return this.bean;
  12. }

可以看出来,BeanWrapper中的bean变量即为实际Bean,如果第一次get肯定为空,就会调用BeanFactory的createBean方法创建Bean,创建出来之后就会一直保存下来。

由此可见,RefreshScope管理了Scope=Refresh的Bean的生命周期。

2. 重新创建RefreshBean

当配置中心刷新配置之后,有两种方式可以动态刷新Bean的配置变量值,(SpringCloud-Bus还是Nacos差不多都是这么实现的):

  • 向上下文发布一个RefreshEvent事件
  • Http访问/refresh这个EndPoint

不管是什么方式,最终都会调用ContextRefresher这个类的refresh方法,那么我们由此为入口来分析一下,热加载配置的原理:

我们一般是使用@Value、@ConfigurationProperties去获取配置变量值,其底层在IOC中则是通过上下文的Environment对象去获取property值,然后依赖注入利用反射Set到Bean对象中去的。

那么如果我们更新Environment里的Property值,然后重新创建一次RefreshBean,再进行一次上述的依赖注入,是不是就能完成配置热加载了呢?@Value的变量值就可以加载为最新的了。

这里说的刷新Environment对象并重新依赖注入则为上述两个方法做的事情:

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

2.1 刷新Environment对象

下面简单介绍一下如何刷新Environment里的Property值


   
   
     
     
     
     
  1. public synchronized Set refreshEnvironment() {
  2. // 获取刷新配置前的配置信息,对比用
  3. Map before = extract(
  4. this.context.getEnvironment().getPropertySources());
  5. // 刷新Environment
  6. addConfigFilesToEnvironment();
  7. // 这里上下文的Environment已经是新的值了
  8. // 进行新旧对比,结果返回有变化的值
  9. Set keys = changes(before,
  10. extract( this.context.getEnvironment().getPropertySources())).keySet();
  11. this.context.publishEvent( new EnvironmentChangeEvent( this.context, keys));
  12. return keys;
  13. }

我们的重点在addConfigFilesToEnvironment方法,刷新Environment:


   
   
     
     
     
     
  1. ConfigurableApplicationContext addConfigFilesToEnvironment() {
  2. ConfigurableApplicationContext capture = null;
  3. try {
  4. // 从上下文拿出Environment对象,copy一份
  5. StandardEnvironment environment = copyEnvironment(
  6. this.context.getEnvironment());
  7. // SpringBoot启动类builder,准备新做一个Spring上下文启动
  8. SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
  9. // banner和web都关闭,因为只是想单纯利用新的Spring上下文构造一个新的Environment
  10. .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
  11. // 传入我们刚刚copy的Environment实例
  12. .environment(environment);
  13. // 启动上下文
  14. capture = builder.run();
  15. // 这个时候,通过上下文SpringIOC的启动,刚刚Environment对象就变成带有最新配置值的Environment了
  16. // 获取旧的外部化配置列表
  17. MutablePropertySources target = this.context.getEnvironment()
  18. .getPropertySources();
  19. String targetName = null;
  20. // 遍历这个最新的Environment外部化配置列表
  21. for (PropertySource source : environment.getPropertySources()) {
  22. String name = source.getName();
  23. if (target.contains(name)) {
  24. targetName = name;
  25. }
  26. // 某些配置源不做替换,读者自行查看源码
  27. // 一般的配置源都会进入 if语句
  28. if (! this.standardSources.contains(name)) {
  29. if (target.contains(name)) {
  30. // 用新的配置替换旧的配置
  31. target.replace(name, source);
  32. }
  33. else {
  34. //....
  35. }
  36. }
  37. }
  38. }
  39. //....
  40. }

可以看到,这里归根结底就是SpringBoot启动上下文那种方法,新做了一个Spring上下文,因为Spring启动后会对上下文中的Environment进行初始化,获取最新配置,所以这里利用Spring的启动,达到了获取最新的Environment对象的目的。然后去替换旧的上下文中的Environment对象中的配置值即可。

2.2 重新创建RefreshBean

经过上述刷新Environment对象的动作,此时上下文中的配置值已经是最新的了。思路回到ContextRefresher的refresh方法,接下来会调用Scope对象的refreshAll方法:


   
   
     
     
     
     
  1. public void refreshAll() {
  2. // 销毁Bean
  3. super.destroy();
  4. this.context.publishEvent( new RefreshScopeRefreshedEvent());
  5. }
  6. public void destroy() {
  7. List errors = new ArrayList();
  8. // 缓存清空
  9. Collection wrappers = this.cache.clear();
  10. // ...
  11. }

还记得上面的管理RefreshBean生命周期那一节关于缓存的讨论吗,cache变量是一个Map保存着RefreshBean实例,这里直接就将Map清空了。

思路回到BeanFactory的doGetBean的流程中,从IOC容器中获取RefreshBean是交给RefreshScope的get方法做的:


   
   
     
     
     
     
  1. public Object getBean() {
  2. // 由于是新的BeanLifecycleWrapper实例,这里一定为null
  3. if ( this.bean == null) {
  4. synchronized ( this.name) {
  5. if ( this.bean == null) {
  6. // 调用IOC容器的createBean,再创建一个Bean出来
  7. this.bean = this.objectFactory.getObject();
  8. }
  9. }
  10. }
  11. return this.bean;
  12. }

可以看到,此时RefreshBean被IOC容器重新创建一个出来了,经过IOC的依赖注入功能,@Value的就是一个新的配置值了。到这里热加载功能实现基本结束。

根据以上分析,我们可以看出只要每次我们都从IOC容器中getBean,那么拿到的RefreshBean一定是带有最新配置值的Bean。

3. 动态刷新的应用

在我们正常使用@RefreshScope的时候,也没有做一些getBean的操作,为什么也可以动态刷新呢?因为Spring利用AOP动态代理了原先的Bean,在调用Bean的方法前,会拦截并从IOC容器中getBean,然后针对返回的新Bean做方法调用,这样就达到了使用的配置值一直是最新的效果了。下面我们来分析分析这AOP动态代理的过程。

3.1 动态代理RefreshBean

在本人另一篇文章, SpringBoot自动装配的魔法 中有讲到,SpringBoot的注解驱动注册Bean是由ConfigurationClassPostProcessor类来做的,其中@ComponentScan会扫描并注册包下带有@Componet注解的类为Bean,达到一个注解驱动注册Bean的效果。而扫描Bean并注册为BeanDefinition这一过程是由ClassPathBeanDefinitionScanner类的doScan方法去做的,我们先来看看扫描Bean的时候,对于RefreshScope的Bean有什么特殊处理:


   
   
     
     
     
     
  1. protected Set doScan(String... basePackages) {
  2. Assert.notEmpty(basePackages, "At least one base package must be specified");
  3. Set beanDefinitions = new LinkedHashSet<>();
  4. // 扫描basePackages所在的包下的所有的类,带@Componet的类都会被注册为BeanDefinition
  5. for (String basePackage : basePackages) {
  6. //...
  7. if (checkCandidate(beanName, candidate)) {
  8. BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
  9. // 这里就是Scope的Bean的统一处理,是一个改变BeanDefinition的回调机会
  10. definitionHolder =
  11. AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
  12. beanDefinitions.add(definitionHolder);
  13. // 注册BeanDefinition到IOC容器中
  14. registerBeanDefinition(definitionHolder, this.registry);
  15. }
  16. }
  17. }
  18. return beanDefinitions;
  19. }

其中关键在于AnnotationConfigUtils的applyScopedProxyMode方法:


   
   
     
     
     
     
  1. static BeanDefinitionHolder applyScopedProxyMode(
  2. ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
  3. // 获取Scope的代理模式
  4. ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
  5. // 如果代理模式为NO,就不进行代理了
  6. if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
  7. return definition;
  8. }
  9. boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
  10. // 进行代理
  11. return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
  12. }

回顾开头分析的@RefreshScope注解,其proxyMode值为ScopedProxyMode.TARGET_CLASS


   
   
     
     
     
     
  1. public @interface RefreshScope {
  2. /**
  3. * @see Scope#proxyMode()
  4. * @return proxy mode
  5. */
  6. ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
  7. }

所以这里被打上@RefreshScope的Bean类会进入接下来ScopedProxyCreator的createScopedProxy方法:


   
   
     
     
     
     
  1. public static BeanDefinitionHolder createScopedProxy(
  2. BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) {
  3. return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass);
  4. }

   
   
     
     
     
     
  1. public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
  2. BeanDefinitionRegistry registry, boolean proxyTargetClass) {
  3. // ...
  4. // Create a scoped proxy definition for the original bean name,
  5. // "hiding" the target bean in an internal target definition.
  6. // 重点,这里构造函数中将beanClass设置为了ScopedProxyFactoryBean.class
  7. RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  8. // targetDefinition是被代理的原生Bean
  9. proxyDefinition.setDecoratedDefinition( new BeanDefinitionHolder(targetDefinition, targetBeanName));
  10. // ...
  11. // Return the scoped proxy definition as primary bean definition
  12. // (potentially an inner bean).
  13. return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
  14. }

这里一堆构建BeanDefinition的逻辑,先不看,只关注一件事,这里将BeanDefinition的beanClass设置为了ScopedProxyFactoryBean.class,而Scope的通用处理类GenericScope类是一个BeanDefinitionRegistryPostProcessor,其在postProcessBeanDefinitionRegistry回调方法中会针对刚刚那个beanClass为ScopedProxyFactoryBean.class的BeanDefinition做一个特殊的处理:


   
   
     
     
     
     
  1. // GenericScope.class
  2. public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
  3. throws BeansException {
  4. // 获取所有BeanDefinition的名称
  5. for (String name : registry.getBeanDefinitionNames()) {
  6. BeanDefinition definition = registry.getBeanDefinition(name);
  7. // 针对RootBeanDefinition这个BeanDefinition来做,这和上面的逻辑吻合
  8. if (definition instanceof RootBeanDefinition) {
  9. RootBeanDefinition root = (RootBeanDefinition) definition;
  10. // 判断BeanClass == ScopedProxyFactoryBean.class
  11. if (root.getDecoratedDefinition() != null && root.hasBeanClass()
  12. && root.getBeanClass() == ScopedProxyFactoryBean.class) {
  13. if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
  14. .getScope())) {
  15. // 将BeanClass换为LockedScopedProxyFactoryBean
  16. root.setBeanClass(LockedScopedProxyFactoryBean.class);
  17. root.getConstructorArgumentValues().addGenericArgumentValue( this);
  18. // surprising that a scoped proxy bean definition is not already
  19. // marked as synthetic?
  20. root.setSynthetic( true);
  21. }
  22. }
  23. }
  24. }
  25. }

到这里可以知道,GenericScope将ScopeBean,变为LockedScopedProxyFactoryBean这个类

在这里插入图片描述

然而这个类又是一个FactoryBean,由其父类ScopedProxyFactoryBean的getObject方法实现FactoryBean接口,我们知道,创建一个FactoryBean,其实最终会调用其getObject方法,这个方法的返回值才是最终被创建出来的Bean实例,所以我们的重点就在getObject方法中:


   
   
     
     
     
     
  1. public Object getObject() {
  2. if ( this.proxy == null) {
  3. throw new FactoryBeanNotInitializedException();
  4. }
  5. return this.proxy;
  6. }

似乎早就被动态代理好了,全局搜索proxy变量在哪里被赋值。可以发现,ScopedProxyFactoryBean还是一个BeanFactoryAware,其setBeanFactory会在比较早的时机被回调:


   
   
     
     
     
     
  1. public void setBeanFactory(BeanFactory beanFactory) {
  2. // ...
  3. // 这里是一个比较关键的点,scopedTargetSource变量是一个SimpleBeanTargetSource
  4. // scopedTargetSource中保存了IOC容器
  5. this.scopedTargetSource.setBeanFactory(beanFactory);
  6. // 创建动态代理前,将动态代理的信息都保存到ProxyFactory中
  7. ProxyFactory pf = new ProxyFactory();
  8. pf.copyFrom( this);
  9. // 注意,这里的TargetSource就是刚刚说的scopedTargetSource
  10. pf.setTargetSource( this.scopedTargetSource);
  11. // ...
  12. this.proxy = pf.getProxy(cbf.getBeanClassLoader());
  13. }

这里先记住,scopedTargetSource是SimpleBeanTargetSource这个类就行,其保存了IOC容器。

接着调用pf的getProxy方法开始进行动态代理

下面就是AOP的一些逻辑了,不是本篇文章讨论的重点,一笔带过


   
   
     
     
     
     
  1. private Callback[] getCallbacks(Class rootClass) throws Exception {
  2. // ...
  3. // Choose an "aop" interceptor (used for AOP calls).
  4. // 这里的MethodInterceptor实例是DynamicAdvisedInterceptor这个类
  5. Callback aopInterceptor = new DynamicAdvisedInterceptor( this.advised);
  6. // ...
  7. return callbacks;
  8. }

我们知道,CGLib动态代理都会实现一个MethodInterceptor,被代理的类的每一个方法调用实质上都是在调用MethodInterceptor的intercept方法,那么我们看看DynamicAdvisedInterceptor这个类的intercept方法:


   
   
     
     
     
     
  1. public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  2. // 记得吗,刚刚我们反复强调的TargetSource
  3. TargetSource targetSource = this.advised.getTargetSource();
  4. try {
  5. // ...
  6. // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
  7. // 重点,这里调用了targetSource的getTarget
  8. // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
  9. target = targetSource.getTarget();
  10. // ...
  11. if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
  12. // ...
  13. // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
  14. retVal = methodProxy.invoke(target, argsToUse);
  15. }
  16. else {
  17. // We need to create a method invocation...
  18. // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
  19. retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
  20. }
  21. // target变量就是被代理的类,调用实际方法的时候反射调用其对应方法
  22. retVal = processReturnType(proxy, target, method, retVal);
  23. return retVal;
  24. }
  25. // ...
  26. }

直接看重点,targetSource的getTarget()方法的返回值是被代理的类,那么这个getTarget做了什么逻辑呢?回顾上面来看我们知道这里TargetSource的实现类是SimpleBeanTargetSource:


   
   
     
     
     
     
  1. public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
  2. @Override
  3. public Object getTarget() throws Exception {
  4. // 从IOC中getBean
  5. return getBeanFactory().getBean(getTargetBeanName());
  6. }
  7. }

到这里就已经揭晓了答案,原来被打上@RefreshScope的Bean都会被Spring做AOP动态代理,每次调用方法之前,都会去IOC中调用getBean方法获取真正的原始Bean,而原始Bean又被存放在GenericScope对象中的Map里,在refresh刷新配置的时候会清空缓存Map,在刷新配置的时候,调用类方法前去IOC获取Bean,然后到GenericScope查看缓存,发现没有这个Bean缓存就会重新从IOC容器创建一份Bean,依赖注入配置属性值的时候注入的就是最新的值了,这样就能达到动态刷新的作用。

3.2 Refresh动态刷新失效问题

大部分场景下RefreshBean动态刷新是不会失效的,但是笔者在使用WebFlux的时候,用到WebFilter过滤器时出现了失效的问题。这里从现象看本质,如果你的应用Refresh也失效了,大概率也是跟我差不多问题,知道了原理,解决这个问题会变得十分简单。

这里重点要看你的Bean是如何被加载使用的,这里看看常见的两个方式是如何使用的:

  • Controller:在Controller中,@Value依赖注入一个外部化配置值,是否能得到动态刷新呢?
  • @Autowired:依赖注入任意一个Bean,注入的Bean中有@Value一个外部化配置值,是否能得到动态刷新呢?

3.2.1 Controller的使用


   
   
     
     
     
     
  1. @RestController
  2. @RefreshScope
  3. public class TestController {
  4. @Value("${test.refresh}")
  5. private boolean test;
  6. @GetMapping("/get/refresh/test/value")
  7. public boolean test() {
  8. return test;
  9. }
  10. }

涉及一些SpringMVC的知识,这里默认读者具有SpringMVC的相关知识

我们写的Controller都会被DispatcherServlet进行路由,而这种路径的路由@RequestMapping这种方式则是由RequestMappingHandlerMapping这个类去找到对应的controller方法,通过反射调用方法从而调用我们上面的test方法,那么就肯定要拿到TestController这个类,这个操作是由其父类AbstractHandlerMethodMapping的getHandlerInternal方法做的:


   
   
     
     
     
     
  1. protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  2. String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  3. this.mappingRegistry.acquireReadLock();
  4. try {
  5. // 根据请求拿到需要反射调用的方法
  6. HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
  7. // 关键:handlerMethod.createWithResolvedBean()获取调用方法的类
  8. return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
  9. }
  10. finally {
  11. this.mappingRegistry.releaseReadLock();
  12. }
  13. }

这里直接看handlerMethod的关键方法createWithResolvedBean:


   
   
     
     
     
     
  1. public HandlerMethod createWithResolvedBean() {
  2. Object handler = this.bean;
  3. if ( this.bean instanceof String) {
  4. Assert.state( this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
  5. String beanName = (String) this.bean;
  6. // 会使用testController这个beanName获取Bean
  7. handler = this.beanFactory.getBean(beanName);
  8. }
  9. return new HandlerMethod( this, handler);
  10. }

如果Controller被打上@RefreshScope注解,这里getBean的返回值就是CGLib动态代理对象,根据以上流程走自然可以达到动态刷新的效果

3.2.2 @Autowired的使用


   
   
     
     
     
     
  1. @Component
  2. public class Test {
  3. @Autowired
  4. private PropertiesSource propertiesSource;
  5. public boolean test(){
  6. propertiesSource.getValue();
  7. }
  8. }
  9. @Service
  10. @RefreshScope
  11. public class PropertiesSource {
  12. @Value("${test.refresh}")
  13. private boolean test;
  14. public boolean getValue(){
  15. return test;
  16. }
  17. }

这样使用也可以达到动态刷新的作用,Test类依赖注入PropertiesSource对象时会注入CGLib动态代理对象。

3.2.3 失效问题

从上面两个使用场景来看,RefreshBean被正确加载为CGLib动态代理对象就能正常动态刷新,那么什么时候会不正常加载呢?这里举一个我在3.2小节开头举的例子,WebFilter。

在WebFlux中,组装WebFilter是在WebHttpHandlerBuilder类的applicationContext方法中:


   
   
     
     
     
     
  1. public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
  2. WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
  3. context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);
  4. // 获取上下文中 Class为WebFilter的所有Bean
  5. List webFilters = context
  6. .getBeanProvider(WebFilter.class)
  7. .orderedStream()
  8. .collect(Collectors.toList());
  9. builder.filters(filters -> filters.addAll(webFilters));
  10. //...
  11. return builder;
  12. }

这里组装加载WebFilter的时候会获取IOC中WebFilter类型的Bean,这里我Debug给大家看看这个List列表
 

在这里插入图片描述

怎么会有两个重复的呢?

思路回到第三节讨论的扫描并注册Bean入口doScan中的createScopedProxy方法,其中针对特定Scope做一个BeanDefinition的修改:


   
   
     
     
     
     
  1. public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
  2. BeanDefinitionRegistry registry, boolean proxyTargetClass) {
  3. // 原始Bean的名称
  4. String originalBeanName = definition.getBeanName();
  5. BeanDefinition targetDefinition = definition.getBeanDefinition();
  6. // 加了前缀的Bean名称
  7. String targetBeanName = getTargetBeanName(originalBeanName);
  8. // ...
  9. // Register the target bean as separate bean in the factory.
  10. // 这里会注册原始Bean,以targetBeanName为beanName
  11. registry.registerBeanDefinition(targetBeanName, targetDefinition);
  12. // Return the scoped proxy definition as primary bean definition
  13. // (potentially an inner bean).
  14. // 注册会生成代理Bean的FactoryBean
  15. return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
  16. }

可以看到,其实这里依然会将原生Bean注册到IOC容器中,只不过beanName是被加了前缀的,这里我debug看看加了什么前缀(假设我的beanName是gatewayPropertiesSource)

在这里插入图片描述

可以看到,这里注册了两个Bean,一个是原始名称的FactoryBean,用于生成CGLib的动态代理Bean,一个是被加上 scopedTarget. 前缀的原生Bean,为什么要注册两个Bean呢?因为后面一个Bean是被Scope对象的Map缓存所管理的,后面那个Bean则是我们的Bean本尊,动态代理Bean调用方法要获取的Bean就是这个对象,从哪里可以看出来呢?回顾3.1节的最后,getObject关键方法:


   
   
     
     
     
     
  1. public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
  2. @Override
  3. public Object getTarget() throws Exception {
  4. // getTargetBeanName,这里是加了前缀的名称
  5. return getBeanFactory().getBean(getTargetBeanName());
  6. }
  7. }

在这里插入图片描述


到这里我们可以知道,其实这个加了前缀的Bean才是我们最终使用的Bean,配置动态刷新的时候变的不是CGLib动态代理的Bean,而是里面加了前缀的Bean,这个Bean被Scope对象的Map所管理,当配置刷新时Map清空,动态代理的Bean调用方法之前先去IOC中获取加了前缀的Bean,这个加了前缀的Bean又是refresh的scope,又会去Scope中的Map寻找,发现找不到,就又在IOC容器中创建一个新的加了前缀的Bean,供后续使用,达到动态刷新的效果。

回到我们3.2.3节开头讲的失效问题,这里组装WebFilter的时候由于会获取类型为WebFilter的所有Bean,所以会把动态代理的Bean和前缀Bean全组装起来,在我们Web应用里就会使用到前缀Bean本尊,由于这个Bean不像那个动态代理Bean一样,每一次方法调用都会去IOC容器getBean,所以这个Bean是一成不变的,就算配置刷新,这个Bean实例还是那个旧的值,导致失效问题。

3.3 更优雅的动态刷新

我们知道,如果是@RefreshScope的话,每次都要去IOC获取一下Bean,感觉调用链路比较长,那么是否能不打@RefreshScope也能动态刷新属性值呢?这里还有一种方式达到动态刷新的目的,那就是@ConfigurationProperties注解,具体用法不过多赘述,这里详细讲讲ConfigurationProperties是如何做到动态刷新的。

思路回到我们第二节讲的,ContextRefresher这个类,当配置刷新的时候,这个类的refresh方法会被调用:


   
   
     
     
     
     
  1. public synchronized Set refresh() {
  2. // 刷新环境
  3. Set keys = refreshEnvironment();
  4. this.scope.refreshAll();
  5. return keys;
  6. }

关键在refreshEnvironment方法:


   
   
     
     
     
     
  1. public synchronized Set refreshEnvironment() {
  2. Map before = extract(
  3. this.context.getEnvironment().getPropertySources());
  4. addConfigFilesToEnvironment();
  5. Set keys = changes(before,
  6. extract( this.context.getEnvironment().getPropertySources())).keySet();
  7. // 发布一个EnvironmentChangeEvent事件
  8. this.context.publishEvent( new EnvironmentChangeEvent( this.context, keys));
  9. return keys;
  10. }

熟悉的方法,这里我们的关注点在于其最后会发布一个EnvironmentChangeEvent事件,这个事件是由ConfigurationPropertiesRebinder这个类来监听的,看名字就知道,这个类负责重新设置ConfigurationProperties值。重点关注这个类的onApplicationEvent方法:


   
   
     
     
     
     
  1. public void onApplicationEvent(EnvironmentChangeEvent event) {
  2. if ( this.applicationContext.equals(event.getSource())
  3. // Backwards compatible
  4. || event.getKeys().equals(event.getSource())) {
  5. // 重新设置值
  6. rebind();
  7. }
  8. }

在这个方法消费EnvironmentChangeEvent这个事件,进入rebind方法:


   
   
     
     
     
     
  1. public void rebind() {
  2. this.errors.clear();
  3. // beans保存着所有ConfigurationProperties实例
  4. for (String name : this.beans.getBeanNames()) {
  5. rebind(name);
  6. }
  7. }

beans实则是一个Map,我们直接debug看看这个Map保存了什么

在这里插入图片描述

可以看到,这里保存了54个ConfigurationProperties类实例,其中我们的ConfigurationProperties类就是上面的greyProperties

在这里插入图片描述

继续往下走,看看rebind对这个实例做了什么:


   
   
     
     
     
     
  1. public boolean rebind(String name) {
  2. //...
  3. if ( this.applicationContext != null) {
  4. try {
  5. // 从上下文中获取这个Bean
  6. Object bean = this.applicationContext.getBean(name);
  7. if (AopUtils.isAopProxy(bean)) {
  8. bean = ProxyUtils.getTargetObject(bean);
  9. }
  10. if (bean != null) {
  11. // TODO: determine a more general approach to fix this.
  12. // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
  13. if (getNeverRefreshable().contains(bean.getClass().getName())) {
  14. return false; // ignore
  15. }
  16. // 调用这个Bean的destroy流程
  17. this.applicationContext.getAutowireCapableBeanFactory()
  18. .destroyBean(bean);
  19. // 调用这个Bean的初始化流程
  20. this.applicationContext.getAutowireCapableBeanFactory()
  21. .initializeBean(bean, name);
  22. return true;
  23. }
  24. }
  25. //...
  26. }
  27. return false;
  28. }

重点在于调用上下文的initializeBean方法初始化Bean:


   
   
     
     
     
     
  1. protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
  2. //...
  3. // 调用aware回调
  4. invokeAwareMethods(beanName, bean);
  5. Object wrappedBean = bean;
  6. if (mbd == null || !mbd.isSynthetic()) {
  7. // 重点,调用BeanPostProcessors回调
  8. wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  9. }
  10. //...
  11. return wrappedBean;
  12. }

其中的重点在于applyBeanPostProcessorsBeforeInitialization方法:


   
   
     
     
     
     
  1. public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
  2. throws BeansException {
  3. Object result = existingBean;
  4. // BeanPostProcessor的回调
  5. for (BeanPostProcessor processor : getBeanPostProcessors()) {
  6. Object current = processor.postProcessBeforeInitialization(result, beanName);
  7. if (current == null) {
  8. return result;
  9. }
  10. result = current;
  11. }
  12. return result;
  13. }

在BeanPostProcessor的回调中,会有一个ConfigurationPropertiesBindingPostProcessor这个BeanPostProcessor在postProcessBeforeInitialization方法中利用Binder对象,将最新的配置值反射调用set方法注入到ConfigurationProperties这样的Bean里,达到动态刷新的效果。这里限于篇幅问题,就不继续深入研究了,感兴趣的小伙伴可以跟着思路去看看源码。

这里我们看到,ConfigurationProperties这种方式不需要一直去IOC容器里getBean,也不需要动态代理,没有以上那样的失效问题,从始至终就只有一个Bean的单例,这种方式个人感觉比较优雅,建议最好使用这种方式去装配外部化配置信息。

但有些场景又需要@RefreshScope这种方式,因为@Value可以解析一些复杂的表达式,比如设置默认值,或者把配置值处理成一个List比较方便:

在这里插入图片描述

总之,@Value这种比较灵活,但如果配置值多了,比如外部化配置有几十个,ConfigurationProperties又比较好了,只需要写一个前缀就可以装配前缀下的所有外部化配置。两者各有好处,但建议最好优先考虑一下ConfigurationProperties,使用@RefreshScope也可以,知道原理也就能避免一些失效问题了。

4. 总结

我们总结一下,当不正常获取RefreshScope的Bean时,动态刷新会失效。例如直接从IOC容器中获取所有某类型的Bean,把beanName带前缀的那个Bean直接拿来使用了。到此,我们可以知道只有使用被动态代理增强过的那个Bean,才可以有动态刷新的效果。

3.2.3节这种WebFilter这种情况告诉我们,不要在这种方式加载使用Bean的地方用@RefreshScope,会导致Filter链重复,你会得到两个一摸一样的Filter并且还不知道,潜在影响性能。解决方法就是把要动态刷新的配置值抽出来变成一个类,在这个类上打上@RefreshScope注解,然后在WebFilter中使用@Autowired这种方式依赖注入刚刚那个配置值的专门类,很好的解决了失效问题。

 

你可能感兴趣的:(spring,java)