Nacos因其出色的的读写性能以及简单灵活的配置方式,被很多公司应用于配置管理,将Nacos当做配置服务器或者配置中心。Nacos还可以很方便的和Spring集成,这也提高了其使用的频率
本文将通过Nacos的简单示例,探索Nacos实现原理,如果没有搭建过Nacos服务器的同学可以参考:链接:Nacos服务器搭建
使用Nacos +SpringCloud 可以很快的完成配置中心搭建并应用,先看一个简单示例(Nacos +SpringCloud),在SpringCloud中主要是使用@RefreshScope + @Value 来实现配置更新。
@Value的作用是通过注解将常量、配置文件中的值、其他bean的属性值注入到变量中,作为变量的初始值。
@RefreshScope 是SpringCloud中的注解,需要热加载的bean就需要加上这个注解 , 表示是需要RefreshScope 代理的bean, 这个bean强制为懒加载,只有第一次使用的时候生成实例,配置变更的时候直接调用destroy()方法销毁当前的bean, 再根据配置信息生成新的bean, 完成热加载。 RefreshScope注解,可以看看另一篇总结 【SpringCloud】RefreshScope 自动刷新配置
1. 增加依赖spring-cloud-alibaba-nacos-config
plugins {
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.springnacos'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
compile'org.springframework.boot:spring-boot-starter-web:2.0.9.RELEASE'
compile 'org.springframework.boot:spring-boot-configuration-processor:2.0.9.RELEASE'
compile 'org.springframework.cloud:spring-cloud-alibaba-nacos-config:0.2.2.RELEASE'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
2. 定义bootstrat.yml文件
Nacos当配置中心的时候,一般将nacos配置信息写入bootstrap.yml , 其中 bootstrap.yml和 application.yml区别如下
bootstrap.yml用来在程序引导时执行,应用于更加早期配置信息读取,bootstrap.yml 先于 application.yml 加载
application.yml 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。
spring:
application:
name: service-system
cloud:
nacos:
config:
#nacos配置中心服务器地址
server-addr: localhost:8848
#配置文件后缀,用于拼接配置配置文件名称
file-extension: yaml
#配置命名空间(填入前面新建的命名空间ID)
namespace: nacos-test
#配置分组
group: TEST_GROUP
#配置自动刷新
refresh-enabled: true
#配置文件的前缀 :prefix−{spring.profile.active}.${file-extension}
#prefix表示配置文件前缀,默认是spring.application.name的值,如果配置了spring.cloud.nacos.config.prefix就取prefix的值
#spring.profile.active 表示项目使用的profile.active配置,没有则配置文件中没有此段名称
#file-extension 表示配置文件的后缀,目前只支持yml和properties
prefix: nacos-demo
3. 定义配置,配置使用@Value注解,同时增加springcloud的热刷新注解@RefreshScope
package com.test.springnacos.conf;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
package com.test.springnacos.example1;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@RefreshScope
@Component
public class ValueConf {
//@Value获取最新值一定要加@RefreshScope注解,配置文件中配置refresh: true
@Value("${datasource.username}")
private String username;
@Value("${datasource.password}")
private String password;
//略getter && setter
}
4. 定义测试类
package com.test.springnacos.example1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NacosRefreshScopeTest {
@Autowired
private ValueConf conf;
//访问:http://localhost:8080/getMessage
@RequestMapping("/getMessage")
public String getMessage() {
return "username:" + conf.getUsername() + "password:" + conf.getPassword();
}
}
5. 测试结果
访问:http://localhost:8080/getMessage
url:nacos-service:3306
username:nacos-n
password:nacos-p
version:1.0.0
当修改配置后username:nacos-name刷新
url:nacos-service:3306
username:nacos-name
password:nacos-p
version:1.0.0
1. 启动报错
检查springcloud的版本是否兼容
2. 没自动更新配置
@Value获取最新值一定要加SpringCloud @RefreshScope注解,配置文件中配置自动刷新 refresh-enabled: true
看看上面的示例效果,我们大概可以了解到Nacos简单的实现原理Nacos 服务其通过Namespace-Group-DataId的方式保存了配置信息,Nacos 和Springcloud集成后,Nacos 服务器信息发生变化的时候,客户端能在很短时间内发现变更,并通过Springcloud@RefreshScope 注解实现更新配置,仔细看看实例,是不是大家也有以下几个疑问呢?下面来深入探索Nacos的实现原理
首先在SpringCloud中应用Nacos 需要引用spring-cloud-alibaba-nacos-config 这个包,查看包中的pom文件会发现加载后还引入了 nacos-client包,
spring-cloud-alibaba-nacos-config包结构大概如下,
我们先看下spring.factories文件,SpringFactories是spring提供的自定义的SPI机制,SPI 全称为(Service Provider Interface)是一种动态发现服务的机制,SpringBoot启动后,它读取META-INF/spring.factories文件中配置的接口实现类名称,然后在程序中读取这些配置文件并实例化。这里提到了2个重要的配置类 NacosConfigBootstrapConfiguration和NacosConfigAutoConfiguration , 这2个Bean都有注解@Configuration ,从Spring3.0以后,@Configuration用于定义配置类,配置类用于替换原来的xml, 在配置类中定义Bean, 当Spring启动启动的时候会加载这些Bean
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigAutoConfiguration,\
org.springframework.cloud.alibaba.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.cloud.alibaba.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
NacosConfigBootstrapConfiguration 中创建了2个Bean : NacosConfigProperties 和 NacosPropertySourceLocator 对象
NacosConfigAutoConfiguration 创建了4个Bean ,
@Configuration
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean //这个注解表示存在了就不再创建相同的Bean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigProperties nacosConfigProperties) {
return new NacosPropertySourceLocator(nacosConfigProperties);
}
}
@Configuration
@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);
}
NacosConfigProperties nacosConfigProperties = new NacosConfigProperties();
return nacosConfigProperties;
}
@Bean
public NacosRefreshProperties nacosRefreshProperties() {
return new NacosRefreshProperties();
}
@Bean
public NacosRefreshHistory nacosRefreshHistory() {
return new NacosRefreshHistory();
}
@Bean
public NacosContextRefresher nacosContextRefresher(
NacosConfigProperties nacosConfigProperties,
NacosRefreshProperties nacosRefreshProperties,
NacosRefreshHistory refreshHistory) {
return new NacosContextRefresher(nacosRefreshProperties, refreshHistory,
nacosConfigProperties.configServiceInstance());
}
}
NacosConfigProperties 是Nacos配置的核心对象,记录了serverAddr,namespace,goup等配置信息以及 List
private String serverAddr;
private String encode;
private String group = "DEFAULT_GROUP";
private String prefix;
private String fileExtension = "properties";
private int timeout = 3000;
private String endpoint;
private String namespace;
private String accessKey;
private String secretKey;
private String contextPath;
private String clusterName;
private String name;
private String sharedDataids;
private String refreshableDataids;
private List extConfig;
private ConfigService configService;
//..
public ConfigService configServiceInstance() {
//...
try {
configService = NacosFactory.createConfigService(properties);
return configService;
}
catch (Exception e) {
log.error("create config service error!properties={},e=,", this, e);
return null;
}
}
NacosContextRefresher 是实现刷新的核心对象,对象本身继承了ApplicationListener ,表明自己本身是Spring的监听器,当Spring发布事件的时候就会获取事件,当捕获事件后会向configserver注册一个监听器
//用于记录配置是否支持刷新
private final NacosRefreshProperties refreshProperties;
//用于和服务器链接
private final ConfigService configService;
public NacosContextRefresher(NacosRefreshProperties refreshProperties,
NacosRefreshHistory refreshHistory, ConfigService configService) {
this.refreshProperties = refreshProperties;
this.refreshHistory = refreshHistory;
this.configService = configService;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// 用于捕获Spring的事件
if (this.ready.compareAndSet(false, true)) {
//通过此方法将自定义的监听器注册Nacos服务器
//以便服务器配置发生变更后通知客户端
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
//如果支持刷新
if (refreshProperties.isEnabled()) {
//所有Repository缓存的配置信息都刷新
for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
.getAll()) {
if (!nacosPropertySource.isRefreshable()) {
continue;
}
String dataId = nacosPropertySource.getDataId();
//将配置注册到服务器端
registerNacosListener(nacosPropertySource.getGroup(), dataId);
}
}
}
private void registerNacosListener(final String group, final String dataId) {
//1。 创建监听器
Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
//监听器的回调方法: 当服务器端配置发生变化的时候会调用这个方法来实现变更客户端信息
//变更客户端配置
//1.变更刷新次数
refreshCountIncrement();
String md5 = "";
if (!StringUtils.isEmpty(configInfo)) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
.toString(16);
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
//2.记录变更历史
refreshHistory.add(dataId, md5);
//3.发布RefreshEvent事件,更新客户端的配置
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
}
}
@Override
public Executor getExecutor() {
return null;
}
});
//2. 将监听器注册到服务器端,
try {
configService.addListener(dataId, group, listener);
}
catch (NacosException e) {
e.printStackTrace();
}
}
刚才看到 NacosConfigProperties中通过 configService = NacosFactory.createConfigService(properties); 那现在来看看ConfigService, ConfigService 是一个接口,属于nacos-api工程中。当引用spring-cloud-alibaba-nacos-config的时候会自动引入这个工程,
public class NacosFactory {
public static ConfigService createConfigService(Properties properties) throws NacosException {
//1.通过工厂获取
return ConfigFactory.createConfigService(properties);
}
// .....
}
public class ConfigFactory {
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
//2.通过反射得到NacosConfigService实例
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());
}
//...
}
从程序上看,通过反射得到了ConfigService接口的一个实例 NacosConfigService (位于nacos-client工程中),查看NacosConfigService 对象信息对象,他持有2个重要对象,代理对象HttpAgent和长轮询对象ClientWorker,
/**
* http agent //1. HTTP代理
*/
private HttpAgent agent;
/**
* longpolling //2. 长轮询对象
*/
private ClientWorker worker;
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);
//3.agent 的实际工作者是ServerHttpAgent
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
worker = new ClientWorker(agent, configFilterChainManager);
}
HttpAgent 是个代理接口,实现对象是 ServiceHttpAgent,通过properties对象解析出serverUrls 完成Http调用
@Override
public HttpResult httpGet(String path, List headers, List paramValues, String encoding,
long readTimeoutMs) throws IOException {
final long endTime = System.currentTimeMillis() + readTimeoutMs;
boolean isSSL = false;
do {
try {
List newHeaders = getSpasHeaders(paramValues);
if (headers != null) {
newHeaders.addAll(headers);
}
HttpResult result = HttpSimpleClient.httpGet(
getUrl(serverListMgr.getCurrentServerAddr(), path, isSSL), newHeaders, paramValues, encoding,
readTimeoutMs, isSSL);
if (result.code == HttpURLConnection.HTTP_INTERNAL_ERROR
|| result.code == HttpURLConnection.HTTP_BAD_GATEWAY
|| result.code == HttpURLConnection.HTTP_UNAVAILABLE) {
LOGGER.error("[NACOS ConnectException] currentServerAddr: {}, httpCode: {}",
serverListMgr.getCurrentServerAddr(), result.code);
} else {
return result;
}
} catch (ConnectException ce) {
LOGGER.error("[NACOS ConnectException] currentServerAddr:{}", serverListMgr.getCurrentServerAddr());
serverListMgr.refreshCurrentServerAddr();
} catch (SocketTimeoutException stoe) {
LOGGER.error("[NACOS SocketTimeoutException] currentServerAddr:{}", serverListMgr.getCurrentServerAddr());
serverListMgr.refreshCurrentServerAddr();
} catch (IOException ioe) {
LOGGER.error("[NACOS IOException] currentServerAddr: " + serverListMgr.getCurrentServerAddr(), ioe);
throw ioe;
}
} while (System.currentTimeMillis() <= endTime);
LOGGER.error("no available server");
throw new ConnectException("no available server");
}
ClientWork维护了2个线程池
@SuppressWarnings("PMD.ThreadPoolCreationRule")
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
//1. 只有1个线程的线程池
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;
}
});
//2.创建一个长链接线程,且是守护线程
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;
}
});
//3.线程池调度每隔10毫秒执行checkConfigInfo方法
executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
//4.从名称上看是一个检查配置的方法
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
//5.检查的时候使用长链接的线程池线程处理长链接任务LongPollingRunnable
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();
//1.循环cacheMap中的每一个CacheData 检查本地配置checkLocalConfig
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);
//2.每一个groupKey 循环处理
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 {
//3.获取服务器端的配置信息
String content = getServerConfig(dataId, group, tenant, 3000L);
//4.将配置信息存入 CacheData 对象中,content记录配置值md5记录将content字符串做md5加密后的值
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))) {
//5.通过checkListenerMd5检测cacheData之前的MD5数据和从服务器获取的MD5配置是否相同,如果不同质性safeNotifyListener
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
} catch (Throwable e) {
LOGGER.error("longPolling error", e);
} finally {
executorService.execute(this);
}
}
}
private void safeNotifyListener(final String dataId, final String group, final String content,
final String md5, final ManagerListenerWrap listenerWrap) {
//6.获取监听,还记得之前NacosContextRefresher对象接收spring事件后会调用ConfigService注册一个监听器吗,这里就开始用到了
final Listener listener = listenerWrap.listener;
//7.独立线程处理
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();
//8.调用监听并接收最新配置
listener.receiveConfigInfo(contentTmp);
//9.更新最新的MD5值
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);
}
看到这里我们大概可以总结整体实现过程:
Nacos启动的时候是如何更新配置的呢?这需要看下核心对象NacosContextRefresher的源码就一目了然了, NacosContextRefresher对象实现了接口ApplicationListener,ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。当ApplicationContext发布ApplicationEvent时,ApplicationListener的所有实现Bean都将自动被触发,那么更新配置肯定是通过事件来实现的
SpringBoot启动后会对事件监听器做很多操作,先starting, 然后start等, 我们查下源码会发现refreshContext阶段处理后会调用#listeners.running(context)
public ConfigurableApplicationContext run(String... args) {
....
SpringApplicationRunListeners listeners = getRunListeners(args);//得到所有监听器
listeners.starting();
try {
.....
context = createApplicationContext();
.....
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);// 这里开始加载配置
.....
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);//运行监听器
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
public interface ConfigService {
/**
* Add a listener to the configuration, after the server modified the configuration, the client will use the
* incoming listener callback. Recommended asynchronous processing, the application can implement the getExecutor
* method in the ManagerListener, provide a thread pool of execution. If not provided, use the main thread callback, May
* block other configurations or be blocked by other configurations.
*
* @param dataId dataId
* @param group group
* @param listener listener
* @throws NacosException NacosException
*/
void addListener(String dataId, String group, Listener listener) throws NacosException;
//...
}
下一篇:SpringSession原理以及源码分析