本節主要對 Apollo 客戶端設計原理進行解析。
1. 設計原理
圖 1 簡要描述了 Apollo 客戶端的實現原理。
- 客戶端和服務端保持了一個長連接,編譯配置的實時更新推送。
- 定時拉取配置是客戶端本地的一個定時任務,默認爲每 5 分鐘拉取一次,也可以通過在運行時指定 System Property:apollo.refreshInterval 來覆蓋,單位是分鐘,推送+定時拉取=雙保險。
- 客戶端從 Apollo 配置中心服務端獲取到應用的最新配置後,會保存在內存中。
- 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份,當服務或者網絡不可用時,可以使用本地的配置,也就是我們的本地開發模式 env=Local。
2. 和 Spring 集成的原理
Apollo 除了支持 API 方式獲取配置,也支持和 Spring/Spring Boot 集成,集成後可以直接通過 @Value 獲取配置,我們來分析下集成的原理。
Spring 從 3.1 版本開始增加了 ConfigurableEnvironment 和 PropertySource:
- ConfigurableEnvironment 實現了 Environment 接口,並且包含了多個 Property-Source。
- PropertySource 可以理解爲很多個 Key-Value 的屬性配置,在運行時的結構形如圖 2 所示。
需要注意的是,PropertySource 之間是有優先級順序的,如果有一個 Key 在多個 property source 中都存在,那麼位於前面的 property source 優先。
集成的原理就是在應用啓動階段,Apollo 從遠端獲取配置,然後組裝成 PropertySource 並插入到第一個即可,如圖 3 所示。
3. 啓動時初始化配置到 Spring
客戶端集成 Spring 的代碼分析,我們也採取簡化的方式進行講解。
首先我們來分析,在項目啓動的時候從 Apollo 拉取配置,是怎麼集成到 Spring 中的。創建一個 PropertySourcesProcessor 類,用於初始化配置到 Spring PropertySource 中。具體代碼如下所示。
@Component
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
String APOLLO_PROPERTY_SOURCE_NAME = "ApolloPropertySources";
private ConfigurableEnvironment environment;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 啓動時初始化配置到Spring PropertySource
Config config = new Config();
ConfigPropertySource configPropertySource = new ConfigPropertySource("ap-plication", config);
CompositePropertySource composite = new CompositePropertySource(APOLLO_PROPERTY_SOURCE_NAME);
composite.addPropertySource(configPropertySource);
environment.getPropertySources().addFirst(composite);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
}
實現 EnvironmentAware 接口是爲了獲取 Environment 對象。實現 BeanFactory-Post-Processor 接口,我們可以在容器實例化 bean 之前讀取 bean 的信息並修改它。
Config 在 Apollo 中是一個接口,定義了很多讀取配置的方法,比如 getProperty:getIntProperty 等。通過子類去實現這些方法,在這裏我們就簡化下,直接定義成一個類,提供兩個必要的方法,具體代碼如下所示。
public class Config {
public String getProperty(String key, String defaultValue) {
if (key.equals("bianchengName")) {
return "C語言中文網";
}
return null;
}
public Set<String> getPropertyNames() {
Set<String> names = new HashSet<>();
names.add("bianchengName");
return names;
}
}
Config 就是配置類,配置拉取之後會存儲在類中,所有配置的讀取都必須經過它,我們在這裏就平格定義需要讀取的 key 爲 bianchengName。
然後需要將 Config 封裝成 PropertySource 才能插入到 Spring Environment 中。
定義一個 ConfigPropertySource 用於將 Config 封裝成 PropertySource,ConfigProperty-Source 繼承了 EnumerablePropertySource,EnumerablePropertySource 繼承了 PropertySource。具體代碼如下所示。
public class ConfigPropertySource extends EnumerablePropertySource<Config> {
private static final String[] EMPTY_ARRAY = new String[0];
ConfigPropertySource(String name, Config source) {
super(name, source);
}
@Override
public String[] getPropertyNames() {
Set<String> propertyNames = this.source.getPropertyNames();
if (propertyNames.isEmpty()) {
return EMPTY_ARRAY;
}
return propertyNames.toArray(new String[propertyNames.size()]);
}
@Override
public Object getProperty(String name) {
return this.source.getProperty(name, null);
}
}
需要做的操作還是重寫 getPropertyNames 和 getProperty 這兩個方法。當調用這兩個方法時,返回的就是 Config 中的內容。
最後將 ConfigPropertySource 添加到 CompositePropertySource 中,並且加入到 Confi-gu-rable-Environment 即可。
定義一個接口用來測試有沒有效果,具體代碼如下所示。
@RestController
public class ConfigController {
@Value("${bianchengName:zhangsan}")
private String name;
@GetMapping("/get")
private String bianchengUrl;
@GetMapping("/get")
public String get() {
return name + bianchengUrl;
}
}
在配置文件中增加對應的配置:
bianchengName=xxx
bianchengUrl=http://minglisoft.cn/cloud
在沒有增加上面講的代碼之前,訪問 /get 接口返回的是 xxxhttp://c.biancheng.net。加上上面講解的代碼之後,返回的內容就變成了猿天地 http://c.biancheng.net。
這是因爲我們在 Config 中對應 bianchengName 這個 key 的返回值是猿天地,也間接證明了在啓動的時候可以通過這種方式來覆蓋本地的值。這就是 Apollo 與 Spring 集成的原理。
4. 運行中修改配置如何刷新
在這一節中,我們來講解下在項目運行過程中,配置發生修改之後推送給了客戶端,那麼這個值如何去更新 Spring 當中的值呢?
原理就是把這些配置都存儲起來,當配置發生變化的時候進行修改就可以。Apollo 中定義了一個 SpringValueProcessor 類,用來處理 Spring 中值的修改。下面只貼出一部分代碼,如下所示。
@Component
public class SpringValueProcessor implements BeanPostProcessor, BeanFactoryAware {
private PlaceholderHelper placeholderHelper = new PlaceholderHelper();
private BeanFactory beanFactory;
public SpringValueRegistry springValueRegistry = new SpringValueRegistry();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class clazz = bean.getClass();
for (Field field : findAllField(clazz)) {
processField(bean, beanName, field);
}
return bean;
}
private void processField(Object bean, String beanName, Field field) {
// register @Value on field
Value value = field.getAnnotation(Value.class);
if (value == null) {
return;
}
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) {
return;
}
for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
springValueRegistry.register(beanFactory, key, springValue);
}
}
}
通過實現 BeanPostProcessor 來處理每個 bean 中的值,然後將這個配置信息封裝成一個 SpringValue 存儲到 springValueRegistry 中。
SpringValue 代碼如下所示。
public class SpringValue {
private MethodParameter methodParameter;
private Field field;
private Object bean;
private String beanName;
private String key;
private String placeholder;
private Class<?> targetType;
private Type genericType;
private boolean isJson;
}
SpringValueRegistry 就是利用 Map 來存儲,代碼如下所示。
public class SpringValueRegistry {
private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
private final Object LOCK = new Object();
public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
if (!registry.containsKey(beanFactory)) {
synchronized (LOCK) {
if (!registry.containsKey(beanFactory)) {
registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
}
}
}
registry.get(beanFactory).put(key, springValue);
}
public Collection<SpringValue> get(BeanFactory beanFactory, String key) {
Multimap<String, SpringValue> beanFactorySpringValues = registry.get(beanFactory);
if (beanFactorySpringValues == null) {
return null;
}
return beanFactorySpringValues.get(key);
}
}
寫個接口用於模擬配置修改,具體代碼如下所示。
@RestController
public class ConfigController {
@Autowired
private SpringValueProcessor springValueProcessor;
@Autowired
private ConfigurableBeanFactory beanFactory;
@GetMapping("/update")
public String update(String value) {
Collection<SpringValue> targetValues = springValueProcessor.springValueRegistry.get(beanFactory,
"bianchengName");
for (SpringValue val : targetValues) {
try {
val.update(value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
return name;
}
}
當我們調用 /update接口後,在前面的 /get 接口可以看到猿天地的值改成了你傳入的那個值,這就是動態修改。
5. 原理分析總結
至此關於 Apollo 核心原理的分析就結束了,通過閱讀 Apollo 的源碼,可以學到很多的東西。
- Apollo 用的是 Mysql。Apollo 是多個庫,通過庫來區分不同環境下的配置。
- Apollo 基於 Http 長連接實現推送,還有容災的定時拉取邏輯。
- Apollo 也有自己的原生獲取值的對象,同時還集成到了 Spring 中,可以兼容老項目的使用方式。
- Apollo 中可以直接使用註解來進行監聽,非常方便。