背景
最近在使用apollo的时候,发现@Value注释的值,有部分能热更新,部分不能;对于ConfigurationProperties的配置类,完全不支持更新。所以想着了解一下apollo的热发布机制,看看是怎么实现的热发布。
官方介绍
从上面三个图可以看出:热更新,就是基于http长连接,当在apollo管理界面上修改配置时,会通知客户端有变更,然后客户端再去拉取最新的配置。
官方文档只是介绍了一下热更新机制,但是对于客户端拿到最新的配置后,怎么做到更新spring的bean的属性,却没提到。
SpringValueProcessor代码
com.ctrip.framework.apollo.spring.annotation目录下的类如下:
可以看到里面有一个SpringValueProcessor,从名字看,这个类的作用似乎是对spring中加了@Value注解的bean做处理的。
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);
private final ConfigUtil configUtil;
private final PlaceholderHelper placeholderHelper;
private final SpringValueRegistry springValueRegistry;
private BeanFactory beanFactory;
private Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions;
public SpringValueProcessor() {
configUtil = ApolloInjector.getInstance(ConfigUtil.class);
placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class);
springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class);
beanName2SpringValueDefinitions = LinkedListMultimap.create();
}
......
......
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
super.postProcessBeforeInitialization(bean, beanName);
processBeanPropertyValues(bean, beanName);
}
return bean;
}
@Override
protected 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);
logger.debug("Monitoring {}", springValue);
}
}
@Override
protected void processMethod(Object bean, String beanName, Method method) {
//register @Value on method
Value value = method.getAnnotation(Value.class);
if (value == null) {
return;
}
//skip Configuration bean methods
if (method.getAnnotation(Bean.class) != null) {
return;
}
if (method.getParameterTypes().length != 1) {
logger.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters",
bean.getClass().getName(), method.getName(), method.getParameterTypes().length);
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, method, false);
springValueRegistry.register(beanFactory, key, springValue);
logger.info("Monitoring {}", springValue);
}
}
......
......
}
SpringValueProcessor.postProcessBeforeInitialization
SpringValueProcessor重要的是postProcessBeforeInitialization方法:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 是否支持自动更新,不支持就啥事情都不做
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
// 调用父类的postProcessBeforeInitialization
super.postProcessBeforeInitialization(bean, beanName);
// PropertyValues的一些处理,不需要关心
processBeanPropertyValues(bean, beanName);
}
return bean;
}
SpringValueProcessor.postProcessBeforeInitialization方法调用的是父类的postProcessBeforeInitialization方法,父类的方法内容如下:
public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class clazz = bean.getClass();
for (Field field : findAllField(clazz)) {
processField(bean, beanName, field);
}
for (Method method : findAllMethod(clazz)) {
processMethod(bean, beanName, method);
}
return bean;
}
......
protected abstract void processField(Object bean, String beanName, Field field);
protected abstract void processMethod(Object bean, String beanName, Method method);
......
private List<Field> findAllField(Class clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
res.add(field);
}
});
return res;
}
private List<Method> findAllMethod(Class clazz) {
final List<Method> res = new LinkedList<>();
ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
res.add(method);
}
});
return res;
}
}
父类的postProcessBeforeInitialization方法做的事情就是拿到bean的所有属性,遍历调用processField;拿到所有方法,遍历调用procecssMethod。
而processField和procecssMethod两个方法是由子类实现的。
SpringValueProcessor.proccessField
SpringValueProcessor 类的proccessField干的事情如下:
@Override
protected void processField(Object bean, String beanName, Field field) {
// 获取属性上的@Value注解
Value value = field.getAnnotation(Value.class);
// 如果没有@Value注解,不需要处理
if (value == null) {
return;
}
// 根据@Value注解的value,生成apollo需要关注的配置字符串
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) {
return;
}
// 将每一个关注的配置字符串注册到springValueRegistry对象中
for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
这里面有个点需要关注下:
为什么placeholderHelper.extractPlaceholderKeys返回的是一个Set,而不是String?@Value不是只能指定一个value么?
@Value支持这样的用法:
@Value("${first.config:${second.config}}")
@Value的默认值机制可以支持变量,而不单单是字面值,当默认值是变量,
需要关注的配置就会是多个,Set刚好启动去重的作用,避免重复设置值。
SpringValueProcessor.proccessMethod
SpringValueProcessor.proccessMethod干的事情如下
@Override
protected void processMethod(Object bean, String beanName, Method method) {
// 获取方法上的@Value注解
Value value = method.getAnnotation(Value.class);
// 没有@Value注解的方法直接忽略
if (value == null) {
return;
}
// 如果方法有@Bean注解,则直接忽略
if (method.getAnnotation(Bean.class) != null) {
return;
}
// 期望加了@Value注解的方法是setter方法,setter方法应该只有一个参数
if (method.getParameterTypes().length != 1) {
logger.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters",
bean.getClass().getName(), method.getName(), method.getParameterTypes().length);
return;
}
// 与processField一样,根据@Value注解的value,得到需要关注的配置
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) {
return;
}
// 遍历,将关注的配置注册到springValueRegistry
for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
springValueRegistry.register(beanFactory, key, springValue);
logger.info("Monitoring {}", springValue);
}
}
代码逻辑基本与processField方法一致~
初步结论
apollo利用BeanFactoryPostProcessor的postProcessBeforeInitialization方法,对spring的bean进行遍历,把bean中加了@Value注解的field和method以SpringValue的形式注册到SpringValueRegistry中。
SpringValue包含了更新值需要的信息,例如@Value加在field上,那么Spring就包含了bean对象,加了注解的Field。@Value加在方法上,则会包含加了注解的Method。
到这,基本可以猜到热更的逻辑了:
1、配置发生变更
2、从SpringValueRegistry中找到需要更新的SpringValue对象
3、利用反射设置SpringValue持有的bean对象的Field或调用method方法
RemoteConfigRepository
RemoteConfigRepository的构造方法如下:
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
m_configCache = new AtomicReference<>();
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
// 长轮训监听配置变更的服务,通过注入框架获取实例,单例
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new AtomicReference<>();
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
// new RemoteConfigRepository对象的时候,就会尝试同步一次配置
this.trySync();
// 周期性的拉取配置进行更新
this.schedulePeriodicRefresh();
// 看方法名字就知道,定时的长轮询,监听配置的变更
this.scheduleLongPollingRefresh();
}
private void scheduleLongPollingRefresh() {
// 就是提交了制定namespace的长轮询任务
remoteConfigLongPollService.submit(m_namespace, this);
}
有一点值得留意:
RemoteConfigRepository的构造方法有一个参数,namespace。也就是说,每个namespace的配置更新是独立~
从官方文档,我们了解到,热发布机制是通过长轮询机制实现的,所以相应的代码应该是在RemoteConfigLongPollService中。
RemoteConfigLongPollService
比较重要的代码如下:
public class RemoteConfigLongPollService {
......
......
public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {
// 通过RemoteConfigRepository的构造函数知道,RemoteConfigLongPollService是单例的,RemoteConfigRepository是每个namespace独立的,所以通过map缓存需要监听的namespace和它对应的RemoteConfigRepository
boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);
// 设置初始的通知id
m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);
// 第一次提交任务的时候,启动轮询
if (!m_longPollStarted.get()) {
startLongPolling();
}
return added;
}
private void startLongPolling() {
// 并发控制
if (!m_longPollStarted.compareAndSet(false, true)) {
//already started
return;
}
try {
// 获取各种配置
final String appId = m_configUtil.getAppId();
final String cluster = m_configUtil.getCluster();
final String dataCenter = m_configUtil.getDataCenter();
final String secret = m_configUtil.getAccessKeySecret();
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);
// 每隔longPollingInitialDelayInMills毫秒请求一次
TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
} catch (InterruptedException e) {
//ignore
}
}
// 真正的长轮询是在该方法
doLongPollingRefresh(appId, cluster, dataCenter, secret);
}
});
} catch (Throwable ex) {
m_longPollStarted.set(false);
ApolloConfigException exception =
new ApolloConfigException("Schedule long polling refresh failed", ex);
Tracer.logError(exception);
logger.warn(ExceptionUtil.getDetailMessage(exception));
}
}
private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {
final Random random = new Random();
ServiceDTO lastServiceDto = null;
while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
//wait at most 5 seconds
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");
String url = null;
try {
if (lastServiceDto == null) {
// 拿到全部服务地址
List<ServiceDTO> configServices = getConfigServices();
// 通过random.nextInt起到负载均衡的作用
lastServiceDto = configServices.get(random.nextInt(configServices.size()));
}
// 构建请求地址
url =
assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,
m_notifications);
logger.debug("Long polling from {}", url);
HttpRequest request = new HttpRequest(url);
request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
if (!StringUtils.isBlank(secret)) {
Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
request.setHeaders(headers);
}
transaction.addData("Url", url);
// 发送轮询请求
final HttpResponse<List<ApolloConfigNotification>> response =
m_httpUtil.doGet(request, m_responseType);
logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);
// http状态码是200并且body不空,代表有更新
if (response.getStatusCode() == 200 && response.getBody() != null) {
// 更新m_notifications里的通知id
updateNotifications(response.getBody());
// 更新已经存在于m_remoteNotificationMessages缓存中的内容,更新方式为合并,以版本大的为准
updateRemoteNotifications(response.getBody());
transaction.addData("Result", response.getBody().toString());
// 真正的更新动作,是这个方法
notify(lastServiceDto, response.getBody());
}
//状态码是304,随机是否将lastServiceDto设置为null,lastServiceDto设置为null,则会触发新一轮的负载均衡,随机选择新的服务地址
if (response.getStatusCode() == 304 && random.nextBoolean()) {
lastServiceDto = null;
}
m_longPollFailSchedulePolicyInSecond.success();
transaction.addData("StatusCode", response.getStatusCode());
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
lastServiceDto = null;
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
transaction.setStatus(ex);
long sleepTimeInSecond = m_longPollFailSchedulePolicyInSecond.fail();
logger.warn(
"Long polling failed, will retry in {} seconds. appId: {}, cluster: {}, namespaces: {}, long polling url: {}, reason: {}",
sleepTimeInSecond, appId, cluster, assembleNamespaces(), url, ExceptionUtil.getDetailMessage(ex));
try {
TimeUnit.SECONDS.sleep(sleepTimeInSecond);
} catch (InterruptedException ie) {
//ignore
}
} finally {
transaction.complete();
}
}
}
private void notify(ServiceDTO lastServiceDto, List<ApolloConfigNotification> notifications) {
if (notifications == null || notifications.isEmpty()) {
return;
}
for (ApolloConfigNotification notification : notifications) {
String namespaceName = notification.getNamespaceName();
//create a new list to avoid ConcurrentModificationException
// 从m_longPollNamespaces拿到namespace对应的RemoteConfigRepository,并包装成一个List。官方备注是避免ConcurrentModificationException异常,但是没太懂,怎么用,会抛这个异常
List<RemoteConfigRepository> toBeNotified =
Lists.newArrayList(m_longPollNamespaces.get(namespaceName));
// 在doLongPollingRefresh方法中调用updateRemoteNotifications方法,把ApolloNotificatiobMessages设置到了m_remoteNotificationMessages中,现在取出来
ApolloNotificationMessages originalMessages = m_remoteNotificationMessages.get(namespaceName);
ApolloNotificationMessages remoteMessages = originalMessages == null ? null : originalMessages.clone();
//since .properties are filtered out by default, so we need to check if there is any listener for it
// updateRemoteNotifications和这里都是这样,对namespace对额外的检查,例如namespace是zidongxiangxi,那么还会检查zidongxiangxi.properties。这机制,可能得再了解下才行
toBeNotified.addAll(m_longPollNamespaces
.get(String.format("%s.%s", namespaceName, ConfigFileFormat.Properties.getValue())));
for (RemoteConfigRepository remoteConfigRepository : toBeNotified) {
try {
// 最终是调用RemoteConfigRepository的onLongPollNotified方法来执行更新
remoteConfigRepository.onLongPollNotified(lastServiceDto, remoteMessages);
} catch (Throwable ex) {
Tracer.logError(ex);
}
}
}
}
......
......
}
RemoteConfigLongPollService只是负责监听配置是否更新,配置的更新最终还是由RemoteConfigRepository完成。
RemoteConfigRepository.onLongPollNotified方法代码很简单,如下:
public void onLongPollNotified(ServiceDTO longPollNotifiedServiceDto, ApolloNotificationMessages remoteMessages) {
m_longPollServiceDto.set(longPollNotifiedServiceDto);
m_remoteMessages.set(remoteMessages);
m_executorService.submit(new Runnable() {
@Override
public void run() {
m_configNeedForceRefresh.set(true);
trySync();
}
});
}
其实就是设置了强制刷新标识,然后执行一次尝试配置同步。trySync方法底层调用的是sync方法,只是做了一些异常处理。
RemoteConfigRepository.sync代码如下:
@Override
protected synchronized void sync() {
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
try {
ApolloConfig previous = m_configCache.get();
ApolloConfig current = loadApolloConfig();
// 上一次版本号和当前版本号不相等才需要更新
if (previous != current) {
logger.debug("Remote Config refreshed!");
// 更新版本号
m_configCache.set(current);
// 触发更新
this.fireRepositoryChange(m_namespace, this.getConfig());
}
if (current != null) {
Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
current.getReleaseKey());
}
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
fireRepositoryChange是RemoteConfigRepository的父类AbstractConfigRepository的方法:
protected void fireRepositoryChange(String namespace, Properties newProperties) {
for (RepositoryChangeListener listener : m_listeners) {
try {
listener.onRepositoryChange(namespace, newProperties);
} catch (Throwable ex) {
Tracer.logError(ex);
logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex);
}
}
}
配置更新,就是遍历所有RepositoryChangeListener,调用这些RepositoryChangeListener的onRepositoryChange方法。
DefaultConfig
DefaultConfig实现了RepositoryChangeListener接口,在initialize方法中,把把自己注册到ConfigRepository的时间监听里。
public class DefaultConfig extends AbstractConfig implements RepositoryChangeListener {
......
public DefaultConfig(String namespace, ConfigRepository configRepository) {
m_namespace = namespace;
m_resourceProperties = loadFromResource(m_namespace);
m_configRepository = configRepository;
m_configProperties = new AtomicReference<>();
m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute
initialize();
}
private void initialize() {
try {
updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
m_namespace, ExceptionUtil.getDetailMessage(ex));
} finally {
//就是这里了!!!把自己注册到ConfigRepository的事件监听里
m_configRepository.addChangeListener(this);
}
}
// 实现RepositoryChangeListener接口的onRepositoryChange方法
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
// 新老配置相同,不处理
if (newProperties.equals(m_configProperties.get())) {
return;
}
ConfigSourceType sourceType = m_configRepository.getSourceType();
Properties newConfigProperties = propertiesFactory.getPropertiesInstance();
newConfigProperties.putAll(newProperties);
// 计算新老配置中的不同
Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties, sourceType);
//没有不同,不处理
if (actualChanges.isEmpty()) {
return;
}
// fireConfigChange触发配置更新
this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));
Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
// 最终是调用ConfigChangeListener来更新配置。。。。
for (final ConfigChangeListener listener : m_listeners) {
// 不感兴趣的事件不处理
if (!isConfigChangeListenerInterested(listener, changeEvent)) {
continue;
}
m_executorService.submit(new Runnable() {
@Override
public void run() {
String listenerName = listener.getClass().getName();
Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);
try {
listener.onChange(changeEvent);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
Tracer.logError(ex);
logger.error("Failed to invoke config change listener {}", listenerName, ex);
} finally {
transaction.complete();
}
}
});
}
}
......
}
嵌套的有点深,DefaultConfig自己并不直接执行配置的更新,而是触发注册到它们这的ConfigChangeListener监听。。。。
AutoUpdateConfigChangeListener
在apollo中,自动更新是AutoUpdateConfigChangeListener来实现的。
public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
......
@Override
public void onChange(ConfigChangeEvent changeEvent) {
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
for (String key : keys) {
// 根据跟新配置的key,查找对应的SpringValue集合
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
// 遍历更新配置
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);
}
}
......
}
到这里,可以确定,跟一开始得出的初步结论一致~
SpringValue
public class SpringValue {
......
public void update(Object newVal) throws IllegalAccessException, InvocationTargetException {
if (isField()) {
injectField(newVal);
} else {
injectMethod(newVal);
}
}
private void injectField(Object newVal) throws IllegalAccessException {
Object bean = beanRef.get();
if (bean == null) {
return;
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(bean, newVal);
field.setAccessible(accessible);
}
private void injectMethod(Object newVal)
throws InvocationTargetException, IllegalAccessException {
Object bean = beanRef.get();
if (bean == null) {
return;
}
methodParameter.getMethod().invoke(bean, newVal);
}
......
}
SpringValue方法的代码也很简单:
- 是加在@Value属性的SpringValue,就利用反射调用Field设置值
- 是加在setter方法上的SpringValue,就用反射调用对应的方法