业务流程

在上面两个章节中我们已经为我们的网关注册中心(api-gateway-center)提供了注册功能,无论是网关服务api-gateway-core还是RPC服务都可以想网关注册中心发起注册。

在第9章的网关注册中心服务初始化中,我们梳理过一次整个网关业务的项目架构

image-afpt.png

我们说到过我们是将api-gateway-core当作一种类似于Mybatis的数据资源(算力资源),这种数据资源在springboot中绝对不能和springboot耦合,因此我们提供了对其的包装api-gateway-assist,将其类比为一个SpringBoot starter

那么现在我们来思考一下这个SpringBoot starter到底应该具有那些功能呢?我们的api-gateway-core在之前的实现过程中,我们一直有一个HttpStatement的映射,正是因为有这个映射,网关通信组件才能知道每一个访问网关的http请求,到底该访问哪一个具体的RPC服务,在之前的api-gateway-center的数据库设计和注册功能实现都是为了最终能够让对应的访问api-gateway-core的具体请求和对应的RPC服务链接起来,即构建成功最终的HttpStatement。

现在我们已经分别提供好了网关注册中心的注册功能,但是由于我们是将api-gateway-core当作一种类似于Mybatis的数据资源(算力资源),无论是从单一职责原则,还是解耦合方面,发起注册的功能绝不能由api-gateway-core进行执行,因此我们引入了类似SpringBoot starterapi-gateway-assist,对数据资源进行包装,但是从结构图中,我们可以看到我们是由api-gateway-engin这个类似于SpringBoot的东西进行注册网关和拉取接口的能力,而这种详细信息的注册和拉去一定是从api-gateway-assist这个SpringBoot starter加载而来的,我们当初设计api-gateway-assist就是为了让api-gateway-coreapi-gateway-engin解耦,后续两者那方有更改只需要更新starter即可。通常像 MyBatis 的 ORM 框架、RPC 的 Dubbo 服务,在接入到 SpringBoot 都是有自己的 Starter 组件的。因为这些组件可以封装一些共性共用的逻辑处理,一次开发处处使用的目的。所以我们要做这样的处理,方便在网关引擎中启动网关算力。可以想象这是把算力注入到引擎中的纽带开发

