【从0到1设计一个网关】整合Nacos-服务注册与服务订阅的实现

文章目录

Nacos

Nacos提供了许多强大的功能:

比如服务发现、健康检测。

Nacos支持基于DNS和基于RPC的服务发现。

同时Nacos提供对服务的实时的健康检查,阻止向不健康的主机活服务发送请求。

并且Nacos也提供了一个可视化的控制台方便我们对实例等信息进行管理。

同时Nacos提供了动态配置服务,可以让我们以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置

Nacos是我在开发自己项目过程中用的最多的一个注册中心和配置中心,并且Nacos的社区相对其他的来说更加活跃,代码也更加容易阅读。

如下是Nacos官网:Nacos官网

我就不再这篇文章里面过多的讲解Nacos的一些特性了。

在这一章节中,我将使用Nacos暴露出来的接口,来完成项目的服务注册功能以及服务发现功能。

完成这一章的学习也会让你更加深入的了解到Nacos的底层运行原理,注册中心原理。

下面是一些我曾经学习Nacos过程中编写的一些文章,有兴趣可以看看。
使用Nacos实现动态线程池技术以及Nacos配置文件更新监听事件
【源码分析】Nacos如何使用AP协议完成服务端之间的数据同步?
【源码分析】Nacos服务端如何更新以及保存注册表信息?
Nacos自动注册原理实现以及服务注册更新并如何保存到注册表

为什么选择Nacos之前的文章简单讲解过,这里我将详细的列举出几个原因:

  • Nacos 提供了让我从微服务平台建设的视角管理数据中心的所有服务及元数据,具体原因可以看看上面我对Nacos源码的分析,Nacos将服务细粒度的划分为了各自实例,并且我们可以管理这些实例的信息
  • Nacos支持基于DNS和基于RPC的服务发现,这也就意味着提供给我们较强的服务发现的选择能力
  • Nacos提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求,也就是安全
  • 动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,我之前也已经利用过这一点来实现对线程池的动态配置具体可以查看这篇文章

定义服务注册与订阅方法

在这一步,我们将需要定义一些网关项目用于连接到Nacos这个注册中心的接口,来实现会将我们的项目链接到注册中心。

要将一个服务注册到注册中心,大概需要初始化、注册、取消注册、服务订阅等方法,也就是我们需要编写一个如下的接口来提供这样子的一个接口并在后面的具体注册中心实例中去实现这个接口方法。

java 复制代码
public interface RegisterCenter {

    /**
     *   初始化
     * @param registerAddress  注册中心地址
     * @param env  要注册到的环境
     */
    void init(String registerAddress, String env);

    /**
     * 注册
     * @param serviceDefinition 服务定义信息
     * @param serviceInstance 服务实例信息
     */
    void register(ServiceDefinition serviceDefinition, ServiceInstance serviceInstance);

    /**
     * 注销
     * @param serviceDefinition
     * @param serviceInstance
     */
    void deregister(ServiceDefinition serviceDefinition, ServiceInstance serviceInstance);

    /**
     * 订阅所有服务变更
     * @param registerCenterListener
     */
    void subscribeAllServices(RegisterCenterListener registerCenterListener);
}

实现完毕接口之后,我们还需要提供一个方法,它的作用是用于监听注册中心的配置的变更。

这也是Nacos作为注册中心和配置中心特别重要的一个功能,接口定义如下:

java 复制代码
public interface RegisterCenterListener {

    void onChange(ServiceDefinition serviceDefinition,
                  Set<ServiceInstance> serviceInstanceSet);
}

服务信息加载与配置

基于上面的服务注册与订阅接口,我们就可以大致编写出来如何将我们的网关注册到Nacos中了。当然,我们还没有具体实现如何注册到Nacos注册中心的方法,但是我们可以先编写出来其大致的一个调用方法。

java 复制代码
@Slf4j
public class Bootstrap
{
    public static void main( String[] args )
    {
        //加载网关核心静态配置
        Config config = ConfigLoader.getInstance().load(args);
        System.out.println(config.getPort());

        //插件初始化
        //配置中心管理器初始化,连接配置中心,监听配置的新增、修改、删除
        //启动容器
        Container container = new Container(config);
        container.start();

        //连接注册中心,将注册中心的实例加载到本地
        final RegisterCenter registerCenter = registerAndSubscribe(config);
        //服务优雅关机
        //进程收到kill信号的时候进行一个注销操作
        Runtime.getRuntime().addShutdownHook(new Thread(){

            /**
             * 下线操作
             */
            @Override
            public void run(){
                registerCenter.deregister(
                        buildGatewayServiceDefinition(config),
                        buildGatewayServiceInstance(config));
            }
        });
    }


