业务流程

在上一章中,我们已经完成了api-gateway-core,api-gateway-assist,api-gateway-engin这一套有关网关服务的组合,在其中,向网关注册中心注册网关服务,并拉取相关的配置。但是网关注册中心在之前我我们定义到,它急需要关联网关服务,也需要关联RPC服务。现在网关相关的服务一套已经基本完成,那么接下来我们就要完成有关RPC服务的。如上图,我们在设计API网关项目的时候,对于RPC服务,我们是希望,它引入api-gateway-sdk组件,一旦服务启动,就将RPC服务的相关信息注册到网关注册中心api-gateway-center(其实这样来看的话,api-gateway-sdk是有些类似于一个starter的)。无论是网关注册中心的功能,还是网关算力模块的使用,都需要使用到RPC服务端的相关信息。而这部分信息就是网关注册中心有关RPC服务的三章表的相关属性字段信息。如下图

那么该如何设计这个组件呢?如何达到我们想要的功能呢?联系前面这个api-gateway-assist这个starter,在某些bean创建过程中完成功能。现在我们这个组件需要提取系统和接口方法的相关信息,重点在于提取那些接口方法,如何拿到其相关的信息。

这里我们以以下方式设计组件api-gateway-sdk:应用注册组件的目的就是提供给RPC接口生产的服务使用,通过RPC生产者服务引入SDK组件,并使用注解配置的方式作为接口标记。当服务启动的时候,SDK组件会采集这些被标记了注解的接口和方法,把这些信息收集后向服务端注册。如下图

  • 开发 api-gateway-sdk 组件,获取 Spring Bean 对象的注册结果。并对已经使用注解标记了的接口进行拦截提取接口和方法信息

  • 本章节暂时只搭建大致骨架,使用自定义接口拿到相关的系统,接口,方法信息,注册功能将在后续章节实现。

业务实现

项目结构如下:

设计梳理

业务流程中我们说过,我们将以自定义注解的方式来提取RPC服务端的相关信息,而对应的我那个管注册中心分别有系统,接口,方法三张表。那么我们需要定义三个注解吗?(这块的设计很多都可以参考到api-gateway-assist这个starter)

对于整个RPC服务系统的信息,系统信息我们无法创造一个注解直接标注在整个系统上面,而系统信息往往应该是由设计研发自定义的系统名,系统id,系统类型,注册中心地址等,这些东西无法做注解直接标注,且大多数一定是产品研发预定好的。这块信息在实际开发中由于内部预定好了,所有大部分时候并不会直接在代码中体现出来(都是公司团队内部的默认),但是由于这里我们这里只是模仿做的一个项目,所以我们尽量不让其在代码中出现,将其信息放置在配置文件中。

而对于另外两张表的信息,接口,方法,就可以设计自定义注解进行标注。而这些注解中的需要的信息就是对应数据库表的信息,在引入组件后,开发人员在对应接口方法上添加注解,将接口方法信息填入。

当时我们这个组件是要被RPC服务依赖引入,我们希望这个东西尽量做到零侵入,可插拔,而这恰好是Java SPI的机制,spring对其有专门的实现(META-INF/spring.factories)。

具体实现

首先是整个系统的信息,我么定义一个配置属性类来保存对应的信息,并让其与配置文件关联,同时在系统初始化后,也将其构建为一个bean,方便信息的拿取。这里面有一个字段address,表示网关注册中心地址,这本来是系统对应的库表没有的,但是这个东西是研发呢不默认的,也是后面需要的,我们将其和系统信息放在一起,从配置文件中获取

