找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1975

积分

0

好友

265

主题
发表于 3 天前 | 查看: 18| 回复: 0

随着项目推进,我们即将进入核心且复杂的服务端注册环节。代码量会逐渐增多,建议大家做好心理准备,并亲手实践。本项目的完整源代码我已开源至 GitHub,欢迎大家自取学习与参考:

https://github.com/CoderLeixiaoshuai/easy-rpc

温馨提示:对于这类实战项目,强烈建议跟随文章节奏,自己动手思考和敲代码。完整跟下来,你一定会收获颇丰!在 云栈社区 中,也有许多开发者分享类似的 开源实战 项目心得,可以相互交流。


这一节,我们的核心目标是实现服务端的关键业务逻辑。概括来说,主要完成两件事:

  • 第一,扫描代码中标记了 @ServiceExpose 注解的类,并将服务接口信息注册到注册中心。
  • 第二,启动网络通信服务器(Netty Server),开始监听指定端口,以便客户端能够连接并发送请求。

初始化RPC服务端

我们继续在 DefaultRpcListener 这个自定义监听器中添加核心逻辑。

public class DefaultRpcListener implements ApplicationListener<ContextRefreshedEvent> {
  // ……省略其他代码

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        final ApplicationContext applicationContext = event.getApplicationContext();
        // 如果是 root application context 就开始执行
        if (applicationContext.getParent() == null) {
            // 初始化 rpc 服务端
            initRpcServer(applicationContext);
        }
    }

  // ……省略其他代码
}  

上面的代码通过 event 对象获取了 Spring 的 Bean 容器 ApplicationContext,并将其作为参数传递给 initRpcServer 方法。接下来,我们实现这个方法:

    private void initRpcServer(ApplicationContext applicationContext) {
      // 1.1 扫描服务端 @ServiceExpose 注解,并将服务接口信息注册到注册中心  
        final Map<String, Object> 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 注解?这样,Spring 在启动时就会将该类初始化成一个 Bean 放入容器。当调用 applicationContext.getBeansWithAnnotation(ServiceExpose.class) 时,Spring 会遍历所有 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 项目计划同时适配两种主流注册中心:ZookeeperNacos。当然,如果你对其他如 Eureka、Consul 等注册中心感兴趣,也可以参照本文的模式自行扩展,这本身就是一个极佳的锻炼机会。

正式使用前,需要引入相应的客户端依赖。我们尽量选择官方或广泛认可的 SDK。

与 Zookeeper 交互,我们可以引入 zkclient

<!-- Zookeeper 客户端 -->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

对于 Nacos,则直接引入官方提供的 nacos-client

<!-- nacos 客户端 -->
<dependency>
  <groupId>com.alibaba.nacos</groupId>
  <artifactId>nacos-client</artifactId>
  <version>2.0.3</version>
</dependency>

定义服务注册接口

在日常编码中,养成面向接口编程的习惯至关重要,这能极大增强代码的可扩展性和可维护性。这正是 后端 & 架构 设计思想中强调的一点。

根据需求,服务注册的核心功能就是 注册服务。因此,我们可以定义一个名为 ServiceRegistry 的接口,其中包含一个 register 方法:

public interface ServiceRegistry {
    /**
     * 注册服务信息
     *
     * @param serviceInterfaceInfo 待注册的服务(接口)信息
     * @throws Exception 异常
     */
    void register(ServiceInterfaceInfo serviceInterfaceInfo) throws Exception;
}

服务向注册中心注册时,需要传递的内容我们用一个 InstanceInterfaceInfo 类来封装。注意,为了清晰区分,我们将前文提到的 ServiceInterfaceInfo 更名为 InstanceInterfaceInfo,以更准确地表示“服务实例的接口信息”。

@Data
public class InstanceInterfaceInfo {
    /**
     * 服务名(接口全限定名)
     */
    private String serviceName;

    /**
     * 实例 id,每个服务实例不一样
     */
    private String instanceId;

    /**
     * 服务实例 ip 地址,每个实例不一样
     */
    private String ip;

    /**
     * 服务端口号,每个实例一样
     */
    private Integer port;

    /**
     * 实现该接口 bean 对象对应的class 对象,用于后续反射调用
     */
    private Class<?> clazz;

    /**
     * 实现该接口的 bean 对象,用于后续反射调用
     */
    private Object obj;

}

Zookeeper 实现服务注册

接口定义完成后,我们开始编写实现。首先尝试用 Zookeeper 来实现服务注册功能。创建一个新类实现前面定义的 ServiceRegistry 接口:

public class ZookeeperServiceRegistry implements ServiceRegistry {
  @Override
    public void register(InstanceInterfaceInfo instanceInterfaceInfo) throws Exception {
      // TODO
    }
}

接下来,重写 register 方法。主要功能是调用 Zookeeper 客户端 API 创建 服务(永久)节点实例(临时)节点

通常,一个服务会部署多个实例,并且会根据业务流量动态扩缩容。因此,服务节点应该是一个永久节点,只需创建一次;而每个服务实例对应的节点应是临时节点,这样当实例故障下线时,节点会自动删除,便于客户端感知。

// ZookeeperServiceRegistry.java

@Override
public void register(InstanceInterfaceInfo instanceInterfaceInfo) throws Exception {
    logger.info(“Registering service: {}”, instanceInterfaceInfo);

    // 创建 ZK 永久节点(服务节点)
    String serviceName = instanceInterfaceInfo.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(instanceInterfaceInfo);
    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);
}

