springcloud关闭自动配置(SpringCloud配置动态刷新)
在SpringIOC中,BeanScope(Bean的作用域)影响了Bean的管理方式。
Bean的作用域:
例如创建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 config或sprring 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 实现流程5. @RefreshScope原理总结
- 需要动态刷新的类标注@RefreshScope 注解。
- @RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是再创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象。
- 如属性发生变更
- 调用 ContextRefresher refresh() -->> RefreshScope refreshAll() 进行缓存清理方法调用;
- 发送刷新事件通知,GenericScope 真正的清理方法destroy() 实现清理缓存。
- 在下一次使用对象的时候,会调用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法创建一个新的对象,并存入缓存中,此时新对象因为Spring 的装配机制就是新的属性了。
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使用注意事项2. @RefreshScope动态刷新失效
- @RefreshScope作用的类,不能是final类,否则启动时会报错。
- @RefreshScope不能单独使用,需要和其他其他bean注解结合使用,如:@Controller、@Service、@Component、@Repository、@Configuration等。
- @RefreshScope 最好不要修饰在 @Scheduled、listener、Timmer等类中,因为配置的刷新会导致原来的对象被清除,需要重新使用对象才能出发生成新对象(但因为对象没了,又没法重新使用对象,死循环)
考虑使用的bean是否是@RefreshScope生成的那个scopedTarget.beanName的 bean
springboot某些低版本貌似有问题,在Controller类上使用不会生效(网上有这么说的,没具体研究)
3. 不使用@RefreshScope也能实现动态刷新
- 解决方法1:注解上加属性@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
- 解决方法2:直接使用其他类单独封装配置参数,使用@RefreshScope @Value方式
- 解决方法3:直接使用@ConfigurationProperties
直接使用@ConfigurationProperties,并不需要加@RefreshScope就能实现动态更新。
@ConfigurationProperties实现动态刷新的原理:
@ConfigurationProperties有ConfigurationPropertiesRebinder这个监听器,监听着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() { } }
1.url初始配置的值为1
@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; } }
请求接口日志:
2.修改url配置的值为2,动态刷新成功
TestConfig:1 TestConfig:1
请求接口日志:
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
原文链接:https://mp.weixin.qq.com/s/Ym0Kmohbdegq_2RGdfYUGg
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com