Spring框架中RefreshScope注解如何实现Bean的热替换

Posted by My Blog on May 16, 2024

0.问题

​ 近来被人问到nacos动态更新的原理,除了长轮询之外,关键就涉及到Spring对bean的管理,还有@RefreshScope的实现。刚好触发了我对Spring源码的再次学习,尤其是bean加载和生命周期管理部分,动态加载的问题恰好是一个很好的切入点来加深理解1

序号 问题
1. 如果一个bean(@RefreshScope注解的,后同)被热更新,那引用方如何更新引用?
2. 如何快速搭建测试环境?
3. @RefreshScope注解处理有何特殊之处?
4. bean是如何完成热替换的?
5. bean的首次初始化时机?配置更新后再次生成时机?
6. @RefreshScope与BeanFactory关系如何?
7. cglib与target关系

1.@RefreshScope热替换原理

​ 首先就会有个疑惑,热替换后,引用更新的问题?由此涉及到bean定义加载,bean初始化,bean容器管理,触发时机,cglib增强等一系列问题。先用一张图把整体过程表达出来,后面再逐个理解:

refreshscope bean

2.测试环境

​ 虽然问题是从nacos配置变更缘起的,但是为了测试的目的,关键是看spring内部的实现,所以可以触发变更即可。本文直接参考了spring官网示例源码Centralized Configuration2。直接使用complete目录即可,其下包含两个工程:

  • 一个是config server基于本地git,所以本地修改,git commit在server端即可看到变更
  • 另一个是config client,访问server拿配置,变更后需要手动刷新以通知变更

核心操作步骤:

server端:

  • 在本地初始化一个git folder

  • spring.cloud.config.server.git.uri=${HOME}/git folder

  • 配置访问:http://localhost:8888/a-bootiful-client/default

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    {
      "name": "a-bootiful-client",
      "profiles": [
        "default"
      ],
      "label": null,
      "version": "dac4409b661c612f382c89bfbe3245664c712bcc",
      "state": null,
      "propertySources": [
        {
          "name": "/git folder/a-bootiful-client.properties",
          "source": {
            "message": "Hello Spring Cloud 3!",
            "key1": "haha9"
          }
        }
      ]
    }
    

client端:

  • 配置访问:http://localhost:8080/message
  • 刷新:curl localhost:8080/actuator/refresh -d {} -H "Content-Type: application/json"

git 端:

  • 修改
  • git commit

环境版本:

spring version

client端基本测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
class MessageRestController {
    @Autowired
    private Config config;
    ...
}

@Data
@RefreshScope
//@Configuration
@Component
public class Config {
    @Value("${message:Hello default}")
    private String message;
  	...
}

3.@RefreshScope在bean定义加载时的特殊处理

​ 经@RefreshScope注解的bean实质上会生成一个代理,引用方调用的实际上是代理。那么必然应该在bean定义加载和实例化时有针对性的特殊处理,先看看bean定义加载。

​ spring框架基于注解的bean定义加载过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// spring boot
SpringApplication.run	
	refreshContext
		// AbstractApplicationContext
		refresh
		    // Invoke factory processors registered as beans in the context.
			invokeBeanFactoryPostProcessors(beanFactory)
				// PostProcessorRegistrationDelegate
				invokeBeanFactoryPostProcessors
				    // postProcessor
					postProcessBeanDefinitionRegistry(registry)
					    // ConfigurationClassPostProcessor
						processConfigBeanDefinitions(registry)
							// ConfigurationClassParser
							parse
								processConfigurationClass
									doProcessConfigurationClass
										// Process any @ComponentScan annotations
										// sourceClass 此时为加了@SpringBootApplication main方法启动类
										Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
												sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
										if (!componentScans.isEmpty() &&
												!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
											for (AnnotationAttributes componentScan : componentScans) {
												// The config class is annotated with @ComponentScan -> perform the scan immediately
												// 加载所有@Configuration,@Service, @Component, @Repository, @Controller注解的类
												Set<BeanDefinitionHolder> scannedBeanDefinitions =
														this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
												// Check the set of scanned definitions for any further config classes and parse recursively if needed
												for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
													if (ConfigurationClassUtils.checkConfigurationClassCandidate(
															holder.getBeanDefinition(), this.metadataReaderFactory)) {
															// 处理 @Configuration 中的bean定义
															parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
													}
												}
											}
										}								
		    
		    ...
		 
		    // bean的实例化
			finishBeanFactoryInitialization

基于上述过程,在org/springframework/context/annotation/ClassPathBeanDefinitionScanner.javaBeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName)处增加条件断点:"config".equals(beanName)

image

其中关键下面这行代码对Scope做了特殊处理:

1
2
definitionHolder =
       AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

org/springframework/context/annotation/AnnotationConfigUtils.java:

1
2
3
4
5
6
7
8
9
10
static BeanDefinitionHolder applyScopedProxyMode(
			ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {

		ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
		if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
			return definition;
		}
		boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
		return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
	}

