前言
鉴于目前大多数项目大都部署在微服务环境下,而不少小伙伴日常维护的项目里都或多或少的用到了自动配置,有的公司可能是zookeeper,有的公司可能用consul,nacos或者apollo等等。这些开源组件都是很好用的能帮助我们很方便的实现和管理或者运维我们的项目配置。那大家有没有想过,我们在修改了一项配置后,这些开源组件是怎么样通知到我们的服务节点的呢?这篇文章,我将记录一下翻阅nacos源码的过程,希望对大家有所帮助。
注:
这次翻阅的是spring-cloud-alibaba-nacos-config 2.2.7版本的源码,之前也看过0.9.0的源码,还是有不少区别的。源码过程是按照springboot和springcloud加载的顺序去看的,大家不知道怎么阅读源码的,也可以借鉴一下,水平有限,错误的地方烦请指出。
源码
1.jar
maven工程pom引入依赖,然后找到源码jar包,在META-INF下找到spring.factories
`
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
org.springframework.context.ApplicationListener=\
com.alibaba.cloud.nacos.logging.NacosLoggingListener
`
springboot就是按照这个顺序去加载jar包下的东西,需要注意的是,cloud的bootstrap的优先级是高于boot的autoconfigure的(从顺序上也可以看出来,bootstrap在最上面)
2.NacosConfigBootstrapConfiguration
// 加proxyBeanMethods=false代表@bean表示的方法的参数每次不再从spring容器里获取,而是直接new一个,并且new的这个对象不经过spring的生命周期,也就是不走cglib代理。
@Configuration(proxyBeanMethods = false)
// 这个是自动配置的开关。默认是开启
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
// 第一步先注册一个NacosConfigProperties
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
// 第二步注册一个NacosConfigManager
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
// 第二步注册一个NacosPropertySourceLocator
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
2.1.NacosConfigProperties
没有构造方法,默认走的空参构造。
// 就是把配置类的一些属性读进来
@ConfigurationProperties(NacosConfigProperties.PREFIX)
public class NacosConfigProperties {
/**
* Prefix of {@link NacosConfigProperties}.
*/
public static final String PREFIX = "spring.cloud.nacos.config";
2.2.NacosConfigManager
public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
// Compatible with older code in NacosConfigProperties,It will be deleted in the
// future.
createConfigService(nacosConfigProperties);
}
static ConfigService createConfigService(
NacosConfigProperties nacosConfigProperties) {
// 双重检查
if (Objects.isNull(service)) {
synchronized (NacosConfigManager.class) {
try {
if (Objects.isNull(service)) {
service = NacosFactory.createConfigService(
nacosConfigProperties.assembleConfigServiceProperties());
}
}
catch (NacosException e) {
log.error(e.getMessage());
throw new NacosConnectionFailureException(
nacosConfigProperties.getServerAddr(), e.getMessage(), e);
}
}
}
return service;
}
public static ConfigService createConfigService(Properties properties) throws NacosException {
return ConfigFactory.createConfigService(properties);
}
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(-400, e.getMessage());
}
}
捋一下这里的逻辑
1.利用双重检查,new一个ConfigService的单例
2.传入的是2.1中new的配置类,配置类已经读取了配置信息
3.利用反射,new了一个NacosConfigService实例。
2.2.1.NacosConfigService
public NacosConfigService(Properties properties) throws NacosException {
// 设置一些编码格式
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
initNamespace(properties);
//发送请求的http工具类
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
worker = new ClientWorker(agent, configFilterChainManager);
}
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
executorService = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling" + agent.getName());
t.setDaemon(true);
return t;
}
});
executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
public void checkConfigInfo() {
// 分任务
int listenerSize = cacheMap.get().size();
// 向上取整为批数
int longingTaskCount = (int)Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int)currentLongingTaskCount; i < longingTaskCount; i++) {
// 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
class LongPollingRunnable implements Runnable {
private int taskId;
public LongPollingRunnable(int taskId) {
this.taskId = taskId;
}
public void run() {
try {
List cacheDatas = new ArrayList();
// check failover config
// 先检查本地
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
List inInitializingCacheList = new ArrayList();
// check server config
// 从服务器获取变动
List changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// 轮询变动的地方,然后拉取变动后的配置
for (String groupKey : changedGroupKeys) {
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
try {
String content = getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(content);
LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, cache.getMd5(),
ContentUtils.truncateContent(content));
} catch (NacosException ioe) {
String message = String.format(
"[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
agent.getName(), dataId, group, tenant);
LOGGER.error(message, ioe);
}
}
// 检查是否变动
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
} catch (Throwable e) {
LOGGER.error("longPolling error", e);
} finally {
// 继续调用自己,轮询。
executorService.execute(this);
}
}
}
void checkListenerMd5() {
// 轮询所有数据,将文件的md5和封装listener的wrap中旧的md5去比较
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
private void safeNotifyListener(final String dataId, final String group, final String content,
final String md5, final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener;
Runnable job = new Runnable() {
public void run() {
ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader appClassLoader = listener.getClass().getClassLoader();
try {
if (listener instanceof AbstractSharedListener) {
AbstractSharedListener adapter = (AbstractSharedListener)listener;
adapter.fillContext(dataId, group);
LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
}
// 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
Thread.currentThread().setContextClassLoader(appClassLoader);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
// 这里回调我们注册的listener
listener.receiveConfigInfo(contentTmp);
listenerWrap.lastCallMd5 = md5;
LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
listener);
} catch (NacosException de) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
dataId, group, md5, listener, de.getErrCode(), de.getErrMsg());
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId, group,
md5, listener, t.getCause());
} finally {
Thread.currentThread().setContextClassLoader(myClassLoader);
}
}
};
final long startNotify = System.currentTimeMillis();
try {
if (null != listener.getExecutor()) {
listener.getExecutor().execute(job);
} else {
job.run();
}
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId, group,
md5, listener, t.getCause());
}
final long finishNotify = System.currentTimeMillis();
LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
name, (finishNotify - startNotify), dataId, group, md5, listener);
}
捋一下new NacosConfigService的逻辑
1.new了一个newCachedThreadPool线程池
2.new了一个newScheduledThreadPool,定时的去执行checkConfigInfo()方法
3.checkConfigInfo方法中,是给线程做调度,给数据分批,默认是3000个一批,如果批数增加一个,就增加一个LongPollingRunnable线程
4.LongPollingRunnable这个线程去监听配置变更,如果发生变更,调用checkListenerMd5()方法;
5.checkListenerMd5()中会根据每部分数据的md5去匹配具体的封装listener的wrap中的md5,然后调用safeNotifyListener()方法
6.safeNotifyListener中去回调这个listener的receiveConfigInfo()方法。
注意,LongPollingRunnable的finally方法中调用自己,轮询执行
2.3.NacosPropertySourceLocator
public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
}
通过构造方法new了一个NacosPropertySourceLocator,然后把2.1的nacosConfigProperties和2.2的nacosConfigManager都指向到自己类成员变量上。
3.NacosConfigAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {
// 这个之前已经加载过了,从老的上下文中直接拿过来
@Bean
public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
if (context.getParent() != null
&& BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
context.getParent(), NacosConfigProperties.class).length > 0) {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
NacosConfigProperties.class);
}
return new NacosConfigProperties();
}
// 这个就是自动刷新的开关,关闭修改配置不会自动刷新
@Bean
public NacosRefreshProperties nacosRefreshProperties() {
return new NacosRefreshProperties();
}
// 这个是历史数据
@Bean
public NacosRefreshHistory nacosRefreshHistory() {
return new NacosRefreshHistory();
}
// 这个会重新加载一下,但是因为存在双重检查,不会新建NacosConfigService
@Bean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
@Bean
public NacosContextRefresher nacosContextRefresher(
NacosConfigManager nacosConfigManager,
NacosRefreshHistory nacosRefreshHistory) {
// Consider that it is not necessary to be compatible with the previous
// configuration
// and use the new configuration if necessary.
return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
}
3.1.NacosContextRefresher
public NacosContextRefresher(NacosConfigManager nacosConfigManager,
NacosRefreshHistory refreshHistory) {
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
this.nacosRefreshHistory = refreshHistory;
this.configService = nacosConfigManager.getConfigService();
this.isRefreshEnabled = this.nacosConfigProperties.isRefreshEnabled();
}
很平常的一个构造方法,但是要注意一点,这个类是个listener:
public class NacosContextRefresher
implements ApplicationListener, ApplicationContextAware
监听了ApplicationReadyEvent事件,这个事件在springboot的refresh()方法中的最后,可以理解为springboot都启动好了之后,推送这个事件。看一下监听到事件的行为:
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
利用cas,保证registerNacosListenersForApplications()方法只执行一次。
private void registerNacosListenersForApplications() {
// 判断是否允许自动刷新的总开关
if (isRefreshEnabled()) {
// 遍历所有的数据,需要刷新的数据页就注册lintener
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// todo feature: support single refresh for listening
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
这里相当于注册了一个listener到listenerMap,同时把这listener加到worker的缓存里,这里为啥用AtomicReference我没搞懂。
private Map listenerMap = new ConcurrentHashMap<>(16);
AtomicReference
在这个listener里,推送了一个真正的刷新事件RefreshEvent
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// todo feature: support single refresh for listening
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
4.NacosConfigEndpointAutoConfiguration
这个配置主要注册一个actuator端点和一个健康检查
@ConditionalOnWebApplication
@ConditionalOnClass(Endpoint.class)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigEndpointAutoConfiguration {
@Autowired
private NacosConfigManager nacosConfigManager;
@Autowired
private NacosRefreshHistory nacosRefreshHistory;
// actuator
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
@Bean
public NacosConfigEndpoint nacosConfigEndpoint() {
return new NacosConfigEndpoint(nacosConfigManager.getNacosConfigProperties(),
nacosRefreshHistory);
}
// 健康检查
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledHealthIndicator("nacos-config")
public NacosConfigHealthIndicator nacosConfigHealthIndicator() {
return new NacosConfigHealthIndicator(nacosConfigManager.getConfigService());
}
}
5.NacosConnectionFailureAnalyzer
处理自定义异常NacosConnectionFailureException
public class NacosConnectionFailureAnalyzer
extends AbstractFailureAnalyzer {
@Override
protected FailureAnalysis analyze(Throwable rootFailure,
NacosConnectionFailureException cause) {
return new FailureAnalysis(
"Application failed to connect to Nacos server: \""
+ cause.getServerAddr() + "\"",
"Please check your Nacos server config", cause);
}
}
总结
按照springboot的启动顺序来说
1,注入NacosConfigProperties,@ConfigurationProperties(NacosConfigProperties.PREFIX)注入配置参数。
2.注入NacosConfigManager,生成NacosConfigService,定时任务线程,启动,定时监听。
3.注入NacosPropertySourceLocator,为了后面加载配置
4.注入NacosRefreshProperties,已过期的类
5.注入NacosRefreshHistory
6.注入NacosContextRefresher
7.注入NacosConfigEndpoint,actuator端点
8.注入NacosConfigHealthIndicator,boot的健康检查
9.spring上下文已经准备好,实现了ApplicationContextInitializer的PropertySourceBootstrapConfiguration执行initialize方法,由于之前注册过NacosPropertySourceLocator,所以触发NacosPropertySourceLocator的locate方法,加载配置。加载的方法大家可以去看一下,先去本地,本地没有再去网络请求。
10.springboot启动完毕,发送ApplicationReadyEvent事件,监听此事件的NacosContextRefresher,注册listener。
11.LongPollingRunnable发现数据变更,然后去获取更新后的内容,然后再遍历所有的listener,找到内容变更的cacheData,调用相关listener的receiveConfigInfo()方法,最终调用innerReceive()方法
12.innerReceive()方法里发送真正的RefreshEvent事件,通知cloud去刷新配置。
13.这个配置被RefreshEventListener监听到,然后刷新环境内的值。这个有兴趣可以去这个类里跟一下源码。
思考
源码还是有很多值得我们学习的地方
1.首先,cloud定义了一套刷新环境变量的事件RefreshEvent,无论你用何种方式去实现自己的配置。需要刷新的时候只需要推送一下这个事件就可以,也就是cloud制订了一套刷新的规则,需要刷新你通知我就可以。可以说是spring的老传统了。那么毫无疑问,即便是你没看过consol或者apollo的源码,想必你也很清楚他们是如何通知cloud去刷新配置信息了。
2.监听类LongPollingRunnable里,有让服务器可以挂起的操作,用来减轻轮询来带的损耗,有兴趣可以去研究一下
3.用newScheduledThreadPool去做线程调度器,用newCachedThreadPool去做真正的线程执行器,实现线程池的良性拓展,也是我们可以运用在我们项目中的一个好方法。
4.如何判断文件有变动呢?并不是每次都拉取所有的内容去比较,而是每个内容都通过md5加密,只需要判断md5是否发生改变就可以知道文件是否变动,我们也可以用在我们一些重要的持久化数据中。每次使用前和旧值md5去对比一下,可以知道我们的重要信息有没有被篡改。