⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/JokerLJG/article/details/120254643 「骑个小蜗牛」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一、@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的Spring ApplicationContext才能使用)

例如创建Scope=singleton的Bean时,IOC会保存实例在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。

SpringCloud新增了一个自定义的作用域:refresh(可以理解为“动态刷新”),同样用了一种独特的方式改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。

这个scope是如何做到热加载的呢?RefreshScope主要做了以下动作:

单独管理Bean生命周期

创建Bean的时候如果是RefreshScope就缓存在一个专门管理的ScopeMap中,这样就可以管理Scope是Refresh的Bean的生命周期了(所以含RefreshScope的其实一共创建了两个bean)。

重新创建Bean

外部化配置刷新之后,会触发一个动作,这个动作将上面的ScopeMap中的Bean清空,这样这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果。

spring cloud configsprring cloud alibaba nacos作为配置中心,其实现原理就是通过@RefreshScope 来实现对象属性的的动态更新。

@RefreshScope 实现配置的动态刷新需要满足一下几点条件:

  • @Scope注解
  • @RefreshScope注解
  • RefreshScope类
  • GenericScope类
  • Scope接口
  • 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();
this.scope.refreshAll();
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();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}


// 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 {
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
// 初始化Bean
public Object getBean() {
if (this.bean == null) {
String var1 = this.name;
synchronized(this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
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 = locks.get(wrapper.getName()).writeLock();
lock.lock();
try {
wrapper.destroy();
}
finally {
lock.unlock();
}
}
catch (RuntimeException e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw wrapIfNecessary(errors.get(0));
}
this.errors.clear();
}

通过观看源代码我们得知,我们截取了三个片段所得之,ContextRefresher 就是外层调用方法用的。

GenericScope类中有一个成员变量BeanLifecycleWrapperCache,用于缓存所有已经生成的Bean,在调用get方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过初始化getBean其对应的Bean。

destroy 方法负责再刷新时缓存的清理工作。清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。

所以在重新创建新的对象时,也就获取了最新的配置,也就达到了配置刷新的目的。

4. @RefreshScope 实现流程

  • 需要动态刷新的类标注@RefreshScope 注解。

  • @RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是再创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象。

  • 如属性发生变更

    • 调用 ContextRefresher refresh() -->> RefreshScope refreshAll() 进行缓存清理方法调用;
    • 发送刷新事件通知,GenericScope 真正的清理方法destroy() 实现清理缓存。
  • 在下一次使用对象的时候,会调用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.当我们后续进行以来查找的时候,会绕过SingletonPrototype分支,进入最后一个分支,通过调用Scope接口的get()获取到该refresh作用域的实例。(AbstractBeanFactory.doGetBean

二、@RefreshScope注意事项

1. @RefreshScope使用注意事项

  • @RefreshScope作用的类,不能是final类,否则启动时会报错。
  • @RefreshScope不能单独使用,需要和其他其他bean注解结合使用,如:@Controller@Service@Component@Repository@Configuration等。
  • @RefreshScope 最好不要修饰在 @ScheduledlistenerTimmer等类中,因为配置的刷新会导致原来的对象被清除,需要重新使用对象才能出发生成新对象(但因为对象没了,又没法重新使用对象,死循环)

2. @RefreshScope动态刷新失效

考虑使用的bean是否是@RefreshScope生成的那个scopedTarget.beanName的 bean

springboot某些低版本貌似有问题,在Controller类上使用不会生效(网上有这么说的,没具体研究)

  • 解决方法1:注解上加属性@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
  • 解决方法2:直接使用其他类单独封装配置参数,使用@RefreshScope+@Value方式
  • 解决方法3:直接使用@ConfigurationProperties

3. 不使用@RefreshScope也能实现动态刷新

直接使用@ConfigurationProperties,并不需要加@RefreshScope就能实现动态更新。

@ConfigurationProperties实现动态刷新的原理:

@ConfigurationPropertiesConfigurationPropertiesRebinder这个监听器,监听着EnvironmentChangeEvent事件。当发生EnvironmentChange事件后,会重新构造原来的加了@ConfigurationProperties注解的Bean对象。这个是Spring Cloud的默认实现。

4. 静态变量利用@RefreshScope动态刷新的坑(求大佬解答)

@RefreshScope
@Component
public class TestConfig {
public static int url;

@Value("${pesticide.url}")
public void setUrl(int url) {
TestConfig.url = url;
}

public void getUrl() {
}
}
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private TestConfig testConfig;

@GetMapping("testConfig")
public int testConfig(){
System.out.println("TestConfig:"+ TestConfig.url);
testConfig.getUrl();
System.out.println("TestConfig:"+ TestConfig.url);
return TestConfig.url;
}
}

1.url初始配置的值为1

请求接口日志:

TestConfig:1
TestConfig:1

2.修改url配置的值为2,动态刷新成功

请求接口日志:

TestConfig:1
TestConfig:2

这里就出现了问题,不调用@RefreshScope生产的代理对象testConfig的方法前(注意,该方法内无代码),取到的值还是为1;调了之后,取到的值为2.后续再次请求接口,取到的值都为2。

TestConfig:2
TestConfig:2
TestConfig:2
TestConfig:2

个人大胆猜想原因:参考上面@RefreshScope 实现流程可知,在第2步骤动态刷新成功时,此时仅仅是再创建类一个代理对象,并清除了实际对象的缓存;当再次通过代理对象来使用,才会触发创建一个新的实例对象,此时才会更新url的值。所以使用静态变量来是实现动态刷新时,一点要注意:使用对象才能出发创建新的实际对象,更新静态变量的值。

Spring Cloud的参考文档指出:

@RefreshScope在@Configuration类上工作,但可能导致令人惊讶的行为:例如,这并不意味着该类中定义的所有@Beans本身都是@RefreshScope。具体来说,依赖于这些bean的任何东西都不能依赖于刷新启动时对其进行更新,除非它本身在@RefreshScope中从刷新的@Configuration重新初始化(在刷新中将其重建并重新注入其依赖项,此时它们将被刷新)。

三、使用@RefreshScope的bean问题

这里之所以要会讨论使用@RefreshScope的bean问题,由上面上面所讲可以总结得到:

  • 使用 @RefreshScope 注解的 bean,不仅会生成一个名为beanName的bean,默认情况下同时会生成名为scopedTarget.beanName的bean
  • 使用 @RefreshScope 注解的会生成一个代理对象,通过这个代理对象来调用名为scopedTarget.beanName的 bean
  • 刷新操作会导致原来的名为scopedTarget.beanName的bean被清除,再次使用会新生成新的名为scopedTarget.beanName的bean,但原来的代理对象不会变动

下面举例说明:

nacos配置

test:
value: 1

配置类获取配置值

@Data
@Component
@RefreshScope
public class TestConfig {

@Value("${test.value}")
private String value;
}

测试接口

@RestController
public class TestController {

@Autowired
private TestConfig testConfig;

@RequestMapping("test11")
public void test11() {
// 代理对象
System.out.println("@Autowired bean==========" + testConfig.getClass().getName());

// 代理对象
TestConfig bean = SpringUtils.getBean(TestConfig.class);
System.out.println("Class bean==========" + bean.getClass().getName());

// 代理对象
Object bean1 = SpringUtils.getBean("testConfig");
System.out.println("name(testConfig) bean==========" + bean1.getClass().getName());

// 原类对象
Object bean2 = SpringUtils.getBean("scopedTarget.testConfig");
System.out.println("name(scopedTarget.testConfig) bean==========" + bean2.getClass().getName());

System.out.println("================================================================================");

}
}

测试

@Autowired注入的是代理对象

  • 通过Class得到的是代理对象
  • 通过名为beanName的得到的是代理对象
  • 通过名为scopedTarget.beanName的得到的是由@RefreshScope生成的那个原类对象

修改配置的值,测试

test:
value: 2

动态刷新后,代理对象没有变化,由@RefreshScope生成的那个原类对象被清除后重新生成了一个新的原类对象

小结:

  • @Autowired方式注入的是代理对象
  • beanName的得到的是代理对象
  • scopedTarget.beanName的得到的@RefreshScope生成的那个原类对象
  • 代理对象不会随着配置刷新而更新
  • @RefreshScope生成的那个原类对象会随着配置的刷新而更新(属性时清除原来的,使用时才生成新的)

四、其它配置刷新方式

这种方法必须有 spring-boot-starter-actuator 这个starter才行。

POST http://localhost:7031/refresh

refresh的底层原理详见:org.springframework.cloud.context.refresh.ContextRefresher#refresh

SpringCloud2.0以后,没有/refresh手动调用的刷新配置地址。

SpringCloud2.0前

加入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在类上,变量上打上@RefreshScope的注解

在启动的时候,都会看到

RequestMappingHandlerMapping : Mapped "{/refresh,methods=[post]}"

也就是SpringCloud暴露了一个接口 /refresh来给我们去刷新配置,但是SpringCloud 2.0.0以后,有了改变。

SpringCloud 2.0后

我们需要在bootstrap.yml里面加上需要暴露出来的地址

management:
endpoints:
web:
exposure:
include: refresh,health

现在的地址也不是/refresh了,而是/actuator/refresh

文章目录
  1. 1. 一、@RefreshScope动态刷新原理
    1. 1.0.0.1. 单独管理Bean生命周期
    2. 1.0.0.2. 重新创建Bean
  • 1.1. 1. @Scope注解
  • 1.2. 2. @RefreshScope注解
  • 1.3. 3. Scope接口
  • 1.4. 4. @RefreshScope 实现流程
  • 1.5. 5. @RefreshScope原理总结
  • 2. 二、@RefreshScope注意事项
    1. 2.1. 1. @RefreshScope使用注意事项
    2. 2.2. 2. @RefreshScope动态刷新失效
    3. 2.3. 3. 不使用@RefreshScope也能实现动态刷新
    4. 2.4. 4. 静态变量利用@RefreshScope动态刷新的坑(求大佬解答)
      1. 2.4.0.1. 1.url初始配置的值为1
      2. 2.4.0.2. 2.修改url配置的值为2,动态刷新成功
  • 3. 三、使用@RefreshScope的bean问题
    1. 3.0.0.1. 小结:
  • 4. 四、其它配置刷新方式
    1. 4.0.0.1. SpringCloud2.0前
    2. 4.0.0.2. SpringCloud 2.0后