所以本章节最大的目的在于搭建起用于封装网关算力服务的api-gateway-core系统为目的,提供网关服务注册发现能力的组件api-gateway-assist,我们希望把这样的统一公用能力进行一致的管理,如果没有这样的组件服务,那么将需要每一个 SpringBoot 服务都要做类似这样的事情,整体来看就会耗费很大的成本,所以要把这样的功能进行收口。如下图(图片来源于xfg

  • api-gateway-core 是网关的算力服务,api-gateway-center 是网关的注册中心,那么为了把这块服务链接起来,中间则需要一套 api-gateway-engin 网关的引擎,用于启动网关的算力服务。

  • 但由于启动网关的算力服务还需要一些功能的整合,来包装网关算力到注册中心的连接,所以这部分需要整合到 api-gateway-assist 这个辅助组件,它是一个 SpringBoot Starter 起到包装和连接的作用。

现在我们来思考下我们应该如何实现starter的自动注册功能,我们现在的需求是,当这个SpringStarter启动时,就会自动向网管注册中心发起注册请求(由于api-gateway-engin还未搭建起来,所以暂时先直接请求到网关注册中心上去,随着后续开发再更改)。

  • 首先通常我们使用一个公用的starter的时候,只需要将相应的依赖添加的Maven的配置文件当中即可,免去了自己需要引用很多依赖类,并且SpringBoot会自动进行类的自动配置。而我们自己开发一个starter也需要做相应的处理。

  • 结合本章节要求就是只要把这个 Starter 作为依赖加进Spring Boot 应用,不用再写任何集成代码,它就会在应用启动结束时,自动把本机的网关信息发到网关中心完成注册,即引入依赖即可生效(零侵入),在应用启动完成后自动把当前网关实例注册到网关中心。

  • SpringBoot 在启动时会去依赖的starter包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于 Java 的 SPI 机制。那我们就可以在这其中进行一个配置类的加载

  • 在加载配置类的过程中引入容器,注册事件监听器,只要某事件触发,就进行注册服务。

  • 即最终底色效果为:引入依赖 → SPI 自动装配生效 → 注册监听器与服务 → 容器刷新完成发布事件 → 自动发起注册请求。这样就无需我们在springboot应用中写一行集成代码。

详细的starter开发可以参考下这篇博客:Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》

详细的有关SPI的知识可以参考博客:自己写的Java SPI梳理 ,其它推荐:Java SPI机制总结系列之开发入门实例Java SPI机制总结系列之万字最详细图解Java SPI机制源码

业务实现

实现再梳理

首先创建好对应的项目api-gateway-assist

我么再仔细梳理下实现流程:

  • springboot启动时会去依赖的starter包中寻找 resources/META-INF/spring.factories 文件,根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于 Java 的 SPI 机制。加载出来了配置类。

  • 在配置类中我们可以注册bean,我们希望是通过事件触发来自动进行注册,那么这个时候就需要至少两个bean,一个用于负责注册的bean,一个用于监听事件的bean。

  • 其次发起注册需要那些信息,这些信息从何而来?我们再设计网关注册中心的数据库表时,关于网关服务的信息有以下信息(如下图),其中服务状态在网关注册中心的注册逻辑中,我们会根据接口的调用自动赋值,也就是说,当前starter只需要提供分组标识,网关标识,网关地址这几个字段属性,但是这里有一个问题,仅有上述三个字段是不知道注册到那个注册中心的,因此这里还需要提供一个网关注册中心的地址字段。最后这些信息从那里读入呢,我们不可能进行硬编码,所以我们需要将这部分内容写入到配置文件中去。而这些信息我们最好绑定到一个配置类中去,并在springboot启动时将其创建为一个bean,以方便注册服务拿到相应的字段。

具体实现

接着上述的梳理,现在就来具体实现这个starter

首先是信息的配置类GatewayServiceProperties利用@ConfigurationProperties注解从配置文件中读取内容

 package com.zshunbao.gateway.assist.config;
 ​
 import org.springframework.boot.context.properties.ConfigurationProperties;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName GatewayServiceProperties
  * @description: 网关配置服务--主要是针对api-gateway-center的网关明细表多了一个 api-gateway-center注册中心地址的属性
  * @author: zs宝
  * @create: 2025-08-21 15:13
  * @Version 1.0
  **/
 @ConfigurationProperties("api-gateway")
 public class GatewayServiceProperties {
     /** 注册中心地址 */
     private String address;
     /** 分组ID */
     private String groupId;
     /** 网关ID */
     private String gatewayId;
     /** 网关名称 */
     private String gatewayName;
     /** 网关地址 */
     private String gatewayAddress;
 ​
     public String getAddress() {
         return address;
     }
 ​
     public void setAddress(String address) {
         this.address = address;
     }
 ​
     public String getGroupId() {
         return groupId;
     }
 ​
     public void setGroupId(String groupId) {
         this.groupId = groupId;
     }
 ​
     public String getGatewayId() {
         return gatewayId;
     }
 ​
     public void setGatewayId(String gatewayId) {
         this.gatewayId = gatewayId;
     }
 ​
     public String getGatewayName() {
         return gatewayName;
     }
 ​
     public void setGatewayName(String gatewayName) {
         this.gatewayName = gatewayName;
     }
 ​
     public String getGatewayAddress() {
         return gatewayAddress;
     }
 ​
     public void setGatewayAddress(String gatewayAddress) {
         this.gatewayAddress = gatewayAddress;
     }
 ​
 }
 ​

然后是提供注册功能的类

RegisterGatewayService.java

 package com.zshunbao.gateway.assist.service;
 ​
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.zshunbao.gateway.assist.GatewayException;
 import com.zshunbao.gateway.assist.common.Result;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName RegisterGatewayService
  * @description: 网关注册服务
  * @author: zs宝
  * @create: 2025-08-21 15:17
  * @Version 1.0
  **/
 public class RegisterGatewayService {
     private Logger logger = LoggerFactory.getLogger(RegisterGatewayService.class);
 ​
     /**
      * 进行向api-gateway-center网关注册中心发起注册的服务
      * @param address 注册中心地址
      * @param groupId api-gateway-core分组ID
      * @param gatewayId 网关ID
      * @param gatewayName 网关名称
      * @param gatewayAddress 网关地址
      */
     public void doRegister(String address, String groupId, String gatewayId, String gatewayName, String gatewayAddress) {
         //封装请求参数
         Map<String,Object> paramMap=new HashMap<>();
         paramMap.put("groupId", groupId);
         paramMap.put("gatewayId", gatewayId);
         paramMap.put("gatewayName", gatewayName);
         paramMap.put("gatewayAddress", gatewayAddress);
         //调用hutool工具包发送请求
         String resultStr = HttpUtil.post(address, paramMap, 350);
         //将返回结果用自定义的结果类包装
         Result result = JSON.parseObject(resultStr, Result.class);
         logger.info("向网关中心注册网关算力服务 gatewayId:{} gatewayName:{} gatewayAddress:{} 注册结果:{}", gatewayId, gatewayName, gatewayAddress, resultStr);
         if(!"0000".equals(result.getCode())){
             throw new GatewayException("网关服务注册异常 [gatewayId:" + gatewayId + "] 、[gatewayAddress:" + gatewayAddress + "]");
         }
     }
 ​
 }
 ​
  • 在 RegisterGatewayService 注册网关类中,调用 api-gateway-center 提供的服务发现接口,向网关中心注册网关算力节点。这是第一步非常重要的关联作用,有了这块逻辑的处理,才能打通整个网关算力和网关注册中心

事件监听类GatewayApplication.java,这里我们希望在容器刷新完成事件 refresh时完成注册

 package com.zshunbao.gateway.assist.application;
 ​
 import com.zshunbao.gateway.assist.config.GatewayServiceProperties;
 import com.zshunbao.gateway.assist.service.RegisterGatewayService;
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextRefreshedEvent;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName GatewayApplication
  * @description: 网关应用;与 Spring 链接,调用网关注册和接口拉取
  * @author: zs宝
  * @create: 2025-08-21 15:26
  * @Version 1.0
  **/
 public class GatewayApplication implements ApplicationListener<ContextRefreshedEvent> {
     private GatewayServiceProperties properties;
     private RegisterGatewayService registerGatewayService;
 ​
     public GatewayApplication(GatewayServiceProperties properties, RegisterGatewayService registerGatewayService) {
         this.properties = properties;
         this.registerGatewayService = registerGatewayService;
     }
 ​
     //当所有容器加载完毕时,即监听到了上下文事件更新,然后调用注册方法,向网关注册中心发起注册
     @Override
     public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
         // 1. 注册网关服务;每一个用于转换 HTTP 协议泛化调用到 RPC 接口的网关都是一个算力,这些算力需要注册网关配置中心
         registerGatewayService.doRegister(properties.getAddress(),
                 properties.getGroupId(),
                 properties.getGatewayId(),
                 properties.getGatewayName(),
                 properties.getGatewayAddress());
     }
 }

