之所以會查找這篇文章,是因為要解決這樣一個問題:
當我使用了jasypt進行配置文件加解密后,如果再使用refresh 去刷新配置,則自動加解密會失效。
原因分析:刷新不是我之前想象的直接調用config獲取最新配置的,而是通過重新創建一個SpringBoot環境(非WEB),等到SpringBoot環境啟動時就相當於重新啟動了一個非web版的服務器。此時config會自動加載到最新的配置。這個過程類似於啟動服務器。相當於是重新啟動了一個springboot環境,而這個環境中沒有加解密功能。拿回的配置,都是原始字符串。
首先先介紹下實現后的效果:
1、在需要動態配置屬性的類上添加注解@RefreshScope表示此類Scope為refresh類型的
2、啟動工程,修改config-server對應的配置文件,這里修改的是system.order.serverName
3、以post的方式調用refresh接口,返回修改后的key值
4、訪問infoTest接口,可以看到修改后的值
詳細流程:
依次啟動config-server,eureka-server后,再啟動訂單服務order-service,首先訪問http://localhost:8100/infoTest 查看serverName的值:
然后修改config-server工程下的order-service-dev.properties中的system.order.serverName為Order Service modified,通過postman使用POST的方式調用refresh接口,可以看到返回了修改的屬性的key,繼續訪問http://localhost:8100/infoTest 顯示新的數據如下:
可以看到對應的屬性值已經變化了。
基本使用演示完了,下面該對屬性刷新原理進行詳細探究:
1、先從基本入口refresh接口入手,在項目啟動時可以看到log
Mapped "{[/refresh || /refresh.json],methods=[POST]}" onto public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint.invoke()
可以知道refresh對應的handler是GenericPostableMvcEndpoint的invoke方法
2、繼續進入GenericPostableMvcEndpoint看invoke源碼:
@RequestMapping(method = RequestMethod.POST) @ResponseBody @Override public Object invoke() { if (!getDelegate().isEnabled()) { return new ResponseEntity<>(Collections.singletonMap( "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND); } return super.invoke(); }
非常簡單,直接調用父類的invoke方法,繼續跟進到AbstractEndpointMvcAdapter類,發現最后調用的是delegate的invoke方法,而且delegate是從構造方法傳入的。
protected Object invoke() { if (!this.delegate.isEnabled()) { // Shouldn't happen - shouldn't be registered when delegate's disabled return getDisabledResponse(); } return this.delegate.invoke(); }
3、步驟2可以看出最終調用的是對應泛型的invoke方法,那么找到注入refresh接口的地方,通過查詢哪里使用到此類,查找到LifecycleMvcEndpointAutoConfiguration,通過refreshMvcEndpoint方法注入了refresh接口
@Bean @ConditionalOnBean(RefreshEndpoint.class) public MvcEndpoint refreshMvcEndpoint(RefreshEndpoint endpoint) { return new GenericPostableMvcEndpoint(endpoint); }
4、可以那么GenericPostableMvcEndpoint中的delegate就是RefreshEndpoint,轉至研究RefreshEndpoint
@ManagedOperation public String[] refresh() { Set<String> keys = contextRefresher.refresh(); return keys.toArray(new String[keys.size()]); } @Override public Collection<String> invoke() { return Arrays.asList(refresh()); }
發現invoke方法很簡單,只是返回一個修改過的屬性key的集合對象。核心方法contextRefresher.refresh()
5、跟進到contextRefresher.refresh()方法,這里就是核心了
public synchronized Set<String> refresh() { //獲取目前系統的配置 Map<String, Object> before = extract( this.context.getEnvironment().getPropertySources()); //獲取最新配置 addConfigFilesToEnvironment(); //對比目前系統配置和最新配置,返回修改后的屬性 Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); //通知系統配置變更 this.context.publishEvent(new EnvironmentChangeEvent(keys)); //對應的bean刷新 this.scope.refreshAll(); return keys; }
6、核心就是獲取最新的配置,那么是如何獲取的呢?之前
還以為是通過直接調用config配置加載呢,那么繼續看addConfigFilesToEnvironment源碼:
private void addConfigFilesToEnvironment() { ConfigurableApplicationContext capture = null; try { StandardEnvironment environment = copyEnvironment( this.context.getEnvironment()); //這里就是核心了,啟動SpringBoot環境 SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class) .bannerMode(Mode.OFF).web(false).environment(environment); // Just the listeners that affect the environment (e.g. excluding logging // listener because it has side effects) builder.application() .setListeners(Arrays.asList(new BootstrapApplicationListener(), new ConfigFileApplicationListener())); capture = builder.run(); if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) { environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE); } MutablePropertySources target = this.context.getEnvironment() .getPropertySources(); String targetName = null; for (PropertySource<?> source : environment.getPropertySources()) { String name = source.getName(); if (target.contains(name)) { targetName = name; } if (!this.standardSources.contains(name)) { if (target.contains(name)) { target.replace(name, source); } else { if (targetName != null) { target.addAfter(targetName, source); } else { if (target.contains("defaultProperties")) { target.addBefore("defaultProperties", source); } else { target.addLast(source); } } } } } } finally { ConfigurableApplicationContext closeable = capture; closeable.close(); } }
通過以上代碼可知,刷新不是我之前想象的直接調用config獲取最新配置的,而是通過重新創建一個SpringBoot環境(非WEB),等到SpringBoot環境啟動時就相當於重新啟動了一個非web版的服務器。此時config會自動加載到最新的配置。這個過程類似於啟動服務器。
等到服務器啟動成功后,獲取到最新的配置,然后跟原來的配置進行對比,返回修改過的key值。
7、獲取到修改后的配置后,發出EnvironmentChangeEvent事件,ConfigurationPropertiesRebinder監聽了此事件,調用rebind方法進行配置重新加載
8、this.scope.refreshAll();首先銷毀scope為refresh的bean。然后發出RefreshScopeRefreshedEvent事件,通知bean生命周期已經變更,已知兩個類EurekaDiscoveryClientConfiguration.EurekaClientConfigurationRefresher接收了此事件,EurekaClientConfigurationRefresher接收到此事件后,進行對eureka服務器重連的操作。
總結:通過以上步驟,配置刷新基本流程就是再起一個SpringBoot環境,加載最新配置,與目前環境配置對應,篩選出變化后的屬性,將scope類型為refresh的bean銷毀。等到下一次獲取時bean時重新裝配bean,這樣最新配置就注入ok了。具體其他細節自己Debug就行了。
本文中的代碼已提交至: https://gitee.com/cmlbeliever/springcloud 歡迎Star