业务流程

在上一章节中,我们的sdk组件已经完成了从接口和方法上收集到相关信息的功能。而所有的相关信息都是需要注册到网关注册中心上面去的,本节我们就来补充将相关信息注册到网关注册中心上去的功能。其开发与api-gateway-assist十分类似。

  • 结合着第20章采集到的服务信息,这里把这些服务信息,向网关中心注册。

  • 通常提供服务接口的实现类,只会有一个接口以及对应的实现类。如果有多个接口会抛出异常提醒。

业务实现

实现思路

实现项目结构如下

首先我们在网关注册中心的对于服务端的注册方法接口如下图

可以看到针对网关注册中心针对服务端的注册有系统,接口,方法几个类,每个类的参数情况和,返回结果都很清楚,因此我们的api-gateway-sdk组件在发起注册时,一定要针对其返回结果和参数进行详细的比对。

首先我们已经在上一节中拿到了相关的调用网关注册中心的注册http请求的所有参数,现在我们需要的就是在任意服务项目引入我们的sdk组件后可以某一个时刻,对注册中心发起注册的http请求,根据接收到的返回结果判断注册是否成功。

上一章,已经拿到了请求所需的参数信息,这一章在真正注册时,只需要调用相关的注册方法即可,并接受到请求的响应结果。

具体实现

首先我们需要一个结果包装类用于接受http请求返回的结果,如下

 package com.zshunbao.gateway.sdk.common;
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName Result
  * @description: 统一返回对象中,Code码、Info描述
  * @author: zs宝
  * @create: 2025-09-01 17:10
  * @Version 1.0
  **/
 public class Result<T> {
     private String code;
     private String info;
     private T data;
 ​
     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;
     }
 ​
     public T getData() {
         return data;
     }
 ​
     public void setData(T data) {
         this.data = data;
     }
 }
 ​