    /**
     * 当前方法用于提供注册和订阅服务信息变更通知
     * @param config
     * @return
     */
    private static RegisterCenter registerAndSubscribe(Config config) {
        //加载服务提供者  具体这里的作用可以 查看我的博客
        ServiceLoader<RegisterCenter> serviceLoader = ServiceLoader.load(RegisterCenter.class);
        final RegisterCenter registerCenter = serviceLoader.findFirst().orElseThrow(() -> {
            log.error("not found RegisterCenter impl");
            return new RuntimeException("not found RegisterCenter impl");
        });
        //初始化注册中心信息
        registerCenter.init(config.getRegistryAddress(), config.getEnv());

        //构造网关服务定义和服务实例
        ServiceDefinition serviceDefinition = buildGatewayServiceDefinition(config);
        ServiceInstance serviceInstance = buildGatewayServiceInstance(config);

        //注册
        registerCenter.register(serviceDefinition, serviceInstance);

        //订阅
        registerCenter.subscribeAllServices(new RegisterCenterListener() {
            @Override
            public void onChange(ServiceDefinition serviceDefinition, Set<ServiceInstance> serviceInstanceSet) {
                log.info("refresh service and instance: {} {}", serviceDefinition.getId(),
                        JSON.toJSON(serviceInstanceSet));
                DynamicConfigManager manager = DynamicConfigManager.getInstance();
                manager.addServiceInstance(serviceDefinition.getId(), serviceInstanceSet);
            }
        });
        return registerCenter;
    }

    /**
     * 构建网关服务实例
     * @param config
     * @return
     */
    private static ServiceInstance buildGatewayServiceInstance(Config config) {
        String localIp = NetUtils.getLocalIp();
        int port = config.getPort();
        ServiceInstance serviceInstance = new ServiceInstance();
        serviceInstance.setServiceInstanceId(localIp + COLON_SEPARATOR + port);
        serviceInstance.setIp(localIp);
        serviceInstance.setPort(port);
        serviceInstance.setRegisterTime(TimeUtil.currentTimeMillis());
        return serviceInstance;
    }

    /**
     * 构建网关服务定义信息
     * @param config
     * @return
     */
    private static ServiceDefinition buildGatewayServiceDefinition(Config config) {
        ServiceDefinition serviceDefinition = new ServiceDefinition();
        serviceDefinition.setInvokerMap(Map.of());
        serviceDefinition.setId(config.getApplicationName());
        serviceDefinition.setServiceId(config.getApplicationName());
        serviceDefinition.setEnvType(config.getEnv());
        return serviceDefinition;
    }

}

其中比较重要的就是这一行代码,也就是加载服务提供者

java 复制代码
ServiceLoader.load(RegisterCenter.class) 

ServiceLoader是 Java 中用于加载服务提供者的工具,通常用于实现服务提供者框架。它的作用是查找和加载指定接口或抽象类的服务提供者实现类,这些实现类在运行时动态注册到系统中,以便其他组件或应用程序可以使用它们的功能。

具体来说,以下是它的作用和用法:

  • 服务接口定义:首先,您需要定义一个服务接口或抽象类,这是您想要不同实现的抽象描述。在您的例子中,RegisterCenter.class 似乎是一个服务接口。

  • 服务提供者实现:不同的模块或库可以提供服务接口的不同实现,这些实现类可以独立于应用程序开发并且可以在运行时加载。

  • 服务提供者注册:每个服务提供者实现类需要在 META-INF/services 目录下创建一个文件,该文件的名称是服务接口的全限定名,内容是服务提供者实现类的全限定名。这告诉 Java 运行时系统哪些类实现了该服务接口。

  • 加载服务提供者:使用 ServiceLoader.load(RegisterCenter.class),您可以加载所有已经注册的服务提供者实现类。这返回一个 ServiceLoader 对象,您可以迭代这个对象以获取所有已加载的实现类的实例。

这个机制允许应用程序在不修改源代码的情况下动态地切换和使用不同的服务提供者实现,从而提高了应用程序的可扩展性和灵活性。它通常用于框架和库,以允许开发者插入他们自己的实现,例如数据库驱动程序、日志记录器、插件等。