GatewaySDKServiceProperties

 package com.zshunbao.gateway.sdk.config;
 ​
 import org.springframework.boot.context.properties.ConfigurationProperties;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName GatewaySDKServiceProperties
  * @description: 定义整个服务系统相关配置,这种配置一般都是既定好的,即就是application_system表的相关内容
  * @author: zs宝
  * @create: 2025-08-29 16:18
  * @Version 1.0
  **/
 ​
 @ConfigurationProperties("api-gateway-sdk")
 public class GatewaySDKServiceProperties {
     /** 网关注册中心地址 */
     private String address;
     /** 系统标识 */
     private String systemId;
     /** 系统名称 */
     private String systemName;
     /** RPC注册中心;zookeeper://127.0.0.1:2181*/
     private String systemRegistry;
 ​
     public String getAddress() {
         return address;
     }
 ​
     public void setAddress(String address) {
         this.address = address;
     }
 ​
     public String getSystemId() {
         return systemId;
     }
 ​
     public void setSystemId(String systemId) {
         this.systemId = systemId;
     }
 ​
     public String getSystemName() {
         return systemName;
     }
 ​
     public void setSystemName(String systemName) {
         this.systemName = systemName;
     }
 ​
     public String getSystemRegistry() {
         return systemRegistry;
     }
 ​
     public void setSystemRegistry(String systemRegistry) {
         this.systemRegistry = systemRegistry;
     }
 }
 ​

系统不需要自定义注解,但是有关接口和方法却需要自定义注解

ApiProducerClazz.java,有关接口

 package com.zshunbao.gateway.sdk.annotation;
 ​
 import java.lang.annotation.*;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName ApiProducerClazz
  * @description: 网关API 生产者自定义注解,用于接口上
  * @author: zs宝
  * @create: 2025-08-29 16:08
  * @Version 1.0
  **/
 ​
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE})
 public @interface ApiProducerClazz {
     /** 接口名称 */
     String interfaceName() default "";
 ​
     /** 接口版本 */
     String interfaceVersion() default "";
 }
 ​

ApiProducerMethod.java,有关方法

 package com.zshunbao.gateway.sdk.annotation;
 ​
 import java.lang.annotation.*;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName ApiProducerMethod
  * @description: 网关API 生产者自定义注解,用于方法上
  * @author: zs宝
  * @create: 2025-08-29 16:12
  * @Version 1.0
  **/
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
 public @interface ApiProducerMethod {
     /** 方法名称 */
     String methodName() default "";
     /** 访问路径;/wg/activity/sayHi */
     String uri() default "";
     /** 接口类型;GET、POST、PUT、DELETE */
     String httpCommandType() default "GET";
     /** 是否认证;true = 1是、false = 0否 */
     int auth() default 0;
 }
 ​
  • 上述有关方法的自定义注解,相比网关注册中心对应应用系统方法的表application_interface_method少了一个方法参数类型的字段?这是由于我们自定义注解,是希望有开发人员在开发接口方法时由开发人员在注解中填写相关信息,以便获取,但是方法参数类型的字段填写起来较为复杂(填的东西又多,还涉及到每个参数的类型路径,容易错),且容易出错,因此这项信息倒不如动态加载获得。

在设计完后系统以及相关注解后,接下来就是这个组件被引入的时候,当服务启动,它该在什么时候获取信息,如何拿到想要的信息。