最后是相关的配置类GatewayAutoConfig.java

 package com.zshunbao.gateway.assist.config;
 ​
 import com.zshunbao.gateway.assist.application.GatewayApplication;
 import com.zshunbao.gateway.assist.service.RegisterGatewayService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName GatewayAutoConfig
  * @description: 网关配置服务,这里利用了Java的 SPI机制进行bean创建
  * @author: zs宝
  * @create: 2025-08-21 15:30
  * @Version 1.0
  **/
 @Configuration
 @EnableConfigurationProperties(GatewayServiceProperties.class)
 public class GatewayAutoConfig {
     private Logger logger = LoggerFactory.getLogger(GatewayAutoConfig.class);
 ​
     @Bean
     public RegisterGatewayService registerGatewayService(){
         return new RegisterGatewayService();
     }
 ​
     @Bean
     public GatewayApplication gatewayApplication(RegisterGatewayService registerGatewayService,GatewayServiceProperties properties){
         return  new GatewayApplication(properties,registerGatewayService);
     }
 }
 ​

相关的spring.factories文件

 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zshunbao.gateway.assist.config.GatewayAutoConfig

最后还有一些自定义的封装返回结果和错误类

Result.java

 package com.zshunbao.gateway.assist.common;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName Result
  * @description: 统一返回对象中,Code码、Info描述
  * @author: zs宝
  * @create: 2025-08-21 15:11
  * @Version 1.0
  **/
 public class Result {
     private String code;
     private String info;
 ​
     public String getCode() {
         return code;
     }
 ​
     public void setCode(String code) {
         this.code = code;
     }
 ​
     public String getInfo() {
         return info;
     }
 ​
     public void setInfo(String info) {
         this.info = info;
     }
 ​
 }
 ​