在有了这个后,我们就需要一个服务类,这个服务类用于向注册中心发起注册请求,并接受结果。GatewayCenterService.java

 package com.zshunbao.gateway.sdk.domain.service;
 ​
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
 import com.zshunbao.gateway.sdk.GatewayException;
 import com.zshunbao.gateway.sdk.common.Result;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 ​
 /**
  * @program: api-gateway-sdk
  * @ClassName GatewayCenterService
  * @description: 网关中心服务,向网关注册中心发起注册
  * @author: zs宝
  * @create: 2025-09-01 17:13
  * @Version 1.0
  **/
 public class GatewayCenterService {
     private Logger logger = LoggerFactory.getLogger(GatewayCenterService.class);
 ​
     /**
      * 注册系统
      * @param address
      * @param systemId
      * @param systemName
      * @param systemType
      * @param systemRegistry
      */
     public void doRegisterApplication( String address,String systemId,
                                        String systemName,
                                        String systemType,
                                        String systemRegistry){
         Map<String, Object> paramMap=new HashMap<>();
         paramMap.put("systemId",systemId);
         paramMap.put("systemName",systemName);
         paramMap.put("systemType",systemType);
         paramMap.put("systemRegistry",systemRegistry);
 ​
         String resultStr;
         try{
             resultStr= HttpUtil.post(address+"/wg/admin/register/registerApplication",paramMap,1200);
         }catch (Exception e){
             logger.error("应用服务接口注册异常,链接资源不可用:{}", address + "/wg/admin/register/registerApplication");
             throw e;
         }
         Result<Boolean> result = JSON.parseObject(resultStr, new TypeReference<Result<Boolean>>() {
         });
         logger.info("向网关中心注册应用服务 systemId:{} systemName:{} 注册结果:{}", systemId, systemName, resultStr);
         if(!result.getCode().equals("0000") && result.getCode().equals("0003")){
             throw new GatewayException("注册应用服务异常 [systemId:" + systemId + "] 、[systemRegistry:" + systemRegistry + "]");
         }
     }
 ​
     /**
      * 注册接口
      * @param address
      * @param systemId
      * @param interfaceId
      * @param interfaceName
      * @param interfaceVersion
      */
     public void doRegisterApplicationInterface(String address,String systemId,
                                                String interfaceId,
                                                String interfaceName,
                                                String interfaceVersion){
         Map<String,Object>paramMap=new HashMap<>();
         paramMap.put("systemId",systemId);
         paramMap.put("interfaceId",interfaceId);
         paramMap.put("interfaceName",interfaceName);
         paramMap.put("interfaceVersion",interfaceVersion);
         String resultStr;
         try {
             resultStr=HttpUtil.post(address+"/wg/admin/register/registerApplicationInterface",paramMap,1200);
         }catch (Exception e){
             logger.error("应用服务接口注册异常,链接资源不可用:{}", address + "/wg/admin/register/registerApplicationInterface");
             throw e;
         }
         Result<Boolean> result = JSON.parseObject(resultStr, new TypeReference<Result<Boolean>>() {
         });
         logger.info("向网关中心注册应用接口服务 systemId:{} interfaceId:{} interfaceName:{} 注册结果:{}", systemId, interfaceId, interfaceName, resultStr);
         if (!"0000".equals(result.getCode()) && !"0003".equals(result.getCode()))
             throw new GatewayException("向网关中心注册应用接口服务异常 [systemId:" + systemId + "] 、[interfaceId:" + interfaceId + "]");
     }
 ​
     /**
      * 注册接口的具体方法
      * @param address
      * @param systemId
      * @param interfaceId
      * @param methodId
      * @param methodName
      * @param parameterType
      * @param uri
      * @param httpCommandType
      * @param auth
      */
     public void doRegisterApplicationInterfaceMethod(String address,String systemId,
                                                      String interfaceId,
                                                      String methodId,
                                                      String methodName,
                                                      String parameterType,
                                                      String uri,
                                                      String httpCommandType,
                                                      Integer auth){
         Map<String,Object>paramMap=new HashMap<>();
         paramMap.put("systemId", systemId);
         paramMap.put("interfaceId", interfaceId);
         paramMap.put("methodId", methodId);
         paramMap.put("methodName", methodName);
         paramMap.put("parameterType", parameterType);
         paramMap.put("uri", uri);
         paramMap.put("httpCommandType", httpCommandType);
         paramMap.put("auth", auth);
 ​
         String resultStr;
 ​
         try {
             resultStr=HttpUtil.post(address+"/wg/admin/register/registerApplicationInterfaceMethod",paramMap,1200);
         }catch (Exception e){
             logger.error("应用服务接口注册方法异常,链接资源不可用:{}", address + "/wg/admin/register/registerApplicationInterfaceMethod");
             throw e;
         }
         Result<Boolean> result = JSON.parseObject(resultStr, new TypeReference<Result<Boolean>>() {
         });
         logger.info("向网关中心注册应用接口方法服务 systemId:{} interfaceId:{} methodId:{} 注册结果:{}", systemId, interfaceId, methodId, resultStr);
         if (!"0000".equals(result.getCode()) && !"0003".equals(result.getCode()))
             throw new GatewayException("向网关中心注册应用接口方法服务异常 [systemId:" + systemId + "] 、[interfaceId:" + interfaceId + "]、[methodId:]" + methodId + "]");
 ​
 ​
     }
 }
 ​
  • 可以看到上述注册方法的大致思路几乎与api-gateway-assist中的网关通信服务注册几乎一致

  • 都需要对应的网关注册中心服务注册的参数以及网关中心的地址。