实现将网关注册到注册中心

想要将网关注册到注册中心,我们首先需要引入Nacos的客户端依赖。

java 复制代码
       <!--引入Nacos的客户端依赖-->
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.0.4</version>
        </dependency>
        <!--导入我们直接实现的注册中心接口-->
        <dependency>
            <groupId>blossom.project</groupId>
            <artifactId>BlossomGateway-Register-Center-Api</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

之后,我们就可以使用Nacos客户端中提供的服务注册方法进行服务注册了。

方式如下:

java 复制代码
@Slf4j
public class NacosRegisterCenter implements RegisterCenter {

    /**
     * 注册中心的地址
     */
    private String registerAddress;

    /**
     * 环境选择
     */
    private String env;

    /**
     * 主要用于维护服务实例信息
     */
    private NamingService namingService;

    /**
     * 主要用于维护服务定义信息
     */
    private NamingMaintainService namingMaintainService;

    /**
     * 监听器列表
     * 这里由于监听器可能变更 会出现线程安全问题
     */
    private List<RegisterCenterListener> registerCenterListenerList = new CopyOnWriteArrayList<>();

    @Override
    public void init(String registerAddress, String env) {
        this.registerAddress = registerAddress;
        this.env = env;

        try {
            this.namingMaintainService = NamingMaintainFactory.createMaintainService(registerAddress);
            this.namingService = NamingFactory.createNamingService(registerAddress);
        } catch (NacosException e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public void register(ServiceDefinition serviceDefinition, ServiceInstance serviceInstance) {
        try {
            //构造nacos实例信息
            Instance nacosInstance = new Instance();
            nacosInstance.setInstanceId(serviceInstance.getServiceInstanceId());
            nacosInstance.setPort(serviceInstance.getPort());
            nacosInstance.setIp(serviceInstance.getIp());
            //实例信息可以放入到metadata中
            nacosInstance.setMetadata(Map.of(GatewayConst.META_DATA_KEY, JSON.toJSONString(serviceInstance)));

            //注册
            namingService.registerInstance(serviceDefinition.getServiceId(), env, nacosInstance);

            //更新服务定义
            namingMaintainService.updateService(serviceDefinition.getServiceId(), env, 0,
                    Map.of(GatewayConst.META_DATA_KEY, JSON.toJSONString(serviceDefinition)));

            log.info("register {} {}", serviceDefinition, serviceInstance);
        } catch (NacosException e) {
            throw new RuntimeException(e);
        }
    }
}

这里需要对Nacos的源码有了解,你才能明白如何将一个服务实例注册到Nacos的注册中心,Nacos这边要求你提供服务的ip、端口、服务名称等信息。

完成这一步之后,我们大概就已经成功的将服务注册到Nacos了。

实现服务的订阅

这里我们开始实现服务订阅,要想实现服务订阅,首先需要拉取Nacos上面的所有的服务的信息,并且服务信息会不断的更新变化,因此我们还需要使用定时任务的方式不断的更新我们的服务订阅信息。

要先实现对Nacos服务信息的订阅,需要用到Nacos的事件监听器,NamingEvent。

在Nacos注册中心中,NamingEvent 是一个事件对象,用于表示与服务命名空间(Naming)相关的事件。NamingEvent 的作用是用于监听和处理命名空间中的服务实例(Service Instance)的变化,以便应用程序可以根据这些变化来动态地更新服务实例列表,以保持与注册中心的同步。

具体来说,NamingEvent 主要用于以下目的:

  • 监听服务实例的变化:Nacos注册中心可以包含大量的服务实例,而这些实例可能会因服务上线、下线、实例元数据变化等原因而发生变化。NamingEvent 允许应用程序注册监听器,以便在服务实例发生变化时得到通知。

  • 动态更新服务实例列表:通过监听 NamingEvent,应用程序可以实时获得有关服务实例的状态变化,从而可以及时更新自己维护的服务实例列表,以确保使用最新的服务实例信息。

  • 实现负载均衡:应用程序可以根据 NamingEvent 提供的信息来实现负载均衡策略,例如选择合适的服务实例以提供服务请求。负载均衡策略可以根据服务实例的可用性、健康状态和其他元数据来进行调整。

  • 动态路由:一些应用程序可能需要实现动态路由,根据服务实例的变化来动态更新路由规则,以确保请求被正确路由到可用的服务实例。

大致的代码实现如下:

java 复制代码
 @Override
    public void subscribeAllServices(RegisterCenterListener registerCenterListener) {
        //服务订阅首先需要将我们的监听器加入到我们的服务列表中
        registerCenterListenerList.add(registerCenterListener);
        //进行服务订阅
        doSubscribeAllServices();

        //可能有新服务加入,所以需要有一个定时任务来检查
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1, new NameThreadFactory(
                "doSubscribeAllServices"));
        //循环执行服务发现与订阅操作
        scheduledThreadPool.scheduleWithFixedDelay(() -> doSubscribeAllServices(), 10, 10, TimeUnit.SECONDS);

    }