这就是GatewaySDKApplication.java要做的事情

 package com.zshunbao.gateway.sdk.application;
 ​
 import com.zshunbao.gateway.sdk.annotation.ApiProducerClazz;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerMethod;
 import com.zshunbao.gateway.sdk.config.GatewaySDKServiceProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.config.BeanPostProcessor;
 ​
 import java.lang.reflect.Method;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName GatewaySDKApplication
  * @description: 应用服务注册
  * @author: zs宝
  * @create: 2025-08-29 16:24
  * @Version 1.0
  **/
 public class GatewaySDKApplication implements BeanPostProcessor {
     private Logger logger = LoggerFactory.getLogger(GatewaySDKApplication.class);
     private GatewaySDKServiceProperties properties;
 ​
     public GatewaySDKApplication(GatewaySDKServiceProperties properties) {
         this.properties = properties;
     }
 ​
     @Override
     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
         //先查看bean中是否有网关专门标注接口的注解
         ApiProducerClazz apiProducerClazz = bean.getClass().getAnnotation(ApiProducerClazz.class);
         //如果没有相关注解,直接返回bean即可
         if(apiProducerClazz==null){
             return bean;
         }
         //有网关标注在接口上的注解,那么就一定有相应的标注方法的接口,接下来,我们要将其找到,取出对应的信息
         //先从字节码中拿到所有方法
         Method[] methods = bean.getClass().getMethods();
         //挨个遍历方法寻找有无标注网关在方法上的注解
         for(Method method:methods){
             ApiProducerMethod apiProducerMethod = method.getAnnotation(ApiProducerMethod.class);
             //若没有,则直接下一个
             if(apiProducerMethod==null){
                 continue;
             }
             //系统信息
             logger.info("\n应用注册:系统信息 \nsystemId: {} \nsystemName: {} \nsystemType: {} \nsystemRegistry: {}",
                     properties.getSystemId(),
                     properties.getSystemName(),
                     "RPC",
                     properties.getSystemRegistry()
             );
             //接口信息
             logger.info("\n应用注册:接口信息 \nsystemId: {} \ninterfaceId: {} \ninterfaceName: {} \ninterfaceVersion: {}",
                     properties.getSystemId(),
                     bean.getClass().getName(),
                     apiProducerClazz.interfaceName(),
                     apiProducerClazz.interfaceVersion()
             );
             //如果有,提取这个方法的相关信息
             //但其实这个方法的相关信息,我们在注解中就已近标注完成,只需要从注解中拿就可以了
             //但是我们在网关后续利用对RPC进行泛化调用的时候,还需要一个方法中的参数类型信息,这个是没有在注解中标注的
             //需要我们动态提取
             Class<?>[] parameterTypes = method.getParameterTypes();
             StringBuilder parameters = new StringBuilder();
             for(Class<?> clazz:parameterTypes){
                 //将每个参数类型名用字符串存储,中间用","隔开
                 parameters.append(clazz.getName()).append(",");
             }
             //将收集到的参数类型名字符串,根据","做拆分(由于最后多了一个","号)
             String parameterType = parameters.toString().substring(0, parameters.toString().lastIndexOf(","));
             //方法信息
             logger.info(
                     "\n应用注册:方法信息 \nsystemId: {} \ninterfaceId: {} \nmethodId: {} \nmethodName: {} \nparameterType: {} \nuri: {} \nhttpCommandType: {} \nauth: {}",
                     properties.getSystemId(),
                     bean.getClass().getName(),
                     method.getName(),
                     apiProducerMethod.methodName(),
                     parameterType,
                     apiProducerMethod.uri(),
                     apiProducerMethod.httpCommandType(),
                     apiProducerMethod.auth()
             );
         }
         return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
     }
 }
 ​
  • 上述类继承了BeanPostProcessor类(BeanPostProcessor 不是普通的 Bean,它是 Spring 容器的一个扩展点,用于监听和干预其他 Bean 的创建过程),Spring 容器内部维护了一个 BeanPostProcessor 的列表,这个容器内部的的bean会在bean创建过程中调用对应的钩子方法,我们创建了实现类,就会优先使用我们的实现类,这种设计使得 SDK 能够"零侵入"地自动发现和注册服务,无需手动配置

  • BeanPostProcessor 是"观察者":它观察其他 Bean 的创建过程

  • GatewaySDKApplication 实现了 BeanPostProcessor:所以它能观察所有其他 Bean

  • Spring 自动注册:实现了 BeanPostProcessor 的 Bean 会被自动添加到处理列表中

  • 处理时机:每当创建任何 Bean 时,都会调用所有 BeanPostProcessor 的方法

  • Spring 容器机制:每创建一个 Bean 都会调用所有 BeanPostProcessor

    • Object bean 参数代表:

      • 当前正在初始化的 Bean 实例

      • 可能是任何类型的业务服务

      • 通过反射获取类型信息和注解