在有了这个服务之后,我们需要利用spring 实现的Java SPI机制将这个服务构造为bean容器以方便应用使用,即GatewaySDKAutoConfig.java

 package com.zshunbao.gateway.sdk.config;
 ​
 import com.zshunbao.gateway.sdk.application.GatewaySDKApplication;
 import com.zshunbao.gateway.sdk.domain.service.GatewayCenterService;
 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 GatewayCenterService gatewayCenterService(){
         return new GatewayCenterService();
     }
 ​
     @Bean
     public GatewaySDKApplication gatewaySDKApplication(GatewaySDKServiceProperties properties,GatewayCenterService gatewayCenterService) {
         logger.info("构建 GatewaySDKApplication bean");
         return new GatewaySDKApplication(properties,gatewayCenterService);
     }
 }
 ​
  • 利用Java spi的机制构造了服务和应用的bean

最后,我们要在上一章的应用中,一旦需要的信息收集完成,就调用对应的服务方法进行注册GatewaySDKApplication

 package com.zshunbao.gateway.sdk.application;
 ​
 import com.alibaba.fastjson.JSON;
 import com.zshunbao.gateway.sdk.GatewayException;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerClazz;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerMethod;
 import com.zshunbao.gateway.sdk.config.GatewaySDKServiceProperties;
 import com.zshunbao.gateway.sdk.domain.service.GatewayCenterService;
 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;
     private GatewayCenterService gatewayCenterService;
 ​
     public GatewaySDKApplication(GatewaySDKServiceProperties properties, GatewayCenterService gatewayCenterService) {
         this.properties = properties;
         this.gatewayCenterService = gatewayCenterService;
     }
 ​
     @Override
     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
         //先查看bean中是否有网关专门标注接口的注解
         ApiProducerClazz apiProducerClazz = bean.getClass().getAnnotation(ApiProducerClazz.class);
         //如果没有相关注解,直接返回bean即可
         if(apiProducerClazz==null){
             return bean;
         }
         //注册系统信息
         logger.info("\n应用注册:系统信息 \nsystemId: {} \nsystemName: {} \nsystemType: {} \nsystemRegistry: {}", properties.getSystemId(), properties.getSystemName(), "RPC", properties.getSystemRegistry());
         gatewayCenterService.doRegisterApplication(properties.getAddress(),
                 properties.getSystemId(),
                 properties.getSystemName(),
                 "RPC",
                 properties.getSystemRegistry());
 ​
         //注册接口信息
         Class<?>[] interfaces = bean.getClass().getInterfaces();
         if(interfaces.length!=1){
             throw new GatewayException(bean.getClass().getName() + "interfaces not one this is " + JSON.toJSONString(interfaces));
         }
         String interfaceId = interfaces[0].getName();
         logger.info("\n应用注册:接口信息 \nsystemId: {} \ninterfaceId: {} \ninterfaceName: {} \ninterfaceVersion: {}", properties.getSystemId(), interfaceId, apiProducerClazz.interfaceName(), apiProducerClazz.interfaceVersion());
         gatewayCenterService.doRegisterApplicationInterface(properties.getAddress(),
                 properties.getSystemId(),
                 interfaceId,
                 apiProducerClazz.interfaceName(),
                 apiProducerClazz.interfaceVersion());
         //有网关标注在接口上的注解,那么就一定有相应的标注方法的接口,接下来,我们要将其找到,取出对应的信息
         //先从字节码中拿到所有方法
         Method[] methods = bean.getClass().getMethods();
         //挨个遍历方法寻找有无标注网关在方法上的注解
         for(Method method:methods){
             ApiProducerMethod apiProducerMethod = method.getAnnotation(ApiProducerMethod.class);
             //若没有,则直接下一个
             if(apiProducerMethod==null){
                 continue;
             }
 ​
             //如果有,提取这个方法的相关信息
             //但其实这个方法的相关信息,我们在注解中就已近标注完成,只需要从注解中拿就可以了
             //但是我们在网关后续利用对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()
             );
             gatewayCenterService.doRegisterApplicationInterfaceMethod(properties.getAddress(),
                     properties.getSystemId(),
                     interfaceId,
                     method.getName(),
                     apiProducerMethod.methodName(),
                     parameterType,
                     apiProducerMethod.uri(),
                     apiProducerMethod.httpCommandType(),
                     apiProducerMethod.auth());
         }
         return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
     }
 }
 ​
  • 分别在系统,接口,方法的信息收集完成后,调用了对应的注册服务