    private void doSubscribeAllServices() {
        try {
            //得到当前服务已经订阅的服务
            //这里其实已经在init的时候初始化过namingservice了,所以这里可以直接拿到当前服务已经订阅的服务
            //如果不了解的可以debug
            Set<String> subscribeService =
                    namingService.getSubscribeServices().stream().map(ServiceInfo::getName).collect(Collectors.toSet());


            int pageNo = 1;
            int pageSize = 100;


            //分页从nacos拿到所有的服务列表
            List<String> serviseList = namingService.getServicesOfServer(pageNo, pageSize, env).getData();

            //拿到所有的服务名称后进行遍历
            while (CollectionUtils.isNotEmpty(serviseList)) {
                log.info("service list size {}", serviseList.size());

                for (String service : serviseList) {
                    //判断是否已经订阅了当前服务
                    if (subscribeService.contains(service)) {
                        continue;
                    }

                    //nacos事件监听器 订阅当前服务
                    //这里我们需要自己实现一个nacos的事件订阅类 来具体执行订阅执行时的操作
                    EventListener eventListener = new NacosRegisterListener();
                    eventListener.onEvent(new NamingEvent(service, null));
                    namingService.subscribe(service, env, eventListener);
                    log.info("subscribe {} {}", service, env);
                }
                //遍历下一页的服务列表
                serviseList = namingService.getServicesOfServer(++pageNo, pageSize, env).getData();
            }

        } catch (NacosException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 实现对nacos事件的监听器
     * NamingEvent 是一个事件对象,用于表示与服务命名空间(Naming)相关的事件。
     * NamingEvent 的作用是用于监听和处理命名空间中的服务实例(Service Instance)的变化,
     * 以便应用程序可以根据这些变化来动态地更新服务实例列表,以保持与注册中心的同步。
     */
    public class NacosRegisterListener implements EventListener {

        @Override
        public void onEvent(Event event) {
            if (event instanceof NamingEvent) {
                log.info("the triggered event info is:{}",JSON.toJSON(event));
                NamingEvent namingEvent = (NamingEvent) event;
                String serviceName = namingEvent.getServiceName();

                try {
                    //获取服务定义信息
                    Service service = namingMaintainService.queryService(serviceName, env);
                    ServiceDefinition serviceDefinition =
                            JSON.parseObject(service.getMetadata().get(GatewayConst.META_DATA_KEY),
                                    ServiceDefinition.class);

                    //获取服务实例信息
                    List<Instance> allInstances = namingService.getAllInstances(service.getName(), env);
                    Set<ServiceInstance> set = new HashSet<>();

                    for (Instance instance : allInstances) {
                        ServiceInstance serviceInstance =
                                JSON.parseObject(instance.getMetadata().get(GatewayConst.META_DATA_KEY),
                                        ServiceInstance.class);
                        set.add(serviceInstance);
                    }

                    registerCenterListenerList.stream().forEach(l -> l.onChange(serviceDefinition, set));
                } catch (NacosException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

此时,我们就完成了在Nacos注册中心发生信息变更的时候,能在一次拉取到最新的配置信息。也就是我们完成了对注册中心的订阅。

相关推荐
希忘auto4 分钟前
详解MySQL安装
java·mysql
冰淇淋烤布蕾15 分钟前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺21 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Jakarta EE38 分钟前
正确使用primefaces的process和update
java·primefaces·jakarta ee
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
java—大象1 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
布川ku子2 小时前
[2024最新] java八股文实用版(附带原理)---Mysql篇
java·mysql·面试
向阳12182 小时前
JVM 进阶:深入理解与高级调优
java·jvm