org/springframework/aop/scope/ScopedProxyUtils.javacreateScopedProxy方法对bean定义进行了特殊操作,是关键实现逻辑。所以重点要看明白此处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
			BeanDefinitionRegistry registry, boolean proxyTargetClass) {

		String originalBeanName = definition.getBeanName();
		BeanDefinition targetDefinition = definition.getBeanDefinition();
		String targetBeanName = getTargetBeanName(originalBeanName);

		// Create a scoped proxy definition for the original bean name,
		// "hiding" the target bean in an internal target definition.
		RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
		proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
		proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
		proxyDefinition.setSource(definition.getSource());
		proxyDefinition.setRole(targetDefinition.getRole());

		proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
		if (proxyTargetClass) {
			targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
			// ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here.
		}
		else {
			proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
		}

		// Copy autowire settings from original bean definition.
		proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
		proxyDefinition.setPrimary(targetDefinition.isPrimary());
		if (targetDefinition instanceof AbstractBeanDefinition) {
			proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition);
		}

		// The target bean should be ignored in favor of the scoped proxy.
		targetDefinition.setAutowireCandidate(false);
		targetDefinition.setPrimary(false);

		// Register the target bean as separate bean in the factory.
		registry.registerBeanDefinition(targetBeanName, targetDefinition);

		// Return the scoped proxy definition as primary bean definition
		// (potentially an inner bean).
		return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
	}

关键逻辑:

  • 将原始bean:config重命名为scopedTarget.config
  • 新定义代理bean: config
  • 为代理bean: config增加targetBeanName属性
  • 将原始bean: scopedTarget.config调用 setAutowireCandidate(false),也就是原始bean不能作为autowire注解对象
  • scopedTarget.config加入registry,也就是bean定义集合。config后续很快也会加入

image

image

​ 经过上面处理,bean定义就包含了两个configscopedTarget.config,注解的时候就依赖代理bean定义config

4.bean的热替换

​ 了解bean定义加载过程以及bean实例化(此处略过)的逻辑,bean的热替换就不难理解了:

  • 被注解引用的实际上是代理bean,不会被替换
  • 删除所有RefreshScope.cache里的scopedTarget.bean对象,真正的bean实例被删除
  • 下次用到的时候再初始化一个新的scopedTarget.bean实例即可

假设对git folder里的配置做一个修改并commit,然后对client端进行刷新:

curl localhost:8080/actuator/refresh -d {} -H "Content-Type: application/json"

接着会进入org/springframework/cloud/endpoint/RefreshEndpoint.java

image

再看refresh()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@WriteOperation
	public Collection<String> refresh() {
		Set<String> keys = this.contextRefresher.refresh();
		return keys;
	}

// 逐步进入this.scope.refreshAll()
public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

@Override
	public void destroy() {
		List<Throwable> errors = new ArrayList<Throwable>();
    // 删除全量scopedTarget.bean
		Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
		for (BeanLifecycleWrapper wrapper : wrappers) {
			try {
				Lock lock = this.locks.get(wrapper.getName()).writeLock();
				lock.lock();
				try {
					wrapper.destroy();
				}
				finally {
					lock.unlock();
				}
			}
      ...
		}
    ...
	}

image

image

到这一步,带有旧配置的bean都被销毁。那么什么时候再次生成这些bean呢,从而更新配置?答案是在下次调用的时候。

为了跟踪再次生成bean的逻辑,想了一个办法,为Config定义构造函数并抛出异常,从而打印出调用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Data
@RefreshScope
//@Configuration
@Component
public class Config {
    @Value("${message:Hello default}")
    private String message;

    public Config() {
        System.out.println("hi new instance for Config!");
        try {
            throw new RuntimeException("hi");
        } catch (RuntimeException ex) {
            ex.printStackTrace();
        }
    }
}

@RestController
class MessageRestController {
    @Autowired
    private Config config;

    @RequestMapping("/message")
    String getMessage() {
        return config.getMessage();
    }
}

访问一下client端的配置:http://localhost:8080/message

image

可以看出确实是在servlet线程进入controller调用后,才初始化的。这里还有一个问题,就intellijidea可能无法断点3

用同样的逻辑,也能追踪出应用启动时,scopedTarget.bean加载的过程:

image

5.其它问题

  • RefreshScope.cache与BeanFactory的关系

RefreshScope对象作为元素存在于AbstractBeanFactory:: Map<String, Scope> scopes

scope

  • setAutowireCandidate(false)与bean的依赖注入

    设置为false,则取消了scopedTarget.config作为依赖被注入的资格

  • 代理bean如何进行代理增强?ScopedProxyFactoryBean

    这一部分需要重温cglib增强原理4 5,以及梳理spring内部的封装关系,需要单独一篇文章。其关键类是GenericScope::LockedScopedProxyFactoryBean

6.代理类如何与目标类关联

  • 代理类的bean定义由于是FactoryBean,在该bean初始化时,先通过“&config”(代表生成configFactoryBean)初始化Factory Bean,此处类型为:GenericScope::LockedScopedProxyFactoryBean

  • 而后通过LockedScopedProxyFactoryBean::getObject()得到代理类:Config\(EnhancerBySpringCGLIB\)8cc3b93e

  • 同时GenericScope::LockedScopedProxyFactoryBean自身会作为Advice来增强代理类Config\(EnhancerBySpringCGLIB\)8cc3b93e

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    public static class LockedScopedProxyFactoryBean<S extends GenericScope> extends ScopedProxyFactoryBean
    			implements MethodInterceptor {
    		@Override
    		public void setBeanFactory(BeanFactory beanFactory) {
    			super.setBeanFactory(beanFactory);
    			Object proxy = getObject();
    			if (proxy instanceof Advised) {
    				Advised advised = (Advised) proxy;
    				advised.addAdvice(0, this);
    			}
    		}
    }
    
  • 那么我们在Controller里调用config.getMessage时会被拦截,进入GenericScope::LockedScopedProxyFactoryBean::invoke方法

    | image | image | | ———————————————————— | ———————————————————— |

    因此,代理类是在方法调用过程中,通过拦截方法动态实时查找的目标对象。因而也就可以灵活替换目标对象了

7. 参考