案例
案例一:本地propreties配置文件配置配置项
定义了一个 HelloController
,这里面通过 @Value
注解注入了一个 hello.msg
配置的值。在 ApolloStudyApplication
启动类上通过注解 @EnableApolloConfig
开启 Apollo 配置。代码如下:
java
@SpringBootApplication
@EnableApolloConfig
public class ApolloStudyApplication {
public static void main(String[] args) {
SpringApplication.run(ApolloStudyApplication.class, args);
}
}
@RestController
public class HelloController {
@Value("${hello.msg}")
private String msg;
@GetMapping("/hello")
public String hello() {
return msg;
}
}
在 application.properties
中的配置如下,在 Apollo 配置中心未配置该配置项:
shell
server.port=9091
spring.application.name=apollostudy
app.id=SampleApp
apollo.config-service=http://192.168.33.123:8080
hello.msg=hello-local
apollo.bootstrap.eagerLoad.enabled=true
apollo.bootstrap.enabled=true
然后访问 Controller,可以看到是可以获取到配置文件中配置的值的。结果如下:
案例二:Apollo和本地propreties配置文件都配置配置项
首先在 Apollo 配置中心将 hello.msg
的先配置为「你好」,然后访问 Controller,结果如下:
然后在 Apollo 配置中将 hello.msg
的先配置为「hello」,然后访问 Controller,结果如下:
从上面的两个案例可以看到当本地配置文件和 Apollo 配置中心都有相同的配置的时候,Apollo 配置中心的值会覆盖本地配置文件的值;当 Apollo 配置中心的值变化时,通过 @Value
注解注入该值的地方可以感知到变化并及时更新。
那 Apollo 配置中心的值是如何做到覆盖本地配置的值并能够及时更新的呢?接下来将从源码的角度分析一下原理。先说下结论:
Apollo 配置客户端在启动的时候会构造一个 PropertySource
,并将这个对象放到 Environment
对象的最前面 ,而 Spring 查找属性的时候是按照顺序先后查找的,如果在前面的 PropertySource
中查找到了就直接返回了,Apollo 正是通过放在最前面这个操作来实现覆盖的。
Apollo 配置客户端在启动的时候会获取所有有 @Value
注解修饰的字段并缓存起来,然后通过长轮询获取服务端的配置变更,然后通过反射的方式更新字段的值,从而实现感知变化并及时更新的。
源码分析
Spring 中提供了 EnvironmentPostProcessor
接口,它有一个 postProcessEnvironment()
方法,它的核心作用是允许你在应用程序启动过程中,在 ApplicationContext 被刷新之前修改 Environment 对象 。前面的文章搞懂这两个组件,Spring 配置问题少一半!中介绍过 xxx.properties 配置文件的内容最终会被加载并放置到 Environment
中保存的,而 Apollo 就是利用 postProcessEnvironment()
可以修改 Environment
对象来实现配置的覆盖。代码如下:
java
@FunctionalInterface
public interface EnvironmentPostProcessor {
void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}
Apollo 提供了一个 ApolloApplicationContextInitializer
类实现了 EnvironmentPostProcessor
接口,在它的 postProcessEnvironment()
方法中会判断 apollo.bootstrap.eagerLoad.enabled
和 apollo.bootstrap.enabled
配置项是否为 true,如果为 true 才会继续往下进行初始化。实际的初始化在 initialize()
方法中实现的。 在该方法中将每个 namespace 的配置包装为一个 Config
对象,然后再基于它创建一个 ProperySource
对象,然后添加到 CompositePropertySource
对象中,最后把 CompositePropertySource
对象添加到 Environment
对象的最前面,这个就是实现 Apollo 配置覆盖本地配置文件配置的关键。代码如下:
java
public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {
// 省略代码
// 查看apollo.bootstrap.eagerLoad.enabled配置项是否配置为true,为true才会继续往下走
Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, String.valueOf(eagerLoadEnabled));
if (!eagerLoadEnabled) {
return;
}
// 查看apollo.bootstrap.enabled配置项是否配置为true,为true才会继续往下走
Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, String.valueOf(bootstrapEnabled));
if (bootstrapEnabled) {
DeferredLogger.enable();
// 调用initialize()方法
initialize(configurableEnvironment);
}
}
protected void initialize(ConfigurableEnvironment environment) {
final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized, replay the logs that were printed before the logging system was initialized
DeferredLogger.replayTo();
if (configUtil.isOverrideSystemProperties()) {
// ensure ApolloBootstrapPropertySources is still the first
PropertySourcesUtil.ensureBootstrapPropertyPrecedence(environment);
}
return;
}
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, namespaces);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
// 这里创建一个 CompositePropertySource 对象
CompositePropertySource composite;
if (configUtil.isPropertyNamesCacheEnabled()) {
composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
} else {
composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
for (String namespace : namespaceList) {
// 这里将每一个namespace的配置包装为一个Config对象
Config config = ConfigService.getConfig(namespace);
// 然后基于Config对象构造一个ProperySource对象添加到CompositePropertySource中
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
if (!configUtil.isOverrideSystemProperties()) {
if (environment.getPropertySources().contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, composite);
return;
}
}
// 这行代码很关键,这里将CompositePropertySource添加到了environment的最前面
environment.getPropertySources().addFirst(composite);
}
Apollo 还提供了一个 ApolloProcessor
实现了 BeanPostProcessor
接口,在它的 postProcessBeforeInitialization()
方法中,会查找所有有 @Value
注解修饰的字段,然后提取它里面配置项的名称,然后构造 SpringValue
对象然后注册到 SpringValueRegistry
中,在这里相当于缓存了所有有 @Value
注解修饰的字段。代码如下:
java
public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered {
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class<?> clazz = bean.getClass();
for (Field field : findAllField(clazz)) {
processField(bean, beanName, field);
}
// 省略代码
return bean;
}
}
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
protected void processField(Object bean, String beanName, Field field) {
// register @Value on field
Value value = field.getAnnotation(Value.class);
if (value == null) {
return;
}
doRegister(bean, beanName, field, value);
}
private void doRegister(Object bean, String beanName, Member member, Value value) {
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) {
return;
}
for (String key : keys) {
SpringValue springValue;
if (member instanceof Field) {
Field field = (Field) member;
springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
} else if (member instanceof Method) {
Method method = (Method) member;
springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
} else {
logger.error("Apollo @Value annotation currently only support to be used on methods and fields, "
+ "but is used on {}", member.getClass());
return;
}
springValueRegistry.register(beanFactory, key, springValue);
logger.info("Monitoring {}", springValue);
}
}
}
Apollo 提供了一个 AutoUpdateConfigChangeListener
监听器,在它的 onChange()
方法中根据发生变化的配置项作为 key 从之前的缓存获取对应的 SpringValue
列表,即被 @Value
注解修饰的字段,然后通过反射的方式更新其值。代码如下:
java
public class AutoUpdateConfigChangeListener implements ConfigChangeListener,
ApplicationListener<ApolloConfigChangeEvent>, ApplicationContextAware {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
// 获取发生变化的配置项
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
for (String key : keys) {
// 获取配置项关联的@Value注解修饰的字段
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
// 2. update the value
for (SpringValue val : targetValues) {
updateSpringValue(val);
}
}
}
private void updateSpringValue(SpringValue springValue) {
try {
Object value = resolvePropertyValue(springValue);
// 更新字段的值
springValue.update(value);
logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
springValue);
} catch (Throwable ex) {
logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
}
}
}
而获取变更则是通过长轮询来实现的。在 RemoteConfigLongPollService
中有一个 startLongPolling()
方法,它会提交一个轮询任务,这个轮询任务会周期性地拉取最新的变更信息。代码如下:
java
private void startLongPolling(String sysAppId) {
if (Boolean.TRUE.equals(m_longPollStarted.putIfAbsent(sysAppId, true))) {
//already started
return;
}
try {
final String appId = sysAppId;
final String cluster = m_configUtil.getCluster();
final String dataCenter = m_configUtil.getDataCenter();
final String secret = m_configUtil.getAccessKeySecret(appId);
final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
m_longPollingService.submit(new Runnable() {
@Override
public void run() {
if (longPollingInitialDelayInMills > 0) {
try {
logger.debug("Long polling will start in {} ms.", longPollingInitialDelayInMills);
TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
} catch (InterruptedException e) {
//ignore
}
}
// 轮询一次
doLongPollingRefresh(appId, cluster, dataCenter, secret);
}
});
} catch (Throwable ex) {
// 省略代码
}
}