一、整体描述
如果想要本文主题中的其他方面的点,欢迎留言。
- 背景:
为相关经验的小伙伴进行知识分享,学习大厂程序员维护的开源项目,理解和吸收源码里面的优秀的设计思想,比如一些问题的解决问题思路,还有一些优秀的设计模式等,逐渐提升自己的技术功底与开发认知。
- 技术定位:
初级、中级、高级
- 目标群体:
想从开源项目上,获取大厂程序员的代码设计和思维方式的小伙伴,提升在公司的代码编写能力
想从开源项目上,吸收和理解某些代码设计,引入到当前自己所在的项目中的小伙伴,提升在公司的创新和应用能力
想从开源项目上,提升自己对于技术知识点的理解和应用场景的小伙伴,提升在技术层面的认知能力
- 技术应用:
除了自身的Nacos的使用之外,可以针对其源码中一些不错的点,进行借鉴,将其引入到自己目前的项目中,比如Nacos的单元测试用例设计、多线程常见的使用方法、常见的设计模式的使用和设计、代码整洁性的检查与维护等。
- 文档整体思路
从源码环境搭建、源码服务注册源码、代码设计、单元测试、设计模式、开源贡献等方面进行分享
- 如何看待Nacos
个人觉得可以这么理解:Nacos是一个基于SpringBoot开发的一个分布式的中间件类型的系统,除了自身对分布式微服务注册与配置中心的设计之外,里面也会有常见功能的CRUD,我们完全可以从每个人的业务系统开发经验上去看待它,可能你会看到自己的项目遇到的某些优化或某些功能是完全可以应用在其身上,或者反过来讲,其上面的某些功能设计也可以引入到当前项目中。因此,如果从业务系统的角度上看,可以理解他为:一堆大厂程序员维护和开发的基于SpringBoot的中间件类型的业务系统。
二、Nacos必备的核心概念
2.1、Nacos架构及功能组成
注意:在这份图中,值得我们学习和关注地方是:
插件机制
事件机制
一致性协议
2.2、临时实例和持久实例
当我们使用SpringBoot客户端将服务注册到Nacos的时候,如果没有明确制定指定ephemeral参数的值,默认情况下他是临时实例,即该值默认为true,所有实例的信息都在内存中存储,如果指定该值且值为false,则说明当前需要将实例的注册信息持久化。
临时实例
默认情况,服务实例仅会注册在Nacos内存,不会持久化到Nacos磁盘,其中:
在V1版本时代,其健康检测机制为Client模式,即Client主动向Server上报其健康状态(类似于推模式),默认心跳间隔为5秒,在15秒内Server未收到Client心跳,则会将其标记为“不健康”状态;在30秒内若收到了Client心跳,则重新恢复“健康”状态,否则该实例将从Server端内存清除。即对于不健康的实例,Server会自动清除;
在V2版本时代,由于临时实例采用GRPC长连接的方式,则由服务端主动检查健康状态。
持久实例
服务实例不仅会注册到Nacos内存,同时也会被持久化到Nacos磁盘,其健康检测机制为Server模式,即Server会主动去检测Client的健康状态(类似于拉模式); 默认每20秒检测一次,健康检测失败后服务实例会被标记为“不健康”状态,但不会被清除,因为其是持久化在磁盘的,其对不健康持久实例的清除。
临时实例和持久化实例最大的区别是健康检查的方式:临时实例使用客户端主动上报的健康检查模式,而持久化实例使用服务端反向探测的模式
2.3、一致性协议:AP模型和CP模型
AP模型的特点,保证可用性,写入实例后马上返回信息,后台采用自己封装的一致性的Distro协议同步数据,用户注册服务实例后,后台线程异步任务去同步数据。AP模式基于Distro协议(向任务阻塞队列添加一个本地服务实例改变任务,去更新本地服务列表,然后在遍历集群中所有节点,分别创建数据同步任务放进阻塞队列异步进行集群数据同步,不保证集群节点数据同步完成即可返回)。
CP模型的特点,保证一致性,写入半数集群节点后返回信息。采用的是Raft算法(集群过半节点写入成功之后才写入),通过leader节点将实例数据更新到内存和磁盘文件中,并且通过CountDownLatch实现了一个简单的raft写入数据的逻辑,必须集群半数以上节点写入成功才会给客户端返回成功)
一些知名的中间件,通常要么采用AP模型,要么采用CP模型,而Nacos根据不同的业务场景,提供了两者的支持。
对于Nacos的服务中心功能来讲,使用CP模式还是AP模式主要是看服务提供客户端选择,如果服务注册指定为临时,那么走AP模式,否则走CP模式。
对于Nacos的配置中心功能来讲,也是根据情况区分AP/CP的。
2.4、Nacos2.x版本的端口占用
默认情况下,除了8848端口以外,其余端口默认都是根据当前内置好的偏移量进行的计算。
对于外部端口的访问,则需要开通8848和9848端口。
对于Nacos2.x客户端来说,默认的9848也是根据当前客户端指定的8848端计算指定的,所以当更改服务端8848端口以后,建议提前计算好对应的GRPC端口,防止由于错误的设置主端口导致冲突。
思考:
1、假设目前内网中有3台待部署的机器,如果设置并修改Nacos的主端口为9601,那么需要申请哪些端口和机器流向?
2、假设内网的Nacos集群的主端口为9601,在上面部署一个Nginx负载均衡代理集群中的3台机器,Nginx的对外暴露的HTTP访问端口为5601,那么客户端和服务器如果都是2.x版本,此时对于客户端来说需要几个端口分别是什么?
2.5、服务相关概念
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
二、Nacos源码开发环境搭建
2.1、Nacos1.x源码开发环境搭建
提示:为了确保可以启动nacos源码,在启动编译前,在IDEA中提前安装好【protobuf】插件,Nacos源码中的一致性协议模块会依赖。
安装插件截图如下所示:
Nacos源码搭建过程相对于其他中间件来说,很容易。虽然Nacos目前最新的版本是2.2,但是1.x版本的核心思想也是很重要的,这里先以Nacos1.4的版本的Nacos的源码工程为例子,下载地址为:
2.2.1、导入Nacos源码到IDEA中
下载后,解压源码包版本的压缩文件1.4.4.zip,然后导入到IDEA开发工具中,如下所示:
2.2.2、编译项目
导入完成后,选择右侧的Maven操作栏中的编译功能,对项目进行整体的编译,如图所示:
编译过程相对耗时,稍等一会儿即可,出现如下效果代表编译成功:
2.2.3、启动项目
在启动的过程中,默认情况下可能会失败,提示需要我们配置JDBC的参数信息,由于默认的启动模式为集群模式,会寻找外部的MySQL配置的数据库信息,这个时候,我们可以通过设置如下参数,让其变成单机模式运行,且用内存derby数据库进行启动,在IDEA的Nacos类的启动JVM参数上进行如下设置:
-Dnacos.standalone=true -Dnacos.home=D:\develop\idea\nacos-1.4.4
其中,nacos.home的参数值为当前工程车的根目录,设置过程如下图所示:
出现如下日志过程,说明启动成功:
2.2.4、测试访问项目
在浏览器中输入:http://localhost:8848/nacos,
并输入默认的用户名和密码即可。
源码环境搭建成功。
三、Nacos核心原理
对于服务的核心原理,可以用如下一张代码流程图简要说明
Nacos核心原理图
如果自己对技术比较感兴趣,Nacos源码很值得去学习的,很容易接触到大厂程序员,了解到其思维方式,相对于其他框架/中间件源码入门来说相对容易,且可以学习到底层相关的JUC、并发、多线程、分布式通信、共识算法等等。同时也可以把自己做业务系统的某些经验提交到Nacos上,作为开源贡献。
四、Nacos代码与工程设计样例
4.1、Optional类的使用
在Java开发过程中经常会遇到空指针的问题,Java 8 API的出现,帮助我们提供了一个Optional的包装类,可以有效的防止代码中的空指针的问题。
先来看下Optional的源代码:
public final class Optional < T > {
/**
* Common instance for {@code empty()}.
*/
private static final Optional <? > EMPTY = new Optional < > ();
/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
private Optional() {
this.value = null;
}
public static < T > Optional < T > empty() {@
SuppressWarnings("unchecked") Optional < T > t = (Optional < T > ) EMPTY;
return t;
}
public static < T > Optional < T > of(T value) {
return new Optional < > (value);
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static < T > Optional < T > ofNullable(T value) {
return value == null ? empty() : of(value);
}
public boolean isPresent() {
return value != null;
}.....
}
从源代码中可以看到Optional类的主要职责就是帮助我们对原始对象进行了包装,进行了空值的判断,让我们的业务处理代码变得更佳优雅了。以前的代码中大量的判空让我们的代码有些丑陋。
再来看看Nacos中Optional是都怎么用的。
1、基于Derby的内存数据库的实现类EmbeddedStoragePersistServiceImpl
@Override
public long findConfigMaxId() {
String sql = "SELECT max(id) FROM config_info";
return Optional.ofNullable(databaseOperate.queryOne(sql, Long.class)).orElse(0 L);
}
可以看到,这个在业务中也很常见,如果数量查询为空就付给一个默认值,这个也优雅的体现了拆箱的判断,以前写丑陋代码的时候,这块也很容易出现拆箱出现空值的问题。
2、基于SPI的加解密插件的处理器EncryptionHandler
这个其实对于企业业务系统开发来讲比较实用的,我们可以参考下,适当的引入到当自己所在的项目中。
public static Pair < String, String > encryptHandler(String dataId, String content) {
if (!checkCipher(dataId)) {
return Pair.with("", content);
}
String algorithmName = parseAlgorithmName(dataId);
Optional < EncryptionPluginService > optional = EncryptionPluginManager.instance().findEncryptionService(algorithmName);
if (!optional.isPresent()) {
LOGGER.warn("[EncryptionHandler] [encryptHandler] No encryption program with the corresponding name found");
return Pair.with("", content);
}
EncryptionPluginService encryptionPluginService = optional.get();
String secretKey = encryptionPluginService.generateSecretKey();
String encryptContent = encryptionPluginService.encrypt(secretKey, content);
return Pair.with(encryptionPluginService.encryptSecretKey(secretKey), encryptContent);
}
可以看到使用SPI加载了加密实现后,并optional.isPresent方法进行了判断。
3、JRaftServer类中invokeToLeader的异常判断
private void invokeToLeader(final String group, final Message request, final int timeoutMillis, FailoverClosure closure) {
try {
final Endpoint leaderIp = Optional.ofNullable(getLeader(group)).orElseThrow(() - > new NoLeaderException(group)).getEndpoint();.......
} catch (Exception e) {
closure.setThrowable(e);
closure.run(new Status(RaftError.UNKNOWN, e.toString()));
}
}
可以看到该代码片段中使用orElseThrow方法优雅的进行了异常抛出处理。
4.2、请求重试的样例逻辑
我们知道,在后端开发中,对于网络间的调用经常会采用代码重试机制去保证代码执行,对于一些经验尚浅的开发者来说,学习一些开源框架中优秀的请求代码重试机制的代码设计也是很有帮助的。
Nacos中的请求代码重试的代码设计思路也是对我们的开发很有帮助的。
这里以RpcClient类为例,他的主要代码如下:
package com.alibaba.nacos.common.remote.client;
public abstract class RpcClient implements Closeable {
/**
* 定义最大重试次数:3次
*/
private static final int RETRY_TIMES = 3;
/**
* 发送请求
*/
public Response request(Request request, long timeoutMills) throws NacosException {
int retryTimes = 0;
Response response;
Exception exceptionThrow = null;
//定义当前请求的开始时间
long start = System.currentTimeMillis();
//如果当前重试次数 小于 默认重试次数 并且 当前时间戳小于未来的超时时间戳
while (retryTimes < RETRY_TIMES && System.currentTimeMillis() < timeoutMills + start) {
boolean waitReconnect = false;
try {
//检查客户端是否处于连接状态
if (this.currentConnection == null || !isRunning()) {
waitReconnect = true;
throw new NacosException(NacosException.CLIENT_DISCONNECT,
"Client not connected, current status:" + rpcClientStatus.get());
}
response = this.currentConnection.request(request, timeoutMills);
// 省略一部分代码
//............
// 省略一部分代码
// return response.
//更新最后时间戳,并返回响应
lastActiveTimeStamp = System.currentTimeMillis();
return response;
} catch (Exception e) {
if (waitReconnect) {
try {
// wait client to reconnect.
Thread.sleep(Math.min(100, timeoutMills / 3));
} catch (Exception exception) {
// Do nothing.
}
}
exceptionThrow = e;
}
//走到这里说明请求失败了,重试次数+1
retryTimes++;
}
//CAS 对象标记为不健康
if (rpcClientStatus.compareAndSet(RpcClientStatus.RUNNING, RpcClientStatus.UNHEALTHY)) {
switchServerAsyncOnRequestFail();
}
//如果存在请求处理异常错误信息
if (exceptionThrow != null) {
throw (exceptionThrow instanceof NacosException) ? (NacosException) exceptionThrow
: new NacosException(SERVER_ERROR, exceptionThrow);
} else {
throw new NacosException(SERVER_ERROR, "Request fail, unknown Error");
}
}
}
可以看到,这个是一个很经典的请求重试的代码设计模板,对于日常开发中自己的业务中需要请求重试的时候,也可以参考这个代码片段的设计实现。同样的代码设计实现,该类中的asyncRequest方法也可以进行参考。这个代码片段是先执行业务代码后,在进行重试次数的处理,如果某些情况需要先对次数进行处理,然后再进行业务代码,此时可以参考该类中的重试次数减法的代码设计。
int startUpRetryTimes = RETRY_TIMES;
while (startUpRetryTimes > 0 && connectToServer == null) {
try {
startUpRetryTimes--;
ServerInfo serverInfo = nextRpcServer();
LoggerUtils.printIfInfoEnabled(LOGGER, "[{}] Try to connect to server on start up, server: {}", name,
serverInfo);
connectToServer = connectToServer(serverInfo);
} catch (Throwable e) {
LoggerUtils.printIfWarnEnabled(LOGGER, "[{}] Fail to connect to server on start up, error message = {}, start up retry times left: {}",
name, e.getMessage(), startUpRetryTimes);
}
}
这段代码其实是Nacos中Grpc进行服务检查的核心代码,通过重试3次的处理,每一次轮训机器中的列表进行网络请求检查。
4.3、使用SPI进行插件化
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架 (opens new window)扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。可以实现让调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
Java中的标准SPI实现:
1、自定义SPI的接口规范。
2、 然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名。
3、使用ServiceLoader类进行加载
ServiceLoader<CustomFactory> uploadCDN = ServiceLoader.load(CustomFactory.class);
Java中的SPI设计不足之处:
1、只能遍历所有的实现,并全部实例化。
2、配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
3、扩展如果依赖其他的扩展,做不到自动注入和装配。
4、扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持
nacos中对ServiceLoader类进行了基础的封装,将系统中的扩展统一加载到内存中,核心代码如下:
public class NacosServiceLoader {
private static final Map<Class<?>, Collection<Class<?>>> SERVICES = new ConcurrentHashMap<Class<?>, Collection<Class<?>>>();
public static <T> Collection<T> load(final Class<T> service) {
if (SERVICES.containsKey(service)) {
return newServiceInstances(service);
}
Collection<T> result = new LinkedHashSet<T>();
for (T each : ServiceLoader.load(service)) {
result.add(each);
cacheServiceClass(service, each);
}
return result;
}
private static <T> void cacheServiceClass(final Class<T> service, final T instance) {
if (!SERVICES.containsKey(service)) {
SERVICES.put(service, new LinkedHashSet<Class<?>>());
}
SERVICES.get(service).add(instance.getClass());
}
public static <T> Collection<T> newServiceInstances(final Class<T> service) {
return SERVICES.containsKey(service) ? newServiceInstancesFromCache(service) : Collections.<T>emptyList();
}
@SuppressWarnings("unchecked")
private static <T> Collection<T> newServiceInstancesFromCache(Class<T> service) {
Collection<T> result = new LinkedHashSet<T>();
for (Class<?> each : SERVICES.get(service)) {
result.add((T) newServiceInstance(each));
}
return result;
}
private static Object newServiceInstance(final Class<?> clazz) {
try {
return clazz.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new ServiceLoaderException(clazz, e);
}
}
}
SERVICES中存储了相关接口的SPI实现。通过这个设计,可以很好的达到扩展插件加载的功能。
4.3.1、加载客户端授权插件ClientAuthPlugin
ClientAuthPluginManager类使用该SPI工具类,加载客户端授权插件:
public void init(List < String > serverList, NacosRestTemplate nacosRestTemplate) {
Collection < AbstractClientAuthService > clientAuthServices = NacosServiceLoader.load(AbstractClientAuthService.class);
for (ClientAuthService clientAuthService: clientAuthServices) {
clientAuthService.setServerList(serverList);
clientAuthService.setNacosRestTemplate(nacosRestTemplate);
clientAuthServiceHashSet.add(clientAuthService);
LOGGER.info("[ClientAuthPluginManager] Load ClientAuthService {} success.",
clientAuthService.getClass().getCanonicalName());
}
if (clientAuthServiceHashSet.isEmpty()) {
LOGGER.warn("[ClientAuthPluginManager] Load ClientAuthService fail, No ClientAuthService implements");
}
}
4.3.2、加载ID生成器插件
IdGeneratorManager类使用该SPI工具类,加载ID生成器实现:
private final Function<String, IdGenerator> supplier;public IdGeneratorManager() {this.supplier = s -> {IdGenerator generator;Collection<IdGenerator> idGenerators = NacosServiceLoader.load(IdGenerator.class);Iterator<IdGenerator> iterator = idGenerators.iterator();if (iterator.hasNext()) {
generator = iterator.next();} else {
generator = new SnowFlowerIdGenerator();}
generator.init();return generator;};}
4.3.3、处理加解密插件
EncryptionPluginManager类使用该SPI工具类,处理加解密的实现,自己根据自己的加密算法进行设计和实现:
private static final Map < String, EncryptionPluginService > ENCRYPTION_SPI_MAP = new ConcurrentHashMap < > ();
private static final EncryptionPluginManager INSTANCE = new EncryptionPluginManager();
public EncryptionPluginManager() {
loadInitial();
}
/**
* Load initial.
*/
private void loadInitial() {
Collection < EncryptionPluginService > encryptionPluginServices = NacosServiceLoader.load(EncryptionPluginService.class);
for (EncryptionPluginService encryptionPluginService: encryptionPluginServices) {
if (StringUtils.isBlank(encryptionPluginService.algorithmName())) {
LOGGER.warn("[EncryptionPluginManager] Load EncryptionPluginService({}) algorithmName(null/empty) fail." + " Please Add algorithmName to resolve.", encryptionPluginService.getClass());
continue;
}
ENCRYPTION_SPI_MAP.put(encryptionPluginService.algorithmName(), encryptionPluginService);
LOGGER.info("[EncryptionPluginManager] Load EncryptionPluginService({}) algorithmName({}) successfully.",
encryptionPluginService.getClass(), encryptionPluginService.algorithmName());
}
}
类似使用的地方还有很多,比如:健康检查的处理、服务实例检查处理器、序列化工厂、事件中心的事件发布者等等。
在Nacos中提倡使用插件扩展的方式增加自定义的功能实现。
4.4、代码工程风格统一保障
Nacos使用了checkstyle配置文件与maven插件,保障每一位参与贡献的开发者遵守相同的代码开发约定,比如空格缩进、注释等。配置如下:
这块也是可以值得学习的,了解下别人家是如何控制开源项目的质量的,为什么开源项目感觉没有特别大的代码风格和质量问题。可以参考一下,引入到当前各自的项目中的,这样当开发者想要编译启动项目的时候,如果发现代码质量和风格存在不统一的问题,会提示错误。常见的如下:
1、代码未注释问题
2、代码空格和Tab缩进问题
等等
4.5、Nacos代码设计截图举例
五、Nacos源码部分设计模式
5.1、单例设计模式
5.1.1、单例模式的定义
属于创建型模式,保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。
5.1.2、单例模式的使用场景
1、意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
(2)主要解决: 一个全局使用的类频繁地创建与销毁。
(3)何时使用: 控制实例数目,节省系统资源的时候。
(4)如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
(5)关键代码: 构造函数是私有的;提供一个获得该实例的对外方法。
5.1.3、Nacos中的使用
在大部分的使用中都采用了饿汉式单例用法。
5.1.3.1、Distro协议配置DistroConfig
该类主要采用的是饿汉式单例用法,核心代码如下:
/**
* 饿汉式单例用法。
*/
public class DistroConfig extends AbstractDynamicConfig {
private static final String DISTRO = "Distro";
private static final DistroConfig INSTANCE = new DistroConfig();
//...省略部分代码
private DistroConfig() {
super(DISTRO);
resetConfig();
}
public static DistroConfig getInstance() {
return INSTANCE;
}
}
这个类的使用本身还是一个模板方法的设计模式,抽象出一个动态配置的关系,同类的涉及到ClientConfig、PushConfig等。
相关的饿汉式的单例类有:ServiceManager、DynamicDataSource、DistroRecordsHolder、ClientFactoryHolder、NamingTpsMonitor等。
同类的还有ConfigChangeHandler、ConfigHttpClientManager、NacosLogging、NamingHttpClientManager、
5.1.4、工作中刻意使用单例模式
对于某些业务,我想定义一个类,不让他用注入的方式进行依赖注入,让其他Service中的代码都统一的调用这个类的单例方法去做操作,比如可以定义一个事件派发管理器:
EventManager.getInstance().pushEvent(new OrderCancelEvent(this,order))
这块可以参考Nacos源码中的一些单例的设计,为什么用单例,而不是IOC容器中的Service。
单例模式是工作中最容易被使用的一种模式,只要你想给某个东西提供一个全局访问点,就可以定义为一个Manager结尾的类,并提供一个Instance方法。
5.2、模板方法设计模式
提示:模板方法设计模式,Nacos中的核心功能通用部分都用模板方法模式进行处理
5.2.1、模板方法模式的定义
在父类定义一个操作中的算法骨架,而将算法的一些因具体情况而定的步骤延迟到子类中实现,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
5.2.2、模板方法模式的结构
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
模板方法:一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。由于模板方法是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
5.2.3、Nacos中模板方法模式的使用
5.2.3.1、Nacos的Distro异步任务
接口类:NacosTask
抽象类:AbstractExecuteTask、AbstractDistroExecuteTask
具体实现类:DistroSyncChangeTask、DistroSyncDeleteTask
其中,DistroSyncChangeTask和DistroSyncDeleteTask类负责,集群之间基于Distro协议的sync数据同步。AbstractDistroExecuteTask类中定义程序的流程,同时暴露了doExecute和doExecuteWithCallback方法,核心代码如下:
public abstract class AbstractDistroExecuteTask extends AbstractExecuteTask
{
private final DistroKey distroKey;
private final DistroComponentHolder distroComponentHolder;
protected AbstractDistroExecuteTask(DistroKey distroKey, DistroComponentHolder distroComponentHolder)
{
this.distroKey = distroKey;
this.distroComponentHolder = distroComponentHolder;
}
@Overridepublic void run()
{
String type = getDistroKey().getResourceType();
DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(type);
if(null == transportAgent)
{
Loggers.DISTRO.warn("No found transport agent for type [{}]", type);
return;
}
Loggers.DISTRO.info("[DISTRO-START] {}", toString());
if(transportAgent.supportCallbackTransport())
{
doExecuteWithCallback(new DistroExecuteCallback());
}
else
{
executeDistroTask();
}
}
private void executeDistroTask()
{
try
{
boolean result = doExecute();
if(!result)
{
handleFailedTask();
}
Loggers.DISTRO.info("[DISTRO-END] {} result: {}", toString(), result);
}
catch (Exception e)
{
Loggers.DISTRO.warn("[DISTRO] Sync data change failed.", e);
handleFailedTask();
}
}
protected abstract DataOperation getDataOperation();
protected abstract boolean doExecute();
protected abstract void doExecuteWithCallback(DistroCallback callback);......省略部分代码
}
其中,DistroSyncChangeTask类的核心代码如下,一般当用到模板方法设计模式的时候,都会在子类中定义名称或者类型的方法,标识自己是谁。
public class DistroSyncChangeTask extends AbstractDistroExecuteTask {
private static final DataOperation OPERATION = DataOperation.CHANGE;
public DistroSyncChangeTask(DistroKey distroKey, DistroComponentHolder distroComponentHolder) {
super(distroKey, distroComponentHolder);
}
/**
* 子类定义一个类型返回
*/
@
Overrideprotected DataOperation getDataOperation() {
return OPERATION;
}
/**
* 实现父类的抽象方法,通常以do开头
*/
@Override、
protected boolean doExecute() {
String type = getDistroKey().getResourceType();
DistroData distroData = getDistroData(type);
if (null == distroData) {
Loggers.DISTRO.warn("[DISTRO] {} with null data to sync, skip", toString());
return true;
} // 策略模式获取传输协议 同步数据return getDistroComponentHolder().findTransportAgent(type).syncData(distroData, getDistroKey().getTargetServer());}/**
* 实现父类的抽象方法, 通常以do开头
* /@Ove@Overrideprotected void doExecuteWithCallback(DistroCallback callback) {String type = getDistroKey().getResourceType();DistroData distroData = getDistroData(type);if (null == distroData) {Loggers.DISTRO.warn("[DISTRO] {} with null data to sync, skip", toString());return;}/ / 策略模式获取传输协议 同步数据getDistroComponentHolder().findTransportAgent(type).syncData(distroData, getDistroKey().getTargetServer(), callback);
}
private DistroData getDistroData(String type) {
DistroData result = getDistroComponentHolder().findDataStorage(type).getDistroData(getDistroKey());
if (null != result) {
result.setType(OPERATION);
}
return result;
}
}
5.3、建造者设计模式
5.3.1、建造者模式的定义
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
5.3.2、建造模式的使用场景
当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。
建造者模式一般是链式调用。
5.3.3、Nacos中的使用位置
nacos中的注册实例的接口,对于接收到的实例信息,采用了InstanceBuilder这个类完成构造,避免这个定义多个构造函数导致的需求变化问题,InstanceBuilder类的使用代码如下所示:
public String register(
@RequestParam(defaultValue = Constants.DEFAULT_NAMESPACE_ID) String namespaceId
,@RequestParam String serviceName, @RequestParam String ip
,@RequestParam(defaultValue = UtilsAndCommons.DEFAULT_CLUSTER_NAME) String cluster
,@RequestParam Integer port, @RequestParam(defaultValue = "true") Boolean healthy
,@RequestParam(defaultValue = "1") Double weight
, @RequestParam(defaultValue = "true") Boolean enabled
,@RequestParam String metadata
, @RequestParam Boolean ephemeral) throws Exception {
// NamingUtils.checkServiceNameFormat(serviceName);checkWeight(weight);
// 使用构造者设计模式建造实例对象
final Instance instance = InstanceBuilder.newBuilder().
setServiceName(serviceName).setIp(ip).setClusterName(cluster).
setPort(port).setHealthy(healthy).setWeight(weight).setEnabled(enabled).
setMetadata(UtilsAndCommons.parseMetadata(metadata)).setEphemeral(ephemeral).build();
if(ephemeral == null)
{
instance.setEphemeral((switchDomain.isDefaultInstanceEphemeral()));
}
instanceServiceV2.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
其中,InstanceBuilder类的代码如下,并采用了非内部类的方式进行定义:
public class InstanceBuilder
{
private String instanceId;
private String ip;
private Integer port;
private Double weight;
private Boolean healthy;
private Boolean enabled;
private Boolean ephemeral;
private String clusterName;
private String serviceName;
private Map < String, String > metadata = new HashMap < > ();
private InstanceBuilder()
{}
public InstanceBuilder setInstanceId(String instanceId)
{
this.instanceId = instanceId;
return this;
}
public InstanceBuilder setIp(String ip)
{
this.ip = ip;
return this;
}
public InstanceBuilder setPort(Integer port)
{
this.port = port;
return this;
}
public InstanceBuilder setWeight(Double weight)
{
this.weight = weight;
return this;
}
public InstanceBuilder setHealthy(Boolean healthy)
{
this.healthy = healthy;
return this;
}
public InstanceBuilder setEnabled(Boolean enabled)
{
this.enabled = enabled;
return this;
}
public InstanceBuilder setEphemeral(Boolean ephemeral)
{
this.ephemeral = ephemeral;
return this;
}
public InstanceBuilder setClusterName(String clusterName)
{
this.clusterName = clusterName;
return this;
}
public InstanceBuilder setServiceName(String serviceName)
{
this.serviceName = serviceName;
return this;
}
public InstanceBuilder setMetadata(Map < String, String > metadata)
{
this.metadata = metadata;
return this;
}
public InstanceBuilder addMetadata(String metaKey, String metaValue)
{
this.metadata.put(metaKey, metaValue);
return this;
}
/**
* Build a new {@link Instance}.
*
* @return new instance
*/
public Instance build()
{
Instance result = new Instance();
if(!Objects.isNull(instanceId))
{
result.setInstanceId(instanceId);
}
if(!Objects.isNull(ip))
{
result.setIp(ip);
}
if(!Objects.isNull(port))
{
result.setPort(port);
}
if(!Objects.isNull(weight))
{
result.setWeight(weight);
}
if(!Objects.isNull(healthy))
{
result.setHealthy(healthy);
}
if(!Objects.isNull(enabled))
{
result.setEnabled(enabled);
}
if(!Objects.isNull(ephemeral))
{
result.setEphemeral(ephemeral);
}
if(!Objects.isNull(clusterName))
{
result.setClusterName(clusterName);
}
if(!Objects.isNull(serviceName))
{
result.setServiceName(serviceName);
}
result.setMetadata(metadata);
return result;
}
public static InstanceBuilder newBuilder()
{
return new InstanceBuilder();
}
}
通过这个方法,可以看到将对象的构造过程封装为了单独的builder类。
5.4、委派模式
5.4.1、委派设计模式概念
委派就像是拿另一种方法替代了原本的方法,交给现在这个替代后的方法使用,使用时和原来的方法没有区别。
允许对象组合实现与继承相同的代码重用。它的基本作用就是负责任务的调用和分配任务,是一种特殊的静态代理,可以理解我全权代理,但是代码模式注重过程,而委派模式注重结果。属于行为型模式。
5.4.2、委派设计模式角色
-
抽象任务角色(Task):定义一个接口,它有若干实现类。
-
委派角色(Delegate):负责在各个具体角色实例之间做出决策,屏判断调用具体实现的方法。
-
具体任务角色(Concrete)真正执行任务的角色
通常我们看到某个类的名称中包含Delegete字符串,基本上都是一种委派设计模式的体现。类关系如下:
client
调用
Delegate类----------实现-----------任务角色接口
具体任务角色1----------实现-----------任务角色接口
具体任务角色2----------实现-----------任务角色接口
5.4.3、Nacos中委派模式的使用
5.4.3.1、客户端模块的请求委派处理
以客户端的请求入口NacosNamingService类为起点,在该类的构造函数中对接口NamingClientProxy进行了委派的初始化,代码如下:
this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder,
properties, changeNotifier);
可以了解到NacosNamingService中的客户端请求的业务逻辑设计为委派模式,去根据自己的业务去控制什么时候采用HTTP请求,什么时候采用GRPC请求。
在Nacos2.1版本中:
对于客户端注册服务方法registerService和取消注册服务方法deregisterService的逻辑,采用动态判断,当实例为临时的方式的时候,使用grpcClientProxy,非临时采用httpClientProxy。
private NamingClientProxy getExecuteClientProxy(Instance instance) {
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
对于获得服务列表的方法,采用的grpcClientProxy的方式:
@Override
public ListView<String> getServiceList(int pageNo, int pageSize,
String groupName, AbstractSelector selector)throws NacosException {
return grpcClientProxy.getServiceList(pageNo, pageSize, groupName, selector);
}
对于订阅subscribe和取消订阅的方法,采用的也是grpcClientProxy的方式。
5.4.3.2、服务端模块的客户端管理器委派处理
以服务端的接收心跳HealthController类为起点,在该类的会判断当前服务器是否支持GRPC的特征,如果是则使用Nacos2中的HealthOperatorV2Impl类进行心跳操作的相关处理,而这个类中需要注入一个ClientManager客户管理期委托实现。
public HealthOperatorV2Impl(NamingMetadataManager metadataManager,
ClientManagerDelegate clientManager,ClientOperationServiceProxy clientOperationService) {
this.metadataManager = metadataManager;
this.clientManager = clientManager;
this.clientOperationService = clientOperationService;
}
所以角色对应关系如下:
通过判断客户端的ID信息,委派不同的任务角色进行处理,核心判断逻辑如下:
private ClientManager getClientManagerById(String clientId) {
if (isConnectionBasedClient(clientId)) {
return connectionBasedClientManager;
}
return clientId.endsWith(ClientConstants.PERSISTENT_SUFFIX) ?
persistentIpPortClientManager : ephemeralIpPortClientManager;
}
private boolean isConnectionBasedClient(String clientId) {
return !clientId.contains(IpPortBasedClient.ID_DELIMITER);
}
5.4.3.3、服务端模块的一致性协议委派处理
Nacos服务端在处理服务一致性协议相关的时候,也采委派模式去选择Distro协议处理业务还是Raft协议处理业务,代码注入如下:
@Resource(name = "consistencyDelegate")private ConsistencyService consistencyService;
所以角色对应关系如下:
可以看到针对持久化协议的设计,本身又是一个内部委派处理方式,代码如下:
@DependsOn("ProtocolManager")
@Service("consistencyDelegate")
public class DelegateConsistencyServiceImpl implements ConsistencyService
{
private final PersistentConsistencyServiceDelegateImpl persistentConsistencyService;
private final EphemeralConsistencyService ephemeralConsistencyService;
public DelegateConsistencyServiceImpl(PersistentConsistencyServiceDelegateImpl
persistentConsistencyService, EphemeralConsistencyService ephemeralConsistencyService){
this.persistentConsistencyService = persistentConsistencyService;
this.ephemeralConsistencyService = ephemeralConsistencyService;
}
@Override
public void put(String key, Record value) throws NacosException{
mapConsistencyService(key).put(key, value);
}
/**
* 委派同时满足
*/
@Override
public boolean isAvailable()
{
return ephemeralConsistencyService.isAvailable() &&
persistentConsistencyService.isAvailable();
}
/**
* 委派判断条件
*/
private ConsistencyService mapConsistencyService(String key)
{
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService :
persistentConsistencyService;
}
}
在Nacos2.1中,针对持久化协议的委派处理,PersistentConsistencyServiceDelegateImpl,相关角色对应关系如下:
判断规则如下:
private PersistentConsistencyService switchOne() {
return switchNewPersistentService ? newPersistentConsistencyService :
oldPersistentConsistencyService;
}
在Nacos中类似使用委派模式的地方还有很多,可以看到通过该模式,让我们对于任务的分配和委派很清晰。对于我们业务开发其实也是有帮助的,日常业务开发中,多思考下企业对于一些兼容类型的处理或扩展类型的处理都可以用到该模式去处理。
七、开源贡献
Nacos源码是比较容易参与到开源项目的贡献的,不仅可以结识到更多大牛,同时也可以快速建立自己的影响力,对于自己以后的提升好处多多。
目前个人为Nacos提交的PR记录次数为9个(针对数据源那块的代码进行小小优化),虽然不是核心改动,但是至少是踏进了开源社区的大门。
7.1、开源贡献的代码方向
1、单词或注释拼写错误修复
2、单元测试用例补充
3、某些自己做过的同类的功能经验方面的贡献
4、解决ISSUE问题
5、新需求开发
6、文档补充和完善
7.2、Nacos源码贡献步骤
这里未来补充下演示过程。
1、fork项目到自己的Github仓库
2、clone项目到本地
3、创建分支开发代码
4、提交代码到远程Git仓库
5、向主仓库提交修改PR审核合并请求
6、主仓库管理员审核通过并merge。
喜欢本篇文章的,请点赞、收藏、分享、评论哦,一起和我交流吧。