一、前置知识
在Spring中bean的作用域(scope)常用的有两种,单例(singleton)、原型(prototype),Bean的Scope影响了Bean的管理方式,例如创建Scope=singleton的Bean时,IOC会将这些Bean实例保存在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。而在SpringCloud中为其新添加了一种作用域为refresh,改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。
- 那这个scope是如何做到热加载的呢?先说结论:
因为可以单独管理Bean的创建和销毁 创建Bean的时候如果scope为refresh,这个Bean就缓存在一个专门管理这类scope的map中, 当外部配置更改过后,会触发一个刷新动作,这个动作将上面的map中的Bean清空,这样,当再次用到这个Bean的时候,这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果 下面我们深入源码,来验证我们上述的讲法。
二、@RefreshScope探究
可以看到@RefreshScope注解只又套了一个@Scope("refresh"),也就意味着被@RefreshScope注解类作用域会变为refresh,并且其proxyMode属性设置为了TARGET_CLASS,如果是TARGET_CLASS,ioc会为其创建一个代理对象。这里为什么设置成TARGET_CLASS后面再介绍
// 单例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 {
// 1、由上面的RefreshScope注解可以知道,这里scopeName=refresh
String scopeName = mbd.getScope();
// 2、获取RefreshScope对象
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 3、让Scope对象去管理Bean
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
而在SpringBoot启动时,把Bean扫描到IOC容器中,不同scope有不同的创建方式,在AbstractBeanFactory#doGetBean方法中,创建scope为refresh的Bean的逻辑就会走最下面的else逻辑。
这里可以得出几个结论:
- 单例和原型scope的Bean是硬编码单独处理的
- 除了单例和原型Bean,其他Scope是由Scope对象处理的
- 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象
通过debug,this.scopes有四类scope,另外3个不常用的scope也对应上了,我们可以看到,返回的是RefreshScope对象,那这个RefreshScope是什么时候加载进来的呢?其实是通过RefreshAutoConfiguration自动装配进来的,不是本文重点,提一下。(这里可以发现我们可以自定义scope,不过一般开发中用不上)
下面我们看下scope.get,前面我们知道这个scope为RefreshScope,所以我们去RefreshScope里面去找get方法,发现没有对其实现,而RefreshScope继承了GenericScope,GenericScope的get如下:
这里就是将Bean包装成一个BeanLifecycleWrapper对象,缓存在一个Map中,下次如果再getBean,还是那个旧的BeanLifecycleWrapper
可以看出来,BeanLifecycleWrapper中的bean变量即为实际Bean,第一次get肯定为空,就会调用BeanFactory的createBean方法创建Bean,创建出来之后就会一直保存下来。
三、刷新Environment对象
当配置中心更改配置之后,有两种方式可以动态刷新Bean的配置变量值
- 向上下文发布一个RefreshEvent事件
- Http访问/actuator/refresh(springboot2.0之前为/refresh,springboot2.0之后默认没有开启refresh端点,需配置)
不管是什么方式,最终都会调用ContextRefresher这个类的refresh方法,那么我们由此为入口来分析一下,热加载配置的原理:
我们一般是使用@Value、@ConfigurationProperties去获取配置变量值,其底层在IOC中则是通过上下文的Environment对象去获取property值,然后依赖注入利用反射Set到Bean对象中去的。
那么如果我们更新Environment里的Property值,然后重新创建一次RefreshBean,再进行一次上述的依赖注入,是不是就能完成配置热加载了呢?@Value的变量值就可以加载为最新的了。
下面说一下几个核心方法
- refreshEnvironment()方法对比新老配置,返回有变化的配置keys,其中有个重点方法addConfigFilesToEnvironment(),通过名字可判断将最新配置加入到环境变量中
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);
//设置一个监听器,监听环境改变
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
// 启动上下文
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 {
//....
}
}
}
}
//....
}
可以看到,这里归根结底就是SpringBoot启动上下文那种方法,新做了一个Spring上下文,因为Spring启动后会对上下文中的Environment进行初始化,获取最新配置,所以这里利用Spring的启动,达到了获取最新的Environment对象的目的。然后去替换旧的上下文中的Environment对象中的配置值即可。
- refreshAll()
这里调用了destroy()就将上文的this.cache(实际就是个map)清空了。
思路回到sopce.get这里,由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例,value.getBean()方法中也会重新去createBean。
最后为什么proxyMode属性设置为了TARGET_CLASS?
首先我们要知道ScopedProxyMode的作用:ScopedProxyMode是一个枚举类,该类共定义了四个枚举值,分别为NO、DEFAULT、INTERFACE、TARGET_CLASS,其中DEFAULT和NO的作用是一样的。INTERFACES代表要使用JDK的动态代理来创建代理对象,TARGET_CLASS代表要使用CGLIB来创建代理对象。比如下面这个场景:
@Component
@RefreshScope
public class Config {
}
@Component
public class UserService {
@Autowired
private Config config;
}
@RestController
public class TestController {
@Autowired
Config config;
}
我们知道对象都是 @Autowired
或者 @Resource
注入进去的,那就会出现一个问题,refresh bean
被销毁重建后,其它类依赖的这个bean
怎么更新?也就是UserService怎么去更新Config对象,答案是代理对象。
首先从代码上解释来说UserService持有的是Config的一个代理bean,而代理bean才持有真正Config的bean。而在refersh的时候是销毁是代理bean持有的bean,代理bean是不会被销毁的,然后再次通过代理bean创建新的Config bean即可。也就是说,这个Config的bean
在 ioc容器
里已经不是原始的类,而是一个代理对象。
下篇文章会通过一个demo演示refresh何时被调用