我们继续在DefaultRpcListener自定义监听器中添加代码:
public class DefaultRpcListener implements ApplicationListener {
// ……省略其他代码
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
final ApplicationContext applicationContext = event.getApplicationContext();
// 如果是 root application context就开始执行
if (applicationContext.getParent() == null) {
// 初始化 rpc 服务端
initRpcServer(applicationContext);
}
}
// ……省略其他代码
}
上面代码中,通过 event 对象可以获取 ApplicationContext 对象,这就是 Spring 的 bean 容器,作为参数传递给initRpcServer方法中,继续写代码:
private void initRpcServer(ApplicationContext applicationContext) {
// 1.1 扫描服务端@ServiceExpose注解,并将服务接口信息注册到注册中心
final Map beans = applicationContext.getBeansWithAnnotation(ServiceExpose.class);
if (beans.size() == 0) {
// 没发现注解
return;
}
for (Object beanObj : beans.values()) {
// 注册服务实例接口信息
registerInstanceInterfaceInfo(beanObj);
}
// 1.2 启动网络通信服务器,开始监听指定端口
// TODO
}
上面代码中,通过ApplicationContext提供的getBeansWithAnnotation方法,可以很方便根据指定注解查找所有的 bean 对象集合。
不知道大家是否还记得,我们在定义@ServiceExpose注解时特地在注解前面加了一个@Component注解,没印象的可以翻一下前面的文章或者看下 RPC 源码。
Spring 首先会把这个类初始化成一个bean放在容器中,当调用:applicationContext.getBeansWithAnnotation(ServiceExpose.class) 时,Spring 会将所有的 bean 对象遍历一遍,如果发现 bean 对象对应类前面有一个@ServiceExpose注解对象就会添加到返回列表中。
拿到所有符合条件的 bean 对象 map 集合后,会逐个进行服务注册,对外暴露接口,这一段逻辑封装在registerInstanceInterfaceInfo方法中,我们继续写代码:
private void registerInstanceInterfaceInfo(Object beanObj) {
final Class<?>[] interfaces = beanObj.getClass().getInterfaces();
if (interfaces.length <= 0) {
// 注解类未实现接口
return;
}
// 暂时只考虑实现了一个接口的场景
Class<?> interfaceClazz = interfaces[0];
String interfaceName = interfaceClazz.getName();
String ip = getLocalAddress();
Integer port = rpcProperties.getExposePort();
String serviceName = rpcProperties.getServiceName();
try {
// 注册服务
serviceRegistry.register(new InstanceInterfaceInfo(serviceName, interfaceName, ip, port, interfaceClazz, beanObj));
} catch (Exception e) {
logger.error("Fail to register service: {}", e.getMessage());
}
}
上面代码中首先会判断使用了@ServiceExpose注解的类是否实现了接口,如果未实现任何接口就忽略这个 bean 对象。加这个限制的原因是后面我们会在客户端中使用 JDK 的动态代理功能,JDK 原生的动态代理要求被代理类必须实现接口,否则代理无效。如果大家打算使用CGLIB动态代理技术,这里可以不用再加限制。
为了使程序更简单,代码中仅仅考虑实现了一个接口的场景,如果加了注解的类实现了多个接口,大家在这个地方可以考虑扩展支持或者进行限制。
紧接着就是将服务接口通过服务注册的方式暴露给客户端,暴露的信息包括:接口名、服务实例 ip、服务实例端口等,代码实现上将暴露的信息封装在一个InstanceInterfaceInfo对象中,调用注册服务的方法,作为参数传过去:
// 注册服务
serviceRegistry.register(xxx);
serviceRegistry 是下面待实现的类。
服务注册是将服务计划暴露的信息注册到一个地方,这样客户端就可以从这个地方获取,为了可靠性注册的地方一般不会保存在本地,这个时候就可以引用一个第三方的注册中心。
业界流行的注册中心非常多,为了满足不同小伙伴的学习兴趣,这次 RPC项目计划同时适配两种注册中心:Zookeeper 和 Nacos,当然如果你对其他注册中心感兴趣也可以自行扩展适配,这也是一个非常好的锻炼方式。
在正式使用前需要引入相应的依赖,注册中心的客户端一般支持多种编码语言,我们尽量选择官方提供的 SDK。
与 Zookeeper 交互可以引入对应的 SDK,zkclient 是个不错的选择:
com.101tec
zkclient
0.10
至于 Nacos,可以直接引入官方提供的 SDK:nacos-client:
com.alibaba.nacos
nacos-client
2.0.3
在日常的工作或者学习编码过程中,我们一定要习惯面向接口编程,这样做有助于增强代码可扩展性。
根据前面的需求描述,服务注册只需要干一件事情:服务注册,我们可以定义一个接口:ServiceRegistry,接口中定义一个方法:register,代码如下:
public interface ServiceRegistry {
/**
* 注册服务信息
*
* @param serviceInterfaceInfo 待注册的服务(接口)信息
* @throws Exception 异常
*/
void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception;
}
服务向注册中心注册,注册的内容定义一个类InstanceInterfaceInfo来封装。
@Data
public class ServiceInterfaceInfo {
/**
* 服务名(接口全限定名)
*/
private String serviceName;
/**
* 实例 id,每个服务实例不一样
*/
private String instanceId;
/**
* 服务实例 ip 地址,每个实例不一样
*/
private String ip;
/**
* 服务端口号,每个实例一样
*/
private Integer port;
/**
* 实现该接口 bean 对象对应的class 对象,用于后续反射调用
*/
private Class<?> clazz;
/**
* 实现该接口的 bean 对象,用于后续反射调用
*/
private Object obj;
}
接口定义好了之后开始写实现,我们尝试用 Zookeeper 来实现服务注册功能,先新建一个类实现前面定义好的服务注册接口:
public class ZookeeperServiceRegistry implements ServiceRegistry {
@Override
public void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception {
// TODO
}
}
接下来重写register方法,主要功能包括调用 Zookeeper 接口创建服务节点和实例节点。
通常一个服务会部署多个实例,也会根据业务流量增加或者减少实例,因此服务节点是一个永久节点,只用创建一次;实例节点是临时节点,如果实例故障下线,实例节点会自动删除。
// ZookeeperServiceRegistry.java
@Override
public void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception {
logger.info("Registering service: {}", serviceInterfaceInfo);
// 创建 ZK 永久节点(服务节点)
String serviceName = serviceInterfaceInfo.getServiceName();
String servicePath = "/com/leixiaoshuai/easyrpc/service/" + serviceName;
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath, true);
logger.info("Created node: {}", servicePath);
}
// 创建 ZK 临时节点(实例节点)
String uri = JSON.toJSONString(serviceInterfaceInfo);
uri = URLEncoder.encode(uri, "UTF-8");
String uriPath = servicePath + "/" + uri;
if (zkClient.exists(uriPath)) {
zkClient.delete(uriPath);
}
zkClient.createEphemeral(uriPath);
logger.info("Created ephemeral node: {}", uriPath);
}
为了图简便,我这里使用接口名称加上固定的字符串拼接成永久节点;将每个实例的接口信息转换为字符串作为临时节点。这样做不是很优雅,大家可以好好优化下。
除了使用 Zookeeper 来实现,我们还可以使用 Nacos,跟上面一样我们还是先建一个类:
public class NacosServiceRegistry implements ServiceRegistry {
@Override
public void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception {
// TODO
}
}
接着编写构造方法,NacosServiceRegistry 类被实例化之后 Nacos 客户端也要连接上 Nacos 服务端。
// NacosServiceRegistry.java
public NacosServiceRegistry(String serverList) throws NacosException {
// 使用工厂类创建注册中心对象,构造参数为 Nacos Server 的 ip 地址,连接 Nacos 服务器
naming = NamingFactory.createNamingService(serverList);
// 打印 Nacos Server 的运行状态
logger.info("Nacos server status: {}", naming.getServerStatus());
}
获得NamingService类的实例对象后,就可以调用实例注册接口完成服务注册了。
// NacosServiceRegistry.java
@Override
public void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception {
super.register(serviceInterfaceInfo);
// 注册当前服务实例
naming.registerInstance(serviceInterfaceInfo.getServiceName(), buildInstance(serviceInterfaceInfo));
}
private Instance buildInstance(InstanceInterfaceInfo instanceInterfaceInfo) {
// 将实例信息注册到 Nacos 中心
Instance instance = new Instance();
instance.setIp(instanceInterfaceInfo.getIp());
instance.setPort(instanceInterfaceInfo.getPort());
// TODO add more metadata
return instance;
}
注意:NamingService 类提供了很多有用的方法,大家可自行进行尝试。
本小节,新建了一个server.registry包,定义了一个服务注册接口:ServiceRegistry.java,选取了业界流行的两个注册中心完成了实现代码:ZookeeperServiceRegistry.java和NacosServiceRegistry.java,具体代码结构如下:
easy-rpc-spring-boot-starter
pom.xml
src
main
java
com
leixiaoshuai
easyrpc
annotation
ServiceExpose.java
ServiceReference.java
common
ServiceInterfaceInfo.java
listener
DefaultRpcListener.java
server
registry
NacosServiceRegistry.java
ServiceRegistry.java
ZookeeperServiceRegistry.java
resources
target
自定义监听器就是为了实现注解的驱动程序,等 Spring 完成所有的 bean 初始化工作后,监听器收到事件开始工作:
服务注册常常依赖第三方注册中心组件,本文选取了业界流行的两个组件:Zookeeper 和 Nacos 分别进行业务代码实现。
将服务接口信息注册到注册中心后就可以开始进行第二步了:启动网络通信服务器(Netty Server),开始监听指定端口,客户端通过注册拿到服务端暴露的接口信息就会与服务端进行通信了。
再次提醒,项目完整的源代码我已经开源到 Github 了,大家可自取:
https://github.com/CoderLeixiaoshuai/easy-rpc
来源:https://mp.weixin.qq.com/s/Ig-a8KNbbCOjA5TVEctFPw
作者:雷小帅
页面更新:2024-05-13
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号