前言
在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註解,那麼就看看這個註解做了什麼:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
可以發現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有不同的創建方式:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
//....
// Create bean instance.
// 單例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 {
// 由上面的RefreshScope註解可以知道,這裏scopeName=refresh
String scopeName = mbd.getScope();
// 獲取Refresh的Scope對象
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 讓Scope對象去管理Bean
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
//...
}
}
//...
}
//...
}
這裏可以看到幾件事情:
- 單例和原型scope的Bean是硬編碼單獨處理的
- 除了單例和原型Bean,其他Scope是由Scope對象處理的
- 具體創建Bean的過程都是由IOC做的,只不過Bean的獲取是通過Scope對象
這裏scope.get獲取的Scope對象爲RefreshScope,可以看到,創建Bean還是由IOC來做(createBean方法),但是獲取Bean,都由RefreshScope對象的get方法去獲取,其get方法在父類GenericScope中實現:
public Object get(String name, ObjectFactory<?> objectFactory) {
// 將Bean緩存下來
BeanLifecycleWrapper value = this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory));
this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
try {
// 創建Bean,只會創建一次,後面直接返回創建好的Bean
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
首先這裏將Bean包裝起來緩存下來
private final ScopeCache cache;
// 這裏進入上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
return (BeanLifecycleWrapper) this.cache.put(name, value);
}
這裏的ScopeCache對象其實就是一個HashMap:
public class StandardScopeCache implements ScopeCache {
private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
//...
public Object get(String name) {
return this.cache.get(name);
}
// 如果不存在,纔會put進去
public Object put(String name, Object value) {
// result若不等於null,表示緩存存在了,不會進行put操作
Object result = this.cache.putIfAbsent(name, value);
if (result != null) {
// 直接返回舊對象
return result;
}
// put成功,返回新對象
return value;
}
}
這裏就是將Bean包裝成一個對象,緩存在一個Map中,下次如果再GetBean,還是那個舊的BeanWrapper。回到Scope的get方法,接下來就是調用BeanWrapper的getBean方法:
// 實際Bean對象,緩存下來了
private Object bean;
public Object getBean() {
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
return this.bean;
}
可以看出來,BeanWrapper中的bean變量即爲實際Bean,如果第一次get肯定爲空,就會調用BeanFactory的createBean方法創建Bean,創建出來之後就會一直保存下來。
由此可見,RefreshScope管理了Scope=Refresh的Bean的生命週期。
2. 重新創建RefreshBean
當配置中心刷新配置之後,有兩種方式可以動態刷新Bean的配置變量值,(SpringCloud-Bus還是Nacos差不多都是這麼實現的):
- 向上下文發佈一個RefreshEvent事件
- Http訪問
/refresh
這個EndPoint
不管是什麼方式,最終都會調用ContextRefresher
這個類的refresh方法,那麼我們由此爲入口來分析一下,熱加載配置的原理:
// 這就是我們上面一直分析的Scope對象(實際上可以看作一個保存refreshBean的Map)
private RefreshScope scope;
public synchronized Set<String> refresh() {
// 更新上下文中Environment外部化配置值
Set<String> keys = refreshEnvironment();
// 調用scope對象的refreshAll方法
this.scope.refreshAll();
return keys;
}
我們一般是使用@Value、@ConfigurationProperties去獲取配置變量值,其底層在IOC中則是通過上下文的Environment對象去獲取property值,然後依賴注入利用反射Set到Bean對象中去的。
那麼如果我們更新Environment裏的Property值,然後重新創建一次RefreshBean,再進行一次上述的依賴注入,是不是就能完成配置熱加載了呢?@Value的變量值就可以加載爲最新的了。
這裏說的刷新Environment對象並重新依賴注入則爲上述兩個方法做的事情:
- Set keys = refreshEnvironment();
- this.scope.refreshAll();
2.1 刷新Environment對象
下面簡單介紹一下如何刷新Environment裏的Property值
public synchronized Set<String> refreshEnvironment() {
// 獲取刷新配置前的配置信息,對比用
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
// 刷新Environment
addConfigFilesToEnvironment();
// 這裏上下文的Environment已經是新的值了
// 進行新舊對比,結果返回有變化的值
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
我們的重點在addConfigFilesToEnvironment方法,刷新Environment:
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);
// 啓動上下文
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對象中的配置值即可。
2.2 重新創建RefreshBean
經過上述刷新Environment對象的動作,此時上下文中的配置值已經是最新的了。思路回到ContextRefresher
的refresh方法,接下來會調用Scope對象的refreshAll方法:
public void refreshAll() {
// 銷燬Bean
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
// 緩存清空
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
// ...
}
還記得上面的管理RefreshBean生命週期那一節關於緩存的討論嗎,cache變量是一個Map保存着RefreshBean實例,這裏直接就將Map清空了。
思路回到BeanFactory的doGetBean的流程中,從IOC容器中獲取RefreshBean是交給RefreshScope的get方法做的:
public Object get(String name, ObjectFactory<?> objectFactory) {
// 由於剛剛清空了緩存Map,這裏就會put一個新的BeanLifecycleWrapper實例
BeanLifecycleWrapper value = this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory));
this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
try {
// 在這裏是新的BeanLifecycleWrapper實例調用getBean方法
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
public Object getBean() {
// 由於是新的BeanLifecycleWrapper實例,這裏一定爲null
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
// 調用IOC容器的createBean,再創建一個Bean出來
this.bean = this.objectFactory.getObject();
}
}
}
return this.bean;
}
可以看到,此時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有什麼特殊處理:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
// 掃描basePackages所在的包下的所有的類,帶@Componet的類都會被註冊爲BeanDefinition
for (String basePackage : basePackages) {
//...
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
// 這裏就是Scope的Bean的統一處理,是一個改變BeanDefinition的回調機會
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
// 註冊BeanDefinition到IOC容器中
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
其中關鍵在於AnnotationConfigUtils的applyScopedProxyMode方法:
static BeanDefinitionHolder applyScopedProxyMode(
ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
// 獲取Scope的代理模式
ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
// 如果代理模式爲NO,就不進行代理了
if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
return definition;
}
boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
// 進行代理
return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}
回顧開頭分析的@RefreshScope註解,其proxyMode值爲ScopedProxyMode.TARGET_CLASS
public @interface RefreshScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
所以這裏被打上@RefreshScope的Bean類會進入接下來ScopedProxyCreator的createScopedProxy方法:
public static BeanDefinitionHolder createScopedProxy(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) {
return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass);
}
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
// ...
// Create a scoped proxy definition for the original bean name,
// "hiding" the target bean in an internal target definition.
// 重點,這裏構造函數中將beanClass設置爲了ScopedProxyFactoryBean.class
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
// targetDefinition是被代理的原生Bean
proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
// ...
// Return the scoped proxy definition as primary bean definition
// (potentially an inner bean).
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
這裏一堆構建BeanDefinition的邏輯,先不看,只關注一件事,這裏將BeanDefinition的beanClass設置爲了ScopedProxyFactoryBean.class,而Scope的通用處理類GenericScope類是一個BeanDefinitionRegistryPostProcessor,其在postProcessBeanDefinitionRegistry回調方法中會針對剛剛那個beanClass爲ScopedProxyFactoryBean.class的BeanDefinition做一個特殊的處理:
// GenericScope.class
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
// 獲取所有BeanDefinition的名稱
for (String name : registry.getBeanDefinitionNames()) {
BeanDefinition definition = registry.getBeanDefinition(name);
// 針對RootBeanDefinition這個BeanDefinition來做,這和上面的邏輯吻合
if (definition instanceof RootBeanDefinition) {
RootBeanDefinition root = (RootBeanDefinition) definition;
// 判斷BeanClass == ScopedProxyFactoryBean.class
if (root.getDecoratedDefinition() != null && root.hasBeanClass()
&& root.getBeanClass() == ScopedProxyFactoryBean.class) {
if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
.getScope())) {
// 將BeanClass換爲LockedScopedProxyFactoryBean
root.setBeanClass(LockedScopedProxyFactoryBean.class);
root.getConstructorArgumentValues().addGenericArgumentValue(this);
// surprising that a scoped proxy bean definition is not already
// marked as synthetic?
root.setSynthetic(true);
}
}
}
}
}
到這裏可以知道,GenericScope將ScopeBean,變爲LockedScopedProxyFactoryBean這個類
然而這個類又是一個FactoryBean,由其父類ScopedProxyFactoryBean的getObject方法實現FactoryBean接口,我們知道,創建一個FactoryBean,其實最終會調用其getObject方法,這個方法的返回值纔是最終被創建出來的Bean實例,所以我們的重點就在getObject方法中:
public Object getObject() {
if (this.proxy == null) {
throw new FactoryBeanNotInitializedException();
}
return this.proxy;
}
似乎早就被動態代理好了,全局搜索proxy變量在哪裏被賦值。可以發現,ScopedProxyFactoryBean還是一個BeanFactoryAware,其setBeanFactory會在比較早的時機被回調:
public void setBeanFactory(BeanFactory beanFactory) {
// ...
// 這裏是一個比較關鍵的點,scopedTargetSource變量是一個SimpleBeanTargetSource
// scopedTargetSource中保存了IOC容器
this.scopedTargetSource.setBeanFactory(beanFactory);
// 創建動態代理前,將動態代理的信息都保存到ProxyFactory中
ProxyFactory pf = new ProxyFactory();
pf.copyFrom(this);
// 注意,這裏的TargetSource就是剛剛說的scopedTargetSource
pf.setTargetSource(this.scopedTargetSource);
// ...
this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}
這裏先記住,scopedTargetSource是SimpleBeanTargetSource這個類就行,其保存了IOC容器。
接着調用pf的getProxy方法開始進行動態代理
下面就是AOP的一些邏輯了,不是本篇文章討論的重點,一筆帶過
private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
// ...
// Choose an "aop" interceptor (used for AOP calls).
// 這裏的MethodInterceptor實例是DynamicAdvisedInterceptor這個類
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
// ...
return callbacks;
}
我們知道,CGLib動態代理都會實現一個MethodInterceptor,被代理的類的每一個方法調用實質上都是在調用MethodInterceptor的intercept方法,那麼我們看看DynamicAdvisedInterceptor這個類的intercept方法:
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 記得嗎,剛剛我們反覆強調的TargetSource
TargetSource targetSource = this.advised.getTargetSource();
try {
// ...
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
// 重點,這裏調用了targetSource的getTarget
// target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
target = targetSource.getTarget();
// ...
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// ...
// target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// We need to create a method invocation...
// target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// target變量就是被代理的類,調用實際方法的時候反射調用其對應方法
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
// ...
}
直接看重點,targetSource的getTarget()方法的返回值是被代理的類,那麼這個getTarget做了什麼邏輯呢?回顧上面來看我們知道這裏TargetSource的實現類是SimpleBeanTargetSource:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {
// 從IOC中getBean
return getBeanFactory().getBean(getTargetBeanName());
}
}
到這裏就已經揭曉了答案,原來被打上@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的使用
@RestController
@RefreshScope
public class TestController {
@Value("${test.refresh}")
private boolean test;
@GetMapping("/get/refresh/test/value")
public boolean test() {
return test;
}
}
涉及一些SpringMVC的知識,這裏默認讀者具有SpringMVC的相關知識
我們寫的Controller都會被DispatcherServlet進行路由,而這種路徑的路由@RequestMapping這種方式則是由RequestMappingHandlerMapping這個類去找到對應的controller方法,通過反射調用方法從而調用我們上面的test方法,那麼就肯定要拿到TestController這個類,這個操作是由其父類AbstractHandlerMethodMapping的getHandlerInternal方法做的:
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
// 根據請求拿到需要反射調用的方法
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
// 關鍵:handlerMethod.createWithResolvedBean()獲取調用方法的類
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
這裏直接看handlerMethod的關鍵方法createWithResolvedBean:
public HandlerMethod createWithResolvedBean() {
Object handler = this.bean;
if (this.bean instanceof String) {
Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
String beanName = (String) this.bean;
// 會使用testController這個beanName獲取Bean
handler = this.beanFactory.getBean(beanName);
}
return new HandlerMethod(this, handler);
}
如果Controller被打上@RefreshScope註解,這裏getBean的返回值就是CGLib動態代理對象,根據以上流程走自然可以達到動態刷新的效果
3.2.2 @Autowired的使用
@Component
public class Test {
@Autowired
private PropertiesSource propertiesSource;
public boolean test(){
propertiesSource.getValue();
}
}
@Service
@RefreshScope
public class PropertiesSource {
@Value("${test.refresh}")
private boolean test;
public boolean getValue(){
return test;
}
}
這樣使用也可以達到動態刷新的作用,Test類依賴注入PropertiesSource對象時會注入CGLib動態代理對象。
3.2.3 失效問題
從上面兩個使用場景來看,RefreshBean被正確加載爲CGLib動態代理對象就能正常動態刷新,那麼什麼時候會不正常加載呢?這裏舉一個我在3.2小節開頭舉的例子,WebFilter。
在WebFlux中,組裝WebFilter是在WebHttpHandlerBuilder類的applicationContext方法中:
public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);
// 獲取上下文中 Class爲WebFilter的所有Bean
List<WebFilter> webFilters = context
.getBeanProvider(WebFilter.class)
.orderedStream()
.collect(Collectors.toList());
builder.filters(filters -> filters.addAll(webFilters));
//...
return builder;
}
這裏組裝加載WebFilter的時候會獲取IOC中WebFilter類型的Bean,這裏我Debug給大家看看這個List列表
怎麼會有兩個重複的呢?
思路回到第三節討論的掃描並註冊Bean入口doScan中的createScopedProxy方法,其中針對特定Scope做一個BeanDefinition的修改:
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
// 原始Bean的名稱
String originalBeanName = definition.getBeanName();
BeanDefinition targetDefinition = definition.getBeanDefinition();
// 加了前綴的Bean名稱
String targetBeanName = getTargetBeanName(originalBeanName);
// ...
// Register the target bean as separate bean in the factory.
// 這裏會註冊原始Bean,以targetBeanName爲beanName
registry.registerBeanDefinition(targetBeanName, targetDefinition);
// Return the scoped proxy definition as primary bean definition
// (potentially an inner bean).
// 註冊會生成代理Bean的FactoryBean
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
可以看到,其實這裏依然會將原生Bean註冊到IOC容器中,只不過beanName是被加了前綴的,這裏我debug看看加了什麼前綴(假設我的beanName是gatewayPropertiesSource)
可以看到,這裏註冊了兩個Bean,一個是原始名稱的FactoryBean,用於生成CGLib的動態代理Bean,一個是被加上 scopedTarget. 前綴的原生Bean,爲什麼要註冊兩個Bean呢?因爲後面一個Bean是被Scope對象的Map緩存所管理的,後面那個Bean則是我們的Bean本尊,動態代理Bean調用方法要獲取的Bean就是這個對象,從哪裏可以看出來呢?回顧3.1節的最後,getObject關鍵方法:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {
// getTargetBeanName,這裏是加了前綴的名稱
return getBeanFactory().getBean(getTargetBeanName());
}
}
到這裏我們可以知道,其實這個加了前綴的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方法會被調用:
public synchronized Set<String> refresh() {
// 刷新環境
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
關鍵在refreshEnvironment方法:
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
// 發佈一個EnvironmentChangeEvent事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
熟悉的方法,這裏我們的關注點在於其最後會發佈一個EnvironmentChangeEvent事件,這個事件是由ConfigurationPropertiesRebinder這個類來監聽的,看名字就知道,這個類負責重新設置ConfigurationProperties值。重點關注這個類的onApplicationEvent方法:
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
// 重新設置值
rebind();
}
}
在這個方法消費EnvironmentChangeEvent這個事件,進入rebind方法:
public void rebind() {
this.errors.clear();
// beans保存着所有ConfigurationProperties實例
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
beans實則是一個Map,我們直接debug看看這個Map保存了什麼
可以看到,這裏保存了54個ConfigurationProperties類實例,其中我們的ConfigurationProperties類就是上面的greyProperties
繼續往下走,看看rebind對這個實例做了什麼:
public boolean rebind(String name) {
//...
if (this.applicationContext != null) {
try {
// 從上下文中獲取這個Bean
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
// 調用這個Bean的destroy流程
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
// 調用這個Bean的初始化流程
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
//...
}
return false;
}
重點在於調用上下文的initializeBean方法初始化Bean:
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
//...
// 調用aware回調
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 重點,調用BeanPostProcessors回調
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
//...
return wrappedBean;
}
其中的重點在於applyBeanPostProcessorsBeforeInitialization方法:
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
// BeanPostProcessor的回調
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
在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這種方式依賴注入剛剛那個配置值的專門類,很好的解決了失效問題。