其次最后就是一个配置类,要利用spring 实现的Java SPI机制来创建我们定义的bean,以便后续在每个服务bean创建过程中收集每个接口方法的信息.GatewaySDKAutoConfig

 package com.zshunbao.gateway.sdk.config;
 ​
 import com.zshunbao.gateway.sdk.application.GatewaySDKApplication;
 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-sdk
  * @ClassName GatewaySDKAutoConfig
  * @description: 网关sdk配置服务
  * @author: zs宝
  * @create: 2025-08-29 16:25
  * @Version 1.0
  **/
 @Configuration
 @EnableConfigurationProperties(GatewaySDKServiceProperties.class)
 public class GatewaySDKAutoConfig {
     private Logger logger = LoggerFactory.getLogger(GatewaySDKAutoConfig.class);
     @Bean
     public GatewaySDKApplication gatewaySDKApplication(GatewaySDKServiceProperties properties){
         logger.info("构建 GatewaySDKApplication bean");
         return new GatewaySDKApplication(properties);
     }
 }
 ​

spring.factories

 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zshunbao.gateway.sdk.config.GatewaySDKAutoConfig

最后的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.gateway</groupId>
     <artifactId>api-gateway-sdk-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.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>
 ​
     </dependencies>
 ​
     <build>
         <finalName>api-gateway-sdk</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>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
                 <executions>
                     <execution>
                         <phase>package</phase>
                         <goals>
                             <goal>shade</goal>
                         </goals>
                     </execution>
                 </executions>
                 <configuration>
                     <artifactSet>
                         <includes>
                             <include>com.zshunbao.gateway:api-gateway-core-09:jar:</include>
                         </includes>
                     </artifactSet>
                 </configuration>
             </plugin>
 ​
         </plugins>
     </build>
 ​
 </project>

测试

这里的测试我们,由于是要其它服务引入我们设计的组件,因此我们再创建一个模块进行专门的测试,结构如下

其中的pom文件中引入我们设计的组件

 <!-- 网关应该服务注册组件 -->
         <dependency>
             <groupId>com.zshunbao.gateway</groupId>
             <artifactId>api-gateway-sdk-01</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>

然后随便定义了一个接口和方法

 package com.zshunbao.gateway.sdk.interfaces;
 ​
 import com.zshunbao.gateway.sdk.annotation.ApiProducerClazz;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerMethod;
 import org.springframework.stereotype.Service;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName UserService
  * @description: 用户服务
  * @author: zs宝
  * @create: 2025-08-29 17:07
  * @Version 1.0
  **/
 @Service
 @ApiProducerClazz(interfaceName = "用户服务",interfaceVersion = "1.0.0")
 public class UserService {
     @ApiProducerMethod(methodName = "探测", uri = "/wg/user/hi", httpCommandType = "POST", auth = 1)
     public String hi(String str) {
         return "hi " + str + " by api-gateway-sdk";
     }
 }
 ​

最后还有一个最基础的springboot启动类

 package com.zshunbao.gateway.sdk;
 ​
 import org.springframework.beans.factory.annotation.Configurable;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName Application
  * @description: 测试启动服务
  * @author: zs宝
  * @create: 2025-08-29 17:07
  * @Version 1.0
  **/
 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
 @Configurable
 public class Application {
 ​
     public static void main(String[] args) {
         SpringApplication.run(Application.class, args);
     }
 ​
 }
 ​

定义配置文件application.yml

 server:
   port: 8004
 ​
 api-gateway-sdk:
   address: http://localhost:8001  # 注册中心;从这里获取接口信息以及完成注册网关操作
   systemId: api-gateway-sdk-00
   systemName: 网关sdk测试工程
   systemRegistry: zookeeper://192.1 68.1.105:2181

启动测试项目

流程如下

 步骤1: Spring 启动,扫描配置类
    ↓
 步骤2: 发现 GatewaySDKAutoConfig 类
    ↓
 步骤3: 创建 GatewaySDKServiceProperties Bean
    ↓
 步骤4: 创建 GatewaySDKApplication Bean
    ↓
 步骤5: Spring 发现 GatewaySDKApplication 实现了 BeanPostProcessor
    ↓
 步骤6: 将 GatewaySDKApplication 添加到 BeanPostProcessor 列表
    ↓
 步骤7: 继续创建其他业务 Bean(如 UserService)
    ↓
 步骤8: 创建 UserService 时,调用所有 BeanPostProcessor 的 postProcessAfterInitialization
    ↓
 步骤9: GatewaySDKApplication.postProcessAfterInitialization 被调用,处理 UserService

参考资料