测试

最后,这次测试我们可以将我们的组件放于一个真正的PRC服务上进行使用,如下,这个测试项目来源于 api-gateway-test-provider

 package cn.bugstack.gateway.interfaces;
 ​
 import cn.bugstack.gateway.rpc.IActivityBooth;
 import cn.bugstack.gateway.rpc.dto.XReq;
 ​
 import com.alibaba.fastjson.JSON;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerClazz;
 import com.zshunbao.gateway.sdk.annotation.ApiProducerMethod;
 import org.apache.dubbo.config.annotation.Service;
 ​
 // 前面章节不涉及到上报服务,需要把以下配置注释掉 ApiProducerClazz、ApiProducerMethod 去掉。
 @Service(version = "1.0.0")
 @ApiProducerClazz(interfaceName = "活动服务", interfaceVersion = "1.0.0")
 public class ActivityBooth implements IActivityBooth {
 ​
     @Override
     @ApiProducerMethod(methodName = "探活方法", uri = "/wg/activity/sayHi", httpCommandType = "GET", auth = 0)
     public String sayHi(String str) {
         return "hi " + str + " by api-gateway-test-provider";
     }
 ​
     @Override
     @ApiProducerMethod(methodName = "插入方法", uri = "/wg/activity/insert", httpCommandType = "POST", auth = 1)
     public String insert(XReq req) {
         return "hi " + JSON.toJSONString(req) + " by api-gateway-test-provider";
     }
 ​
     @Override
     @ApiProducerMethod(methodName = "测试方法", uri = "/wg/activity/test", httpCommandType = "POST", auth = 0)
     public String test(String str, XReq req) {
         return "hi " + str + JSON.toJSONString(req) + " by api-gateway-test-provider";
     }
 ​
 }
 ​

注意测试项目的依赖要换成我们自己的

         <dependency>
             <groupId>com.zshunbao.gateway</groupId>
             <artifactId>api-gateway-sdk-02</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>