为了简化演示,这里直接将接口名与固定路径拼接作为永久节点路径;将每个实例的接口信息 JSON 序列化并 URL 编码后,作为临时节点名称。这并非生产环境的最佳实践,你可以基于此进行优化,例如使用更清晰的节点结构。

Nacos 实现服务注册

除了 Zookeeper,我们同样可以实现一个 Nacos 版本。首先创建对应的类:

public class NacosServiceRegistry implements ServiceRegistry {
   @Override
    public void register(InstanceInterfaceInfo instanceInterfaceInfo) throws Exception {
      // TODO
    }
}

接着编写构造方法。NacosServiceRegistry 类实例化时,需要连接 Nacos 服务端。

// NacosServiceRegistry.java

public NacosServiceRegistry(String serverList) throws NacosException {
    // 使用工厂类创建注册中心对象,构造参数为 Nacos Server 的地址
    naming = NamingFactory.createNamingService(serverList);
    // 打印 Nacos Server 的运行状态
    logger.info(“Nacos server status: {}”, naming.getServerStatus());
}

获得 NamingService 实例后,就可以调用其服务注册接口完成注册了。

// NacosServiceRegistry.java

@Override
public void register(InstanceInterfaceInfo instanceInterfaceInfo) throws Exception {
    // 注册当前服务实例
    naming.registerInstance(instanceInterfaceInfo.getServiceName(), buildInstance(instanceInterfaceInfo));
}

private Instance buildInstance(InstanceInterfaceInfo instanceInterfaceInfo) {
    // 构建 Nacos 实例对象
    Instance instance = new Instance();
    instance.setIp(instanceInterfaceInfo.getIp());
    instance.setPort(instanceInterfaceInfo.getPort());
    // TODO 可以添加更多元数据 (metadata)
    return instance;
}

NamingService 类提供了丰富的服务治理方法,感兴趣的话可以深入探索其 API。

代码结构

本小节,我们在项目中新建了 server.registry 包,定义了一个服务注册接口 ServiceRegistry,并基于主流的 ZookeeperNacos 注册中心完成了实现。具体的代码结构如下:

├── easy-rpc-spring-boot-starter
    ├── pom.xml
    ├── src
    │   └── main
    │       ├── java
    │       │   └── com
    │       │       └── leixiaoshuai
    │       │           └── easyrpc
    │       │               ├── annotation
    │       │               │   ├── ServiceExpose.java
    │       │               │   └── ServiceReference.java
    │       │               ├── common    
    │       │               │   └── InstanceInterfaceInfo.java    
    │       │               ├── listener
    │       │               │   └── DefaultRpcListener.java
    │       │               └── server
    │       │                   └── registry
    │       │                       ├── NacosServiceRegistry.java
    │       │                       ├── ServiceRegistry.java
    │       │                       └── ZookeeperServiceRegistry.java
    │       └── resources
    └── target

小结

我们实现的自定义监听器,本质上是 Spring Boot 注解驱动机制的核心。当 Spring 完成所有 Bean 的初始化工作后,监听器收到事件并开始执行:

  1. 扫描所有 Bean 对象,识别出标记了 @ServiceExpose 注解的类,将其加入到待处理列表。
  2. 逐个处理这些 Bean,提取服务接口信息,并将其注册到我们选定的注册中心。

服务注册通常依赖于第三方注册中心组件,本文选取了业界流行的 Zookeeper 和 Nacos,分别实现了业务代码。通过面向接口的设计,未来扩展其他注册中心将非常方便。

完成服务接口信息注册后,下一步就是启动网络通信服务器(Netty Server),监听端口。客户端从注册中心获取到服务端暴露的接口信息后,就能据此发起网络通信了。关于网络通信服务器的启动逻辑,我们将在下一小节详细介绍。


项目完整的源代码我已经开源到 GitHub 了,欢迎大家查看、学习甚至参与贡献。




上一篇:FinClip:将小程序容器化,打造企业自有应用生态的技术实践
下一篇:在RPC框架中利用Spring监听器实现自定义注解的启动时处理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 12:17 , Processed in 0.778248 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表