GatewayException.java

 package com.zshunbao.gateway.assist;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName GatewayException
  * @description:
  * @author: zs宝
  * @create: 2025-08-21 15:25
  * @Version 1.0
  **/
 public class GatewayException extends RuntimeException{
     public GatewayException(String msg){
         super(msg);
     }
     public GatewayException(String msg, Throwable cause) {
         super(msg, cause);
     }
 }
 ​

相关pom文件

 <?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 ​
     <groupId>com.zshunbao</groupId>
     <artifactId>api-gateway-assist-01</artifactId>
     <version>1.0-SNAPSHOT</version>
 ​
     <packaging>jar</packaging>
 ​
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
         <version>2.3.5.RELEASE</version>
         <relativePath/>
     </parent>
 ​
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
             <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-autoconfigure</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-aop</artifactId>
         </dependency>
         <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
         <dependency>
             <groupId>org.mybatis.spring.boot</groupId>
             <artifactId>mybatis-spring-boot-starter</artifactId>
             <version>2.1.4</version>
         </dependency>
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>5.1.34</version>
         </dependency>
         <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-test -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-test</artifactId>
             <scope>test</scope>
         </dependency>
         <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-test</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>commons-beanutils</groupId>
             <artifactId>commons-beanutils</artifactId>
             <version>1.9.4</version>
         </dependency>
         <dependency>
             <groupId>commons-lang</groupId>
             <artifactId>commons-lang</artifactId>
             <version>2.6</version>
         </dependency>
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
             <version>1.2.75</version>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
             <scope>test</scope>
         </dependency>
         <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
             <version>5.8.10</version>
         </dependency>
     </dependencies>
 ​
     <build>
         <finalName>api-gateway-assist</finalName>
         <resources>
             <resource>
                 <directory>src/main/resources</directory>
                 <filtering>true</filtering>
                 <includes>
                     <include>**/**</include>
                 </includes>
             </resource>
         </resources>
         <testResources>
             <testResource>
                 <directory>src/test/resources</directory>
                 <filtering>true</filtering>
                 <includes>
                     <include>**/**</include>
                 </includes>
             </testResource>
         </testResources>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <version>2.12.4</version>
                 <configuration>
                     <skipTests>true</skipTests>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>2.5</version>
                 <configuration>
                     <encoding>${project.build.sourceEncoding}</encoding>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>2.3.2</version>
                 <configuration>
                     <source>1.8</source>
                     <target>1.8</target>
                     <encoding>${project.build.sourceEncoding}</encoding>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-source-plugin</artifactId>
                 <version>2.1.2</version>
                 <executions>
                     <execution>
                         <id>attach-sources</id>
                         <goals>
                             <goal>jar</goal>
                         </goals>
                     </execution>
                 </executions>
             </plugin>
         </plugins>
     </build>
 ​
 </project>

测试

 package com.zshunbao.gateway.assist.test;
 ​
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.zshunbao.gateway.assist.common.Result;
 import org.junit.Test;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @program: api-gateway-assist
  * @ClassName ApiTest
  * @description:
  * @author: zs宝
  * @create: 2025-08-21 15:39
  * @Version 1.0
  **/
 public class ApiTest {
     public static void main(String[] args) {
         System.out.println("Hi Api Gateway");
     }
 ​
     @Test
     public void test_register_gateway() {
         Map<String, Object> paramMap = new HashMap<>();
         paramMap.put("groupId", "10001");
         paramMap.put("gatewayId", "api-gateway-g4");
         paramMap.put("gatewayName", "电商配送网关");
         paramMap.put("gatewayAddress", "127.0.0.1");
 ​
         String resultStr = HttpUtil.post("http://localhost:8001/wg/admin/config/registerGateway", paramMap, 600);
         System.out.println(resultStr);
 ​
         Result result = JSON.parseObject(resultStr, Result.class);
         System.out.println(result.getCode());
     }
 }
 ​

记得测试时启动网关注册中心api-gateway-center

网关注册中心打印日志如下

参考资料