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增强等一系列问题。先用一张图把整体过程表达出来,后面再逐个理解:
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
环境版本:
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.java中BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName)
处增加条件断点:"config".equals(beanName)
其中关键下面这行代码对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.java,createScopedProxy方法对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后续很快也会加入
经过上面处理,bean定义就包含了两个config和scopedTarget.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
再看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();
}
}
...
}
...
}
到这一步,带有旧配置的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
可以看出确实是在servlet线程进入controller调用后,才初始化的。这里还有一个问题,就intellijidea可能无法断点3
用同样的逻辑,也能追踪出应用启动时,scopedTarget.bean加载的过程:
5.其它问题
- RefreshScope.cache与BeanFactory的关系
RefreshScope对象作为元素存在于AbstractBeanFactory:: Map<String, Scope> scopes
-
setAutowireCandidate(false)与bean的依赖注入
设置为false,则取消了scopedTarget.config作为依赖被注入的资格
-
代理bean如何进行代理增强?ScopedProxyFactoryBean
这一部分需要重温cglib增强原理4 5,以及梳理spring内部的封装关系,需要单独一篇文章。其关键类是GenericScope::LockedScopedProxyFactoryBean
6.代理类如何与目标类关联
-
代理类的bean定义由于是FactoryBean,在该bean初始化时,先通过“&config”(代表生成config的FactoryBean)初始化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方法
| | | | ———————————————————— | ———————————————————— |
因此,代理类是在方法调用过程中,通过拦截方法动态实时查找的目标对象。因而也就可以灵活替换目标对象了