從源碼角度,解析Nacos Config客戶端與Spring Boot、Spring Cloud的深度集成
原創博文,轉載請注明來源
Nacos與Spring Boot集成
@NacosPropertySource和@NacosValue
@PropertySource的用法並不陌生,它是spring原生的注解,我們可以這么用:
@Configuration
@PropertySource(value = "classpath:demo.properties",ignoreResourceNotFound = false)
public class SpringPropertysourceApplication {
//...
}
意思是:把在classpath路徑下,名為demo.properties的配置文件注入到spring容器中,這樣,我們就可以直接在類的屬性上通過@Value注解獲取到demo.properties屬性值了。
Nacos為了達到以上目的,提供了一個叫@NacosPropertySource的注解,和@PropertySource目的一樣:把配置注入到spring容器;使用方式一樣,用於任意被spring管理的類上。當然,Nacos提供了更高級的功能,比如Property變更,自動刷新的功能,下面來分析一下,Nacos是怎么集成的
com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor
這個類實現了org.springframework.beans.factory.config.BeanFactoryPostProcessor(Spring鈎子,它在所有spring bean定義生成后,實例化之前調用,允許覆蓋或添加其屬性)等接口,主要作用是,掃描由spring所有的bean,查看其類上,是否有@NacosPropertySource注解,如果有的話,則生成com.alibaba.nacos.spring.core.env.NacosPropertySource實例對象(@NacosPropertySource注解標注了當前PropertySource指定的DataId,也就是一個完成的配置文件,生成實例其實就是調用Nacos原生API獲取配置構造NacosPropertySource對象),再把實例添加到spring env.PropertySources中去,其實完成這幾步,我們就可以通過使用@Value或者ENV.getProperty()這種方式獲取到由Nacos管理的配置項了。
NacosPropertySourcePostProcessor代碼片段:
上面的功能僅僅是把Nacos的配置注入到spring中,那動態刷新的功能怎么做的呢。
回顧一下,原生的Nacos sdk是怎么樣監聽配置變化的
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("recieve:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
可以看到,監聽器與配置文件(DataId)是一對一的,所以,對於一個NacosPropertySource來說,應該有一個對應的監聽器,在上訴NacosPropertySourcePostProcessor的代碼片段截圖中可以看到對應的代碼:
這個添加listener的邏輯可以根據上面Nacos sdk的用法得出:
可以看到,在listener回調的邏輯里面,當有配置變更時會重新生成NacosPropertySource並替換掉ENV中過時的NacosPropertySource,完成這個部分的邏輯,我們通過ENV.getProperty()就可以動態獲取到屬性值了,但是通過@Value方式注入的到Bean對象的配置項,由於Bean已經生成,還是沒辦法動態更新,那Nacos是怎么做的?
com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor
上面說到如果通過ENV獲取配置項的話,已經可以做到動態的目的了,但是如果此時持有配置項的Bean已經生成,則需要通過反射的機制,去動態更新了,從功能設計角度舉個例子來講清原理:
有類TestController,通過@Value的方式把demo.properties中app.config.threshold的配置項注入到屬性threshold,那應該是這樣的:
@RestController
@PropertySource(value = "classpath:demo.properties")
public class TestController {
@Value("${app.config.threshold}")
private String threshold;
}
那如果要通過反射設置屬性的話那就需要這么一個映射關系:
app.config.threshold -> TestController.threshold
所以如果把這個映射關系保存在內存,當listener回調通知的時候,找到配置中的對應屬性,反射設置進去就好了。
Nacos也是這么做的:
NacosValueAnnotationBeanPostProcessor實現了org.springframework.beans.factory.config.BeanPostProcessor(spring鈎子函數,當bean對象實例化完成,注入容器之前調用 ),在其Object postProcessBeforeInitialization(Object bean, String beanName) 方法中,我們可以解析bean中所有注有@Value的注解,並將上訴映射關系,保存在內存中:
其中doWithFields實現如下:
其中有一點需要注意的地方,Nacos並沒有使用原生的@Value注解去達到動態刷新的目的,因為違背了spring使用@Value的初衷,nacos自己實現了@NacosValue的注解
綜上所訴,@NacosPropertySource和@NacosValue組合使用達到動態配置的效果是這樣實現的:
@NacosValue
前面解析了@NacosPropertySource和@NacosValue組合使用達到動態配置原理,遺漏了一個細節點就是使用自定義注解@NacosValue是怎么在bean初始化的過程中注入屬性的(前面說的動態刷新,是通過反射設置的,是建立在bean已經初始化完畢的基礎上)。
還是com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor這個類,除了上面說的postProcessBeforeInitialization建立配置項和屬性的映射關系這個方法外,還有兩個方法,就是用於@NacosValue在bean初始過程中注入屬性的:
簡單理解一下這兩個方法的目的:
-
doGetInjectedBean首先獲取了@NacosValue中的配置項比如app.config.threshold,通過beanFactory解析出配置項對應的值(在ENV中),Member是一個隊Field和Method的抽象類,如果Mem是Field則把取出的值進行轉換和Field保持一致,如果是方法,則取出方法參數的Field進行轉換
-
buildInjectedObjectCacheKey用於對doGetInjectedBean方法中已經轉換過的值生一個cacheKey,這樣就不用做多次轉換的無用功
這兩個方法都不是spring的鈎子函數的方法,是在alibaba的spring-context-support包下,抽象類com.alibaba.spring.beans.factory.annotation.AnnotationInjectedBeanPostProcessor提供的,這個類的作用是:解析被子類泛型指定的注解(public class NacosValueAnnotationBeanPostProcessor extends ValueAnnotationBeanPostProcessor
AnnotationInjectedBeanPostProcessor這個類的實現,參照了org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor的實現,而這個類就是用於Spring原生注解@Autowired 和@Value在bean初始化過程中注入依賴的。