然后在我们启动测试项目之前还要做一些准备

  • 首先是清空 api-gateway-center中有关服务端的数据库表:application_interface,application_interface_method,application_system。

  • 其次我们在第19章中,我们当时由于部署到docker会自动分配IP地址,因此我们在api-gateway-core的netty服务端对于地址端口的绑定代码做了修改,但是由于我们本机使用的校园网,每一次重启电脑都会的IP地址都会改变,这意味每次重启本机都需要根据IP地址进行重新部署(我们是在windows上安装了docker,没有部署到服务器上去),因此这里我们还是直接在本地将多个模块进行联调。由于是在本地,因此第19章中修改的netty服务端通信的代码需要修改,加上IP地址的绑定,即GatewaySocketServer.java

 /**
  * @program: api-gateway-core
  * @ClassName GatewaySocketServer
  * @description: 网关会话服务端
  * @author: zs宝
  * @create: 2025-07-25 16:48
  * @Version 1.0
  **/
 ​
 package com.zshunbao.gateway.core.socket;
 ​
 import com.zshunbao.gateway.core.session.Configuration;
 import com.zshunbao.gateway.core.session.defaults.DefaultGatewaySessionFactory;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 import java.net.InetSocketAddress;
 import java.util.concurrent.Callable;
 ​
 //实现异步调用接口,使整个会话服务端的过程异步建立
 public class GatewaySocketServer implements Callable<Channel> {
     private final Logger logger = LoggerFactory.getLogger(GatewaySocketServer.class);
     private EventLoopGroup boss;
     private EventLoopGroup worker;
     private Channel channel;
 ​
     private DefaultGatewaySessionFactory gatewaySessionFactory;
     private final Configuration configuration;
 ​
     public GatewaySocketServer(Configuration configuration,DefaultGatewaySessionFactory gatewaySessionFactory) {
         this.gatewaySessionFactory = gatewaySessionFactory;
         this.configuration = configuration;
         this.initEventLoopGroup();
     }
 ​
     private void initEventLoopGroup() {
         boss=new NioEventLoopGroup(configuration.getBossNThreads());
         worker=new NioEventLoopGroup(configuration.getWorkNThreads());
     }
 ​
     @Override
     public Channel call() throws Exception {
         ChannelFuture channelFuture=null;
         try {
             ServerBootstrap b=new ServerBootstrap();
             b.group(boss,worker)
                     .channel(NioServerSocketChannel.class)
                     //设置 TCP 最大连接等待队列
                     .option(ChannelOption.SO_BACKLOG,128)
                     .childHandler(new SessionChannelInitializer(gatewaySessionFactory,configuration));
             //InetSocketAddress 是 Java 用于表示“IP + 端口”的地址对象
             //syncUninterruptibly() 会阻塞直到启动完成(不抛出异常)
             // Docker 容器部署会自动分配IP,所以我们只设定端口即可。
             channelFuture = b.bind(new InetSocketAddress(configuration.getHostName(), configuration.getPort())).syncUninterruptibly();
             //channelFuture = b.bind(configuration.getPort()).syncUninterruptibly();
             this.channel=channelFuture.channel();
         }catch (Exception e){
             logger.error("socket server start error.", e);
         }finally {
             if(null!=channelFuture && channelFuture.isSuccess()){
                 logger.info("socket server start done.");
             }else{
                 logger.error("socket server start error.");
             }
         }
         return channel;
     }
 }
  • 接着本地重新clean,install api-gateway-core,api-gateway-assist这两个模块。

  • 最后根据本地情况,在api-gateway-test-provider这个测试项目的application.yml中添加sdk需要读取的系统信息

 api-gateway-sdk:
   address: http://localhost:8001  # 注册中心;从这里获取接口信息以及完成注册网关操作
   systemId: api-gateway-test-provider
   systemName: 网关sdk测试工程
   systemRegistry: zookeeper://127.0.0.1:2181
  • 启动api-gateway-center,启动api-gateway-assist-00(api-gateway-assist的使用工程),启动测试工程api-gateway-test-provider

网关注册中心日志及数据库

然后我们打开ApiPost,进行测试

bug

在上面测试中,我们包裹一个bug,这个bug数据库什么的信息都正确注册,但是就是在ApiPost进行方法调用的时候报错

我们最开始是以为api-gateway-core中的代码出现了问题,但是仔细想想,我们对于其中的代码只修改了一行,切着修改后的一行在第19章未部署之前一直测试争取,可以被调用,那就说明这不是出现在api-gateway-core代码上的问题,而应该是其它问题。

我们仔细梳理一下,对应http请求到达网关要想进行远程泛化调用走的是api-gateway-core的逻辑,但是这个网关通信模块是被集成到了其starter api-gateway-assist中去了的,因此,我们打开api-gateway-assist的日志,发现

在日志中表示,当网关通信服务向网管注册中心进行配置拉取的时候,拉回来一个空集,

我们仔细查看网管注册中心有关网关服务拉取配置的方法,应该说在网关注册中心的代码我么从来没有修改过,且之前测试都毫无问题,所以逻辑上得处理肯定没有问题,但是这里有一个点就是拉取配置是首先从gateway_distribution表中去进行查询,根据匹配查询结果,再去找服务端相关的配置,然后我们打开gateway_distribution表

发现其中的信息网关服务对应的远程服务为api-gateway-test,与我们测试项目中写的api-gateway-test-provider不同,所以最后查询到的远程服务集合为空,所以调用报错

这里我们修改gateway_distribution表的对应配置即可

参考资料