SpringCloud配置动态刷新@RefreshScope注解
⼀、@RefreshScope动态刷新原理
在SpringIOC中,BeanScope(Bean的作⽤域)影响了Bean的管理⽅式。
Bean的作⽤域:
作⽤域描述
singleton(单例)每⼀个Spring IoC容器都拥有唯⼀的⼀个实例对象(默认作⽤域)
prototype(原型)⼀个Bean定义,任意多个对象
request(请求)每⼀个HTTP请求都有⾃⼰的Bean实例(只在基于web的Spring ApplicationContext中可⽤)session(会话)⼀个Bean的作⽤域为HTTPsession的⽣命周期(只有基于web的Spring ApplicationContext才能使⽤)
global session(全局会
话)⼀个Bean的作⽤域为全局HTTPSession的⽣命周期。通常⽤于门户⽹站场景(只有基于web的Spri
ng ApplicationContext
才能使⽤)
例如创建Scope=singleton的Bean时,IOC会保存实例在⼀个Map中,保证这个Bean在⼀个IOC上下⽂有且仅有⼀个实例。
SpringCloud新增了⼀个⾃定义的作⽤域:refresh(可以理解为“动态刷新”),同样⽤了⼀种独特的⽅式改变了Bean的管理⽅式,使得其可以通过外部化配置(.properties)的刷新,在应⽤不需要重启的情况下热加载新的外部化配置的值。
这个scope是如何做到热加载的呢?RefreshScope主要做了以下动作:
1. 单独管理Bean⽣命周期
创建Bean的时候如果是RefreshScope就缓存在⼀个专门管理的ScopeMap中,这样就可以管理Scope是Refresh的Bean的⽣命周期了(所以含RefreshScope的其实⼀共创建了两个bean)。spring ioc注解
2. 重新创建Bean
外部化配置刷新之后,会触发⼀个动作,这个动作将上⾯的ScopeMap中的Bean清空,这样这些Bean就会重新被IOC容器创建⼀次,使⽤最新的外部化配置的值注⼊类中,达到热加载新值的效果。
spring cloud config或sprring cloud alibaba nacos作为配置中⼼,其实现原理就是通过@RefreshScope 来实现对象属性的的动态更新。
@RefreshScope 实现配置的动态刷新需要满⾜⼀下⼏点条件:
1. @Scope注解
2. @RefreshScope注解
3. RefreshScope类
4. GenericScope类
5. Scope接⼝
6. ContextRefresher类
@RefreshScope 能实现动态刷新全仰仗着@Scope 这个注解。
1. @Scope注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interface Scope {
/**
* Alias for {@link #scopeName}.
* @see #scopeName
*/
@AliasFor("scopeName")
String value()default"";
/
**
* singleton 表⽰该bean是单例的。(默认)
* prototype 表⽰该bean是多例的,即每次使⽤该bean时都会新建⼀个对象。
* request 在⼀次http请求中,⼀个bean对应⼀个实例。
* session 在⼀个httpSession中,⼀个bean对应⼀个实例
*/
@AliasFor("value")
String scopeName()default"";
/**
* DEFAULT 不使⽤代理。(默认)
* NO 不使⽤代理,等价于DEFAULT。
* INTERFACES 使⽤基于接⼝的代理(jdk dynamic proxy)。
* TARGET_CLASS 使⽤基于类的代理(cglib)。
*/
ScopedProxyMode proxyMode()default ScopedProxyMode.DEFAULT;
}
@Scope有两个主要属性value 和 proxyMode,其中proxyMode就是@RefreshScope 实现的本质了。
proxyMode属性是⼀个ScopedProxyMode类型的枚举对象。
public enum ScopedProxyMode {
DEFAULT,
NO,
INTERFACES,// JDK 动态代理
TARGET_CLASS;// CGLIB 动态代理
private ScopedProxyMode(){
}
}
当proxyMode属性的值为ScopedProxyMode.TARGET_CLASS时,会给当前创建的bean ⽣成⼀个代理对象,会通过代理对象来访问,每次访问都会创建⼀个新的对象。
2. @RefreshScope注解
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public@interface RefreshScope {
/**
* @see Scope#proxyMode()
*/
ScopedProxyMode proxyMode()default ScopedProxyMode.TARGET_CLASS;
}
它使⽤就是 @Scope ,⼀个scopeName="refresh"的@Scope。
proxyMode值为ScopedProxyMode.TARGET_CLASS,通过CGLIB动态代理的⽅式⽣成Bean。
使⽤ @RefreshScope 注解的 bean,不仅会⽣成⼀个beanName的bean,默认情况下同时会⽣成 scopedTarget.beanName的 bean。
@RefreshScope不能单独使⽤,需要和其他其他bean注解结合使⽤,如:@Controller、@Service、@Component、@Repository 等。
3. Scope接⼝
public interface Scope {
/**
* Return the object with the given name from the underlying scope,
* {@link org.springframework.beans.factory.ObjectFactory#getObject() creating it}
* if not found in the underlying storage mechanism.
* <p>This is the central operation of a Scope, and the only operation
* that is absolutely required.
* @param name the name of the object to retrieve
* @param objectFactory the {@link ObjectFactory} to use to create the scoped
* object if it is not present in the underlying storage mechanism
* @return the desired object (never {@code null})
* @throws IllegalStateException if the underlying scope is not currently active
*/
Object get(String name, ObjectFactory<?> objectFactory);
@Nullable
Object remove(String name);
void registerDestructionCallback(String name, Runnable callback);
@Nullable
Object resolveContextualObject(String key);
@Nullable
String getConversationId();
}
Object get(String name, ObjectFactory<?> objectFactory)
这个⽅法帮助我们来创建⼀个新的bean ,也就是说,@RefreshScope 在调⽤刷新的时候会使⽤此⽅法来给我们创建新的对象,这样就可以通过spring 的装配机制将属性重新注⼊了,也就实现了所谓的动态刷新。
RefreshScope extends GenericScope, GenericScope implements Scope
GenericScope 实现了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) ⽅法,在GenericScope ⾥⾯ 包装了⼀个内部类 BeanLifecycleWrapperCache 来对加了 @RefreshScope 从⽽创建的对象进⾏缓存,使其在不刷新时获取的都是同⼀个对象。(这⾥你可以把 BeanLifecycleWrapperCache 想象成为⼀个⼤Map 缓存了所有@RefreshScope 标注的对象)
知道了对象是缓存的,所以在进⾏动态刷新的时候,只需要清除缓存,重新创建就好了。
// ContextRefresher 外⾯使⽤它来进⾏⽅法调⽤ ============================== 我是分割线
public synchronized Set<String>refresh(){
Set<String> keys =refreshEnvironment();
freshAll();
return keys;
}
}
// RefreshScope 内部代码 ============================== 我是分割线
@ManagedOperation(description ="Dispose of the current instance of all beans in this scope and force a refresh on next method execution.") public void refreshAll(){
super.destroy();
}
// GenericScope ⾥的⽅法 ============================== 我是分割线
//进⾏对象获取,如果没有就创建并放⼊缓存
@Override
public Object get(String name, ObjectFactory<?> objectFactory){
BeanLifecycleWrapper value =this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory));
locks.putIfAbsent(name,new ReentrantReadWriteLock());
try{
Bean();
}
catch(RuntimeException e){
throw e;
}
}
// 初始化Bean
public Object getBean(){
if(this.bean ==null){
String var1 =this.name;
synchronized(this.name){
if(this.bean ==null){
this.bean =Object();
}
}
}
return this.bean;
}
//进⾏缓存的数据清理
@Override
public void destroy(){
List<Throwable> errors =new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers =this.cache.clear();
for(BeanLifecycleWrapper wrapper : wrappers){
try{
Lock lock = (Name()).writeLock();
lock.lock();
try{
wrapper.destroy();
}
finally{
lock.unlock();
}
}
catch(RuntimeException e){
errors.add(e);
}
}
if(!errors.isEmpty()){
throw (0));
}
}
通过观看源代码我们得知,我们截取了三个⽚段所得之,ContextRefresher 就是外层调⽤⽅法⽤的。
GenericScope类中有⼀个成员变量BeanLifecycleWrapperCache,⽤于缓存所有已经⽣成的Bean,在调⽤get⽅法时尝试从缓存加载,如果没有的话就⽣成⼀个新对象放⼊缓存,并通过初始化getBean其对应的Bean。
destroy ⽅法负责再刷新时缓存的清理⼯作。清空缓存后,下次访问对象时就会重新创建新的对象并放⼊缓存了。
所以在重新创建新的对象时,也就获取了最新的配置, 也就达到了配置刷新的⽬的。
4. @RefreshScope 实现流程
1. 需要动态刷新的类标注@RefreshScope 注解。
2. @RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是再创建⼀
个代理,在每次调⽤的时候都⽤它来调⽤GenericScope get ⽅法来获取对象。
3. 如属性发⽣变更
1. 调⽤ ContextRefresher refresh() -->> RefreshScope refreshAll() 进⾏缓存清理⽅法调⽤;
2. 发送刷新事件通知,GenericScope 真正的清理⽅法destroy() 实现清理缓存。
4. 在下⼀次使⽤对象的时候,会调⽤GenericScope get(String name, ObjectFactory<?> objectFactory) ⽅法创建⼀个新的对象,并
存⼊缓存中,此时新对象因为Spring 的装配机制就是新的属性了。
5. @RefreshScope原理总结
1. SpringCloud程序的存在⼀个⾃动装配的类,这个类默认情况下会⾃动初始化⼀个RefreshScope实例,该实例是GenericScope的
⼦类,然后注册到容器中。(RefreshAutoConfiguration.java, )
2. 当容器启动的时候,GenericScope会⾃⼰把⾃⼰注册到scope中(ConfigurableBeanFactory#registerScope)
(GenericScope)
3. 然后当⾃定义的Bean(被@RefreshScope修饰)注册的时候,会被容器读取到其作⽤域为refresh。
(AnnotatedBeanDefinitionReader#doRegisterBean)
通过上⾯三步,⼀个带有@RefreshScope的⾃定义Bean就被注册到容器中来,其作⽤域为refresh。
4. 当我们后续进⾏以来查的时候,会绕过Singleton和Prototype分⽀,进⼊最后⼀个分⽀,通过调⽤Scope接⼝的get()获取到该
refresh作⽤域的实例。(AbstractBeanFactory.doGetBean)
⼆、@RefreshScope注意事项
1. @RefreshScope使⽤注意事项
1. @RefreshScope作⽤的类,不能是final类,否则启动时会报错。
2. @RefreshScope不能单独使⽤,需要和其他其他bean注解结合使⽤,如:@Controller、@Service、@Component、
@Repository、@Configuration等。
3. @RefreshScope 最好不要修饰在 @Scheduled、listener、Timmer等类中,因为配置的刷新会导致
原来的对象被清除,需要重新使
⽤对象才能出发⽣成新对象(但因为对象没了,⼜没法重新使⽤对象,死循环)
2. @RefreshScope动态刷新失效
1. 考虑使⽤的bean是否是@RefreshScope⽣成的那个scopedTarget.beanName的 bean
2. springboot某些低版本貌似有问题,在Controller类上使⽤不会⽣效(⽹上有这么说的,没具体研究)
解决⽅法1:注解上加属性@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
解决⽅法2:直接使⽤其他类单独封装配置参数,使⽤@RefreshScope+@Value⽅式
解决⽅法3:直接使⽤@ConfigurationProperties
3. 不使⽤@RefreshScope也能实现动态刷新
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论