相信大家都知道送漠,每個項(xiàng)目中會有一些配置信息放在一個獨(dú)立的properties文件中喧兄,比如application.properties。這個文件中會放一些常量的配置宅粥,比如數(shù)據(jù)庫連接信息参袱、線程池大小、限流參數(shù)秽梅。
在傳統(tǒng)的開發(fā)模式下抹蚀,這種方式很方便,一方面能夠?qū)ε渲眠M(jìn)行統(tǒng)一管理企垦,另一方面环壤,我們在維護(hù)的時(shí)候很方便。
但是隨著業(yè)務(wù)的發(fā)展以及架構(gòu)的升級钞诡,在微服務(wù)架構(gòu)中郑现,服務(wù)的數(shù)量以及每個服務(wù)涉及到的配置會越來越多,并且對于配置管理的需求越來越高臭增,比如要求實(shí)時(shí)性、獨(dú)立性竹习。
另外誊抛,在微服務(wù)架構(gòu)下,會涉及到不同的環(huán)境下的配置管理整陌、灰度發(fā)布拗窃、動態(tài)限流瞎领、動態(tài)降級等需求,包括對于配置內(nèi)容的安全與權(quán)限随夸,所以傳統(tǒng)的配置維護(hù)方式很難達(dá)到需求九默。
因此,就產(chǎn)生了分布式配置中心宾毒。
- 傳統(tǒng)的配置方式不方便維護(hù)
- 配置內(nèi)容的安全和訪問權(quán)限驼修,在傳統(tǒng)的配置方式中很難實(shí)現(xiàn)
- 更新配置內(nèi)容時(shí),需要重啟
配置中心的工作流程
<center>圖11-1</center>
Spring Boot的外部化配置
在本次課程中诈铛,我們會Zookeeper集成到Spring Boot的外部化配置中乙各,讓用戶無感知的使用配置中心上的數(shù)據(jù)作為數(shù)據(jù)源,所以我們需要先了解Spring Boot中的外部化配置幢竹。
Spring Boot的外部化配置是基于Environment來實(shí)現(xiàn)的耳峦,它表示Spring Boot應(yīng)用運(yùn)行時(shí)的環(huán)境信息,先來看基本使用
Environment的使用
-
在spring boot應(yīng)用中焕毫,修改aplication.properties配置
key=value
-
創(chuàng)建一個Controller進(jìn)行測試
@RestController public class EnvironementController { @Autowired Environment environment; @GetMapping("/env") public String env(){ return environment.getProperty("key"); } }
@Value注解使用
在properties文件中定義的屬性蹲坷,除了可以通過environment的getProperty方法獲取之外,spring還提供了@Value注解邑飒,
@RestController
public class EnvironementController {
@Value("${env}")
private String env;
@GetMapping("/env")
public String env(){
return env;
}
}
spring容器在加載一個bean時(shí)循签,當(dāng)發(fā)現(xiàn)這個Bean中有@Value注解時(shí),那么它可以從Environment中將屬性值進(jìn)行注入幸乒,如果Environment中沒有這個屬性懦底,則會報(bào)錯。
Environment設(shè)計(jì)猜想
Spring Boot的外部化配置罕扎,不僅僅只是appliation.properties聚唐,包括命令行參數(shù)、系統(tǒng)屬性腔召、操作系統(tǒng)環(huán)境變量等杆查,都可以作為Environment的數(shù)據(jù)來源。
- @Value("${java.version}") 獲取System.getProperties 臀蛛, 獲取系統(tǒng)屬性
- 配置command的jvm參數(shù)亲桦,
-Denvtest=command
,然后通過@Value("${envtest}")
<center>圖11-2</center>
- 第一部分是屬性定義浊仆,這個屬性定義可以來自于很多地方客峭,比如application.properties、或者系統(tǒng)環(huán)境變量等抡柿。
- 然后根據(jù)約定的方式去指定路徑或者指定范圍去加載這些配置舔琅,保存到內(nèi)存中。
- 最后洲劣,我們可以根據(jù)指定的key從緩存中去查找這個值备蚓。
擴(kuò)展Environment
我們可以自己擴(kuò)展Environment中的數(shù)據(jù)源课蔬,代碼如下;
其中,EnvironmentPostProcessor:它可以在spring上下文構(gòu)建之前可以設(shè)置一些系統(tǒng)配置郊尝。
CusEnvironmentPostProcessor
public class CusEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final Properties properties=new Properties();
private String propertiesFile="custom.properties";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource=new ClassPathResource(propertiesFile);
environment.getPropertySources().addLast(loadProperties(resource));
}
private PropertySource<?> loadProperties(Resource resource){
if(!resource.exists()){
throw new IllegalArgumentException("file:{"+resource+"} not exist");
}
try {
properties.load(resource.getInputStream());
return new PropertiesPropertySource(resource.getFilename(),properties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
custom.properties
在classpath目錄下創(chuàng)建custom.properties文件
name=mic
age=18
spring.factories
在META-INF目錄下創(chuàng)建spring.factories文件二跋,因?yàn)镋nvironmentPostProcessor的擴(kuò)展實(shí)現(xiàn)是基于SPI機(jī)制完成的。
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.springbootzookeeper.CusEnvironmentPostProcessor
TestController
創(chuàng)建測試類流昏,演示自定義配置加載的功能扎即。
@RestController
public class TestController {
@Value("${name}")
public String val;
@GetMapping("/")
public String say(){
return val;
}
}
總結(jié)
通過上面的例子我們發(fā)現(xiàn),在Environment中横缔,我們可以通過指定PropertySources來增加Environment外部化配置信息铺遂,使得在Spring Boot運(yùn)行期間自由訪問到這些配置。
那么我們要實(shí)現(xiàn)動態(tài)配置中心茎刚,無非就是要在啟動的時(shí)候襟锐,從遠(yuǎn)程服務(wù)器上獲取到數(shù)據(jù)保存到PropertySource中,并且添加到Environment膛锭。
下面我們就開始來實(shí)現(xiàn)這個過程粮坞。
Zookeeper實(shí)現(xiàn)配置中心
在本小節(jié)中,主要基于Spring的Environment擴(kuò)展實(shí)現(xiàn)自己的動態(tài)配置中心初狰,代碼結(jié)構(gòu)如圖11-3所示莫杈。
<center>圖11-3</center>
自定義配置中心的相關(guān)說明
在本次案例中,我們并沒有完全使用EnvironmentPostProcessor這個擴(kuò)展點(diǎn)奢入,而是基于SpringFactoriesLoader自定義了一個擴(kuò)展點(diǎn)筝闹,主要目的是讓大家知道EnvironmentPostProcessor擴(kuò)展點(diǎn)的工作原理,以及我們以后自己也可以定義擴(kuò)展點(diǎn)腥光。
代碼實(shí)現(xiàn)
以下是所有代碼的實(shí)現(xiàn)過程关顷,按照下面這個步驟去開發(fā)即可完成動態(tài)配置中心。
ZookeeperApplicationContextInitializer
ApplicationContextInitializer擴(kuò)展武福,它是在ConfigurableApplicationContext通過調(diào)用refresh函數(shù)來初始化Spring容器之前會進(jìn)行回調(diào)的一個擴(kuò)展方法议双,我們可以在這個擴(kuò)展中實(shí)現(xiàn)Environment的擴(kuò)展。
所以這個類的主要作用就是在ApplicationContext完成refresh之前捉片,擴(kuò)展Environment平痰,增加外部化配置注入。
public class ZookeeperApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
//PropertySourceLocator接口支持?jǐn)U展自定義配置加載到spring Environment中伍纫。
private final List<PropertySourceLocator> propertySourceLocators;
public ZookeeperApplicationContextInitializer(){
//基于SPI機(jī)制加載所有的外部化屬性擴(kuò)展點(diǎn)
ClassLoader classLoader=ClassUtils.getDefaultClassLoader();
//這部分的代碼是SPI機(jī)制
propertySourceLocators=new ArrayList<>(SpringFactoriesLoader.loadFactories(PropertySourceLocator.class,classLoader));
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//獲取運(yùn)行的環(huán)境上下文
ConfigurableEnvironment environment=applicationContext.getEnvironment();
//MutablePropertySources它包含了一個CopyOnWriteArrayList集合宗雇,用來包含多個PropertySource。
MutablePropertySources mutablePropertySources = environment.getPropertySources();
for (PropertySourceLocator locator : this.propertySourceLocators) {
//回調(diào)所有實(shí)現(xiàn)PropertySourceLocator接口實(shí)例的locate方法莹规,收集所有擴(kuò)展屬性配置保存到Environment中
Collection<PropertySource<?>> source = locator.locateCollection(environment,applicationContext);
if (source == null || source.size() == 0) {
continue;
}
//把PropertySource屬性源添加到environment中赔蒲。
for (PropertySource<?> p : source) {
//addFirst或者Last決定了配置的優(yōu)先級
mutablePropertySources.addFirst(p);
}
}
}
}
創(chuàng)建classpath:/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperApplicationContextInitializer
PropertySourceLocator
PropertySourceLocator接口支持?jǐn)U展自定義配置加載到spring Environment中。
public interface PropertySourceLocator {
PropertySource<?> locate(Environment environment,ConfigurableApplicationContext applicationContext);
//Environment表示環(huán)境變量信息
//applicationContext表示應(yīng)用上下文
default Collection<PropertySource<?>> locateCollection(Environment environment, ConfigurableApplicationContext applicationContext) {
return locateCollection(this, environment,applicationContext);
}
static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
Environment environment,ConfigurableApplicationContext applicationContext) {
PropertySource<?> propertySource = locator.locate(environment,applicationContext);
if (propertySource == null) {
return Collections.emptyList();
}
return Arrays.asList(propertySource);
}
}
ZookeeperPropertySourceLocator
ZookeeperPropertySourceLocator用來實(shí)現(xiàn)基于Zookeeper屬性配置的擴(kuò)展點(diǎn),它會訪問zookeeper獲取遠(yuǎn)程服務(wù)器數(shù)據(jù)嘹履。
public class ZookeeperPropertySourceLocator implements PropertySourceLocator{
private final CuratorFramework curatorFramework;
private final String DATA_NODE="/data"; //僅僅為了演示,所以寫死目標(biāo)數(shù)據(jù)節(jié)點(diǎn)
public ZookeeperPropertySourceLocator() {
curatorFramework= CuratorFrameworkFactory.builder()
.connectString("192.168.221.128:2181")
.sessionTimeoutMs(20000).connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("config").build();
curatorFramework.start();
}
@Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
System.out.println("開始加載遠(yuǎn)程配置到Environment中");
CompositePropertySource composite = new CompositePropertySource("configService");
try {
Map<String,Object> dataMap=getRemoteEnvironment();
//基于Map結(jié)構(gòu)的屬性源
MapPropertySource mapPropertySource=new MapPropertySource("configService",dataMap);
composite.addPropertySource(mapPropertySource);
addListener(environment,applicationContext);
} catch (Exception e) {
e.printStackTrace();
}
return composite;
}
private Map<String,Object> getRemoteEnvironment() throws Exception {
String data=new String(curatorFramework.getData().forPath(DATA_NODE));
//暫時(shí)支持json格式
ObjectMapper objectMapper=new ObjectMapper();
Map<String,Object> map=objectMapper.readValue(data, Map.class);
return map;
}
//添加節(jié)點(diǎn)變更事件
private void addListener(Environment environment, ConfigurableApplicationContext applicationContext){
NodeDataCuratorCacheListener curatorCacheListener=new NodeDataCuratorCacheListener(environment,applicationContext);
CuratorCache curatorCache=CuratorCache.build(curatorFramework,DATA_NODE,CuratorCache.Options.SINGLE_NODE_CACHE);
CuratorCacheListener listener=CuratorCacheListener
.builder()
.forChanges(curatorCacheListener).build();
curatorCache.listenable().addListener(listener);
curatorCache.start();
}
}
配置擴(kuò)展點(diǎn): classpath:/META-INF/spring.factories
com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator
配置動態(tài)變更邏輯
NodeDataCuratorCacheListener
NodeDataCuratorCacheListener用來實(shí)現(xiàn)持久化訂閱機(jī)制债热,當(dāng)目標(biāo)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變更時(shí)砾嫉,需要收到變更并且應(yīng)用。
public class NodeDataCuratorCacheListener implements CuratorCacheListenerBuilder.ChangeListener {
private Environment environment;
private ConfigurableApplicationContext applicationContext;
public NodeDataCuratorCacheListener(Environment environment, ConfigurableApplicationContext applicationContext) {
this.environment = environment;
this.applicationContext=applicationContext;
}
@Override
public void event(ChildData oldNode, ChildData node) {
System.out.println("數(shù)據(jù)發(fā)生變更");
String resultData=new String(node.getData());
ObjectMapper objectMapper=new ObjectMapper();
try {
Map<String,Object> map=objectMapper.readValue(resultData, Map.class);
ConfigurableEnvironment cfe=(ConfigurableEnvironment)environment;
MapPropertySource mapPropertySource=new MapPropertySource("configService",map);
cfe.getPropertySources().replace("configService",mapPropertySource);
//發(fā)布事件窒篱,用來更新@Value注解對應(yīng)的值(事件機(jī)制可以分兩步演示)
applicationContext.publishEvent(new EnvironmentChangeEvent(this));
System.out.println("數(shù)據(jù)更新完成");
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
EnvironmentChangeEvent
定義一個環(huán)境變量變更事件焕刮。
public class EnvironmentChangeEvent extends ApplicationEvent {
public EnvironmentChangeEvent(Object source) {
super(source);
}
}
ConfigurationPropertiesRebinder
ConfigurationPropertiesRebinder接收事件,并重新綁定@Value注解的數(shù)據(jù)墙杯,使得數(shù)據(jù)能夠動態(tài)改變
@Component
public class ConfigurationPropertiesRebinder implements ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private Environment environment;
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans,Environment environment) {
this.beans = beans;
this.environment=environment;
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
rebind();
}
public void rebind(){
this.beans.getFieldMapper().forEach((k,v)->{
v.forEach(f->f.resetValue(environment));
});
}
}
ConfigurationPropertiesBeans
ConfigurationPropertiesBeans實(shí)現(xiàn)了BeanPostPorocessor接口配并,該接口我們也叫后置處理器,作用是在Bean對象在實(shí)例化和依賴注入完畢后高镐,在顯示調(diào)用初始化方法的前后添加我們自己的邏輯溉旋。注意是Bean實(shí)例化完畢后及依賴注入完成后觸發(fā)的。
我們可以在這個后置處理器的回調(diào)方法中嫉髓,掃描指定注解的bean观腊,收集這些屬性,用來觸發(fā)事件變更算行。
@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor {
private Map<String,List<FieldPair>> fieldMapper=new HashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class clz=bean.getClass();
if(clz.isAnnotationPresent(RefreshScope.class)){ //如果某個bean聲明了RefreshScope注解梧油,說明需要進(jìn)行動態(tài)更新
for(Field field:clz.getDeclaredFields()){
Value value=field.getAnnotation(Value.class);
List<String> keyList=getPropertyKey(value.value(),0);
for(String key:keyList){
//使用List<FieldPair>存儲的目的是,如果在多個bean中存在相同的key州邢,則全部進(jìn)行替換
fieldMapper.computeIfAbsent(key,(k)->new ArrayList()).add(new FieldPair(bean,field,value.value()));
}
}
}
return bean;
}
//獲取key信息儡陨,也就是${value}中解析出value這個屬性
private List<String> getPropertyKey(String value,int begin){
int start=value.indexOf("${",begin)+2;
if(start<2){
return new ArrayList<>();
}
int middle=value.indexOf(":",start);
int end=value.indexOf("}",start);
String key;
if(middle>0&&middle<end){
key=value.substring(start,middle);
}else{
key=value.substring(start,end);
}
//如果是這種用法,就需要遞歸量淌,@Value("${swagger2.host:127.0.0.1:${server.port:8080}}")
List<String> keys=getPropertyKey(value,end);
keys.add(key);
return keys;
}
public Map<String, List<FieldPair>> getFieldMapper() {
return fieldMapper;
}
}
RefreshScope
定義注解來實(shí)現(xiàn)指定需要動態(tài)刷新類的識別骗村。
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshScope {
}
FieldPair
這個類中主要通過PropertyPlaceholderHelper將字符串里的占位符內(nèi)容,用我們配置的properties里的替換类少。
public class FieldPair {
private static PropertyPlaceholderHelper propertyPlaceholderHelper=new PropertyPlaceholderHelper("${","}",":",true);
private Object bean;
private Field field;
private String value;
public FieldPair(Object bean, Field field, String value) {
this.bean = bean;
this.field = field;
this.value = value;
}
public void resetValue(Environment environment){
boolean access=field.isAccessible();
if(!access){
field.setAccessible(true);
}
//從新從environment中將占位符替換為新的值
String resetValue=propertyPlaceholderHelper.replacePlaceholders(value,((ConfigurableEnvironment) environment)::getProperty);
try {
//通過反射更新
field.set(bean,resetValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
訪問測試ConfigController
@RefreshScope
@RestController
public class ConfigController {
@Value("${name}")
private String name;
@Value("${job}")
private String job;
@GetMapping
public String get(){
return name+":"+job;
}
}
基于自定義PropertySourceLocator擴(kuò)展
由于在上述代碼中叙身,我們創(chuàng)建了一個PropertySourceLocator接口,并且在整個配置加載過程中硫狞,我們都是基于PropertySourceLocator擴(kuò)展點(diǎn)來進(jìn)行加載的信轿,所以也就是意味著除了上述使用的Zookeeper作為遠(yuǎn)程配置裝載以外,我們還可以通過擴(kuò)展PropertySourceLocator來實(shí)現(xiàn)其他的擴(kuò)展残吩,具體實(shí)現(xiàn)如下
CustomPropertySourceLocator
創(chuàng)建一個MapPropertySource作為Environment的屬性源财忽。
public class CustomPropertySourceLocator implements PropertySourceLocator{
@Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
Map<String, Object> source = new HashMap<>();
source.put("age","18");
MapPropertySource propertiesPropertySource = new MapPropertySource("configCenter",source);
return propertiesPropertySource;
}
}
spring.factories
由于CustomPropertySourceLocator是自定義擴(kuò)展點(diǎn),所以我們需要在spring.factories文件中定義它的擴(kuò)展實(shí)現(xiàn)泣侮,修改如下
com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator,\
com.gupaoedu.example.zookeepercuratordemo.config.CustomPropertySourceLocator
ConfigController
接下來即彪,我們通過下面的代碼進(jìn)行測試,從結(jié)果可以看到,我們自己定義的propertySource被加載到Environment中了隶校。
@RefreshScope
@RestController
public class ConfigController {
@Value("${name}")
private String name;
@Value("${job}")
private String job;
@Value("${age}")
private String age;
@GetMapping
public String get(){
return name+":"+job+":"+age;
}
}