对于Spring-Cloud 集成Nacos作为配置中心,集成比较简单,而且使用只需在bean上增加@RefreshScope注解即可。
入门实例参考官网:quick-start
按照官网配置在controller中使用@Value注解的变量,并使用@RefreshScope注解对应的Controller,通过Nacos配置界面更改配置值后,访问controller接口可以实时生效。
但是,如果采用该方式直接在一个service或者component中定义动态配置,并且仅在service或者component内使用类内部定义的动态配置,则会出现失效问题!
问题演示:启动服务后,通过nacos修改配置print.paused=true,不生效。
@Slf4j
@RefreshScope
@Component
public class HelloHandler implements InitializingBean {
@Value("${print.paused:false}")
private volatile boolean paused;
private final ThreadPoolExecutor executor = ThreadUtil.newPool(1, 1, 0L, 1, "hello_thread");
@Override
public void afterPropertiesSet() {
this.executor.submit(this::hello);
}
private void hello() {
while (true) {
if (paused) {
log.info("borker paused.");
break;
}
try {
System.out.println("hello world");
ThreadUtil.sleep(5000);
} catch (Exception e) {
log.error("error => {}", e.getMessage());
ThreadUtil.sleep(5000);
}
}
}
}
跟踪代码发现nacos发布的配置可以正常被服务接收到,并且最终通过ContextRefresher.refresh()方法进行了刷新操作,refresh() -> org.springframework.cloud.context.refresh.RefreshScope.refreshAll()方法。并发布RefreshScopeRefreshedEvent事件,
在这之间通过ConfigurationPropertiesRebinder重新绑定了环境变量的properties对应的bean,先销毁后创建,debug发现通过配置的Nacos dataID: demo-dev.properties对应的PropertySource中存在已经发布更新的变量。但是HelloHandler中的变量依然是未更新的。
到此处出现第一种方式,就是将配置变量提出来单独作为一个HelloProperties类,使用@ConfigurationProperties注解标注配置类,在通过Nacos发布配置后,配置类会被重新rebind,此时变量动态修改生效,使用的地方@EnableConfigurationProperties,通过@Autowired注入。
@Data
@ConfigurationProperties("print")
public class HelloProperties {
@Value("${paused:false}")
private volatile boolean paused;
}
后来通过查阅资料,在通过@RefreshScope注解的类会在context启动时创建一个动态代理类,
ClassPathBeanDefinitionScanner.doScan(..packageName) -> AnnotationConfigUtils.applyScopedProxyMode(..) 该类会判断BeanDefinition的scope为ScopedProxyMode.TARGET_CLASS的bean为其创建动态代理,代理类为ScopedProxyFactoryBean是个FactoryBean。而RefreshScope类继承GenericScope,在postProcessBeanDefinitionRegistry中会对该类进行判断,如果判断是ScopedProxyFactoryBean,将其beanClass设置为LockedScopedProxyFactoryBean是ScopedProxyFactoryBean的子类同时也是FactoryBean,所以最终获取实际被代理的类就是调用LockedScopedProxyFactoryBean的getObject类,debug发现getObject返回了一个proxy,该proxy是SimpleBeanTargetSource,该类每次从容器中获取被代理的bean,获取时首先经过GenericScope.get(...)获取,scoped的bean被缓存在一个Map中,在发布动态配置时会清空map重新创建一个bean放到map中,重新创建的bean变量就是动态获取到的最新值。而重建的bean是动态代理前缀为scopedTarget.的bean,被wrap的原始bean不会发生变化,所以配置不会被更新。
该代理类生成的bean名称为:scopedTarget.helloHandler, 在原beanName基础上加了个 scopedTarget. 前缀,而原bean的名称还是helloHandler, 而在原bean中执行的代码获取的变量就是一开始的默认值,并不会动态刷新。通过idea的beans插件或者actuator监控可以发现HelloHandler类型的bean有两个,一个是helloHandler,一个是scopedTarget.helloHandler,每次发布更新配置时,生效的是scopedTarget.helloHandler,如果其他地方有Autowired使用helloHandler bean,每次获取时最终通过RefreshScope.get(x..)获取到的是scopedTarget.helloHandler,而bean本身内部获取内部动态属性时获取的是helloHandler的所以不生效。
第二种方式将属性提出来最为HelloProperties并且使用@RefreshScope+@Component注解,在需要使用的地方直接@Autowired注入即可。
@Data
@Component
@RefreshScope
public class HelloProperties {
@Value("${print.paused:false}")
private volatile boolean paused;
}
@Configuration注解初始化的bean动态更新
// configuration 配置类
@Configuration
public class TestConfiguration {
@Bean
@RefreshScope
public TestBean test(@Value("${test:hello}") String name) {
return new TestBean(name);
}
}
// bean
@Slf4j
@Data
public class TestBean {
private String name;
public TestBean(String name) {
this.name = name;
}
public void print() {
log.info("print => {}", name);
}
}
// controller
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private TestBean bean;
@GetMapping("name")
public String test() {
bean.print();
return bean.getName();
}
}
原文链接:https://blog.csdn.net/fly_leopard/article/details/107489937