《API网关》第1章:HTTP请求会话协议处理

一、学习指引

别把我想的太大!

首先在API网关设计实现上,不要把它过于神化或把它想的太复杂。从功能角度分析,它更像是把自己伪装成 WEB 服务的 SpringMVC 工程。说白了,如果没有网关服务,也就是使用 SpringMVC 包装 RPC(Dubbo) 接口,对外提供 HTTP 接口的过程。

所以API网关的存在更像是帮我们解决了需要搭建 SpringMVC 的包装 RPC 接口的过程,只需要把 RPC 接口注册到网关,在经过 HTTP 访问就能直达 RPC 调用并返回结果。大哥:贼TM就是网关呀!

img.png

二、会话设计

HTTP请求到API网关,网关再去调用到对应的RPC服务,那么这样一个流程一次请求,把它抽象为是做了一次会话操作

之所以称之为会话,是因为一次 HTTP 请求,就要完成;建立连接、协议转换、方法映射、泛化调用、返回结果等一些列操作。而在这些操作过程中的各类行为处理,其实也类似于ORM框架,只不过一个是对数据库的处理,一个是对 RPC服务的处理。所有的学习都是举一反三,核心的设计万变不离其宗!
此外之所以要单独创建出一个 api-gateway-core 的工程,是因为我们要把这个工程独立于各种容器,例如它并不是直接与 SpringBoot 一起开发,因为那样会让组件失去灵活性。它的存在更应该像是一个 ORM 框架,可以独立使用,谁也都可以结合。

三、会话实现

工程结构

api-gateway-01
└── src
├── main
│ └── java
│ └── cn.bugstack.gateway.session
│ ├── handlers
│ │ └── SessionServerHandler.java
│ ├── BaseHandler.java
│ ├── SessionChannelInitializer.java
│ └── SessionServer.java
└── test
└── java
└── cn.bugstack.mybatis.test.dao
├── dao
│ ├── ISchoolDao.java
│ └── IUserDao.java
└── ApiTest.java

会话协议处理,服务搭建核心类关系,如图 1-3 所示

img_1.png

注意:关于 Netty 部分如果你感觉有些陌生,可以学习小傅哥的《Netty小册》 —— 你暂时不需要太多的理论,只要跟着这套案例做,就可以快速上手 Netty
首先从结构上来看,小傅哥定义了 session 会话包,把 Netty 对于 HTTP 的网络请求处理封装到会话结构下。通过这样的方式来包装网络请求。
SessionServer 是用于启动 Netty 服务的,相当于你启动 Socket 的过程。而 Netty 的启动配置除了端口外,还需要把对应数据的处理一并初始化到Channel管道中,也就是对应的 SessionChannelInitializer 类的功能。
在 SessionChannelInitializer 类中包装了 HTTP Get/Post 协议的解析,同时还包括我们自己要完成的网络请求。也就是 SessionServerHandler 部分的内容。其实如果你了解一些Netty,那么把重点放到 SessionServerHandler 的实现即可,因为整个 Netty 的创建就相当于是一个模板结构,是固定的使用方式

网络协议

Netty 的通信会为每个连接上来的用户建立一条 Channle 管道(对应的 ChannelID 唯一),并在管道中插入一道道板子,这些板子可以是;编码器、解码器、流量整形、SSL、自定义服务处理等各类模块。通过这样的方式,让我们可以扩展各类功能。对应的也就是接口 ChannelInitializer 的实现类所完成的事情。

HttpRequestDecoder、HttpResponseEncoder 是 Netty 本身提供的HTTP编解器,这部分涉及到网络通信中的通信协议和半包粘包处理,可以跳转到小傅哥的《Netty小册》 学习。
HttpObjectAggregator 用于处理除了 GET 请求外的 POST 请求时候的对象信息,否则只有上面的信息,是拿不到 POST 请求的。这就很像不断的在管道中添加板子,不同的板子处理不同的功能
最后一个 SessionServerHandler 是我们自己实现的会话处理,用于拿到HTTP网络请求后,处理我们自己需要的业务逻辑。

数据处理

会话的核心处理在于接收到数据以后,根据请求的地址 http://localhost/c/queryUserInfo … 解析,拿到对应的方法做后续的调用。在目前我们本章会先拿到这个调用方法,只返回简单的记录。在下一章再完善关于 RPC 的泛化调用。

DefaultFullHttpResponse 相当于就是在构建 HTTP 会话所需的协议信息,包括头信息、编码、响应体长度、跨域访问等。
这些信息中还包括了我们要向网页端返回的数据,也就是 response.content().writeBytes(…) 中写入的数据内容。

服务创建

Netty 本身就是一个 Socket NIO 的包装,所以也要包括对服务的启动处理,这里我们实现 Callable 接口,让服务在线程池中启动。

EventLoopGroup 分别启动的是你的连接等待和数据处理,之后这里的 childHandler 添加的就是会话的初始信息。 当这些信息创建完成,绑定端口启动服务即可。

四、功能测试

启动服务

创建 SessionServer 会话服务,交给线程池启动并检测服务的启动结果,当启动成功后开始网络请求;
在浏览器输入:http://localhost:7397/queryUserInfo

测试结果

如果你已经能输出这条信息,代表着你已经顺利的完成了本章节的内容。另外在学习的过程中,可以尝试在 SessionServerHandler 中打断点,检测程序调用时候的网络请求信息。

《API网关》第2章:代理RPC泛化调用
一、学习指引
你不能总是硬编码解决所有问题!
对于中间件框架的实现,往往都离不开代理的操作,因为需要使用代理用逻辑封装行为。例如你原本是通过硬编码调用一个接口,那么现在因为所有的行为都被提炼到中间件中控制,那么对于接口的调用就不在一个而是一堆。而对于这样一堆接口的调用,它们是有共性的,比如可以统一使用Java反射获取到方法名、入参、出参等等信息,再根据这些信息做代理逻辑包装,让每一个接口的调用都被中间件处理。
那么我们本章要实现的就是把来自网关的HTTP请求,转换到RPC调用上,这里就涉及到了RPC所提供的泛化调用,按照对应的泛化调用的逻辑,提供对应的接口和方法以及入参信息就可以拿到最终的结果。文档:https://dubbo.apache.org/zh/docs/advanced/generic-reference/

二、绑定设计

这里需要思考🤔,为了把 HTTP 与 RPC 建立连接,就像你把 DAO 与 SQL 执行建立连接一样,两个服务的中间需要被绑定。也就是把 HTTP 地址中的接口方法与 RPC 对应的服务建立起一种关联关系,这样才能满足在调用 HTTP 网关接口时,调用到对应的 RPC 服务上。

img_2.png
就像如图所示 HTTP 经过网关调用到 RPC 中间的执行逻辑就是把两个模块用绑定的方式建立起连接,生成一个代理对象。代理对象中包装的就是执行网关接口泛化调用的参数准备和执行以及返回结果的操作。
这里的第一个知识点是泛化调用,它是 RPC 接口设计中提供的一种反射调用机制,你不需要硬编码调用接口,只需要提供接口的方法名称、入参信息,即可调用到对应的 RPC 接口服务。可以参考官网文档:https://dubbo.apache.org/zh/docs/advanced/generic-reference/
这里的第二个知识点是代理包装,虽然 RPC 框架提供了泛化调用,也就是说这里可以拿到网络协议转换的 HTTP 请求信息以后,就能直接调用到 RPC 接口。但这样的操作方式不太方便使用,存在硬编码的风险,后续不好迭代升级,也不好扩展其他的接口。因为每一个 RPC 的实现,泛化调用的方法名称还是有所不同的,另外是扩展非 RPC 框架的逻辑,也不方便处理。所以这里需要单独提供一个代理包装逻辑。
这里的第三个知识点是Cglib,因为有第二个知识点中代理操作的存在,我们就需要选择一种方式来做代理处理,而Cglib 可以满足我们自定义创建接口的方式进行代理,同时又可以在让一个代理类有多个接口。注意:多个接口的意思是,一个接口是用于标准的描述,在于使用上。另外一个接口是自主生成的,生成的是我们的 RPC 描述性接口,相当于自主生成了class字节码。这部分我会在视频中再加以介绍

三、绑定实现

  1. 工程结构

api-gateway-02
└── src
├── main
│ └── java
│ └── cn.bugstack.gateway
│ ├── bind
│ │ ├── GenericReferenceProxy.java
│ │ ├── GenericReferenceProxyFactory.java
│ │ ├── GenericReferenceRegistry.java
│ │ └── IGenericReference.java
│ ├── session
│ │ ├── defaults
│ │ │ └── GenericReferenceSessionFactory.java
│ │ └── handlers
│ │ └── SessionServerHandler.java
│ ├── BaseHandler.java
│ ├── Configuration.java
│ ├── GenericReferenceSessionFactoryBuilder.java
│ ├── IGenericReferenceSessionFactory.java
│ ├── SessionChannelInitializer.java
│ └── SessionServer.java
└── test
└── java
└── cn.bugstack.gateway.test
├── ApiTest.java
├── CglibTest.java
└── RPCTest.java

绑定处理,服务搭建核心类关系,如图 2-2 所示

img_3.png

图 2-2 绑定处理,服务搭建核心类关系

img_4.png

Configuration 配置项用于贯穿整个会话周期,所以本章节添加了配置项用于存放代理对象、服务配置等信息。
GenericReferenceProxy 用于调用RPC接口的泛化调用处理,封装 RPC 提供的 GenericService 方法:genericService.$invoke(“sayHi”, new String[]{“java.lang.String”}, new Object[]{“world”});
IGenericReference 是一种定义出来的接口,专门给到通信层做编码调用使用的。
GenericReferenceProxyFactory 用于创建代理对象,这里用到了 CGLIB 操作,它先构建出一个 RPC 接口信息,在把我们定义的 IGenericReference 接口和构建的接口,一起给代理类实现。也就是说一个代理类,实现了2个接口。你可以在大脑袋中想一下这个事 创建完成后,我们使用定义的接口调用,模拟创建出来的接口逻辑。

  1. 实现代理类

因为网关服务是不引入 RPC 接口的包描述接口信息的,所以也就是只能使用泛化调用进行处理,那么我们在网关执行中又希望可以通过编码的方式调用接口,做一层装饰使用,所以这里要定义一个 IGenericReference 泛化调用的接口,并把这个接口和 RPC 使用 CGLIB 创建出来的接口结合起来交给代理类实现。

2.1 统一泛化调用接口

public interface IGenericReference {

String $invoke(String args);

}

目前只在这里提供一个标准的 String 的入参和出参类型的泛化调用接口。

2.2 泛化调用封装

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Class<?>[] parameterTypes = method.getParameterTypes();
String[] parameters = new String[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameters[i] = parameterTypes[i].getName();
}
// 举例:genericService.$invoke(“sayHi”, new String[]{“java.lang.String”}, new Object[]{“world”});
return genericService.$invoke(methodName, parameters, args);
}

实现了 MethodInterceptor 接口的 GenericReferenceProxy 代理类,对 RPC 的泛化调用进行了封装。

2.3 代理工厂

public IGenericReference newInstance(String method) {
return genericReferenceCache.computeIfAbsent(method, k -> {
// 泛化调用
GenericReferenceProxy genericReferenceProxy = new GenericReferenceProxy(genericService, method);
// 创建接口
InterfaceMaker interfaceMaker = new InterfaceMaker();
interfaceMaker.add(new Signature(method, Type.getType(String.class), new Type[]{Type.getType(String.class)}), null);
Class<?> interfaceClass = interfaceMaker.create();
// 代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
// IGenericReference 统一泛化调用接口
// interfaceClass 根据泛化调用注册信息创建的接口,建立 http -> rpc 关联
enhancer.setInterfaces(new Class[]{IGenericReference.class, interfaceClass});
enhancer.setCallback(genericReferenceProxy);
return (IGenericReference) enhancer.create();
});
}

在代理工厂创建对象的实现类中,首先使用 CGlib 给 RPC 的绑定接口,创建出一个接口出来。并实现我们定义的 IGenericReference 接口和创建的接口,到代理实现类上。

  1. 注册代理类

因为我们要做的是一种把 HTTP 的请求方法与对应的 RPC 方法做关系绑定,所以这里还需要提供一个代理绑定操作。

3.1 注册

public void addGenericReference(String application, String interfaceName, String methodName) {
// 获取基础服务(创建成本较高,内存存放获取)
ApplicationConfig applicationConfig = configuration.getApplicationConfig(application);
RegistryConfig registryConfig = configuration.getRegistryConfig(application);
ReferenceConfig reference = configuration.getReferenceConfig(interfaceName);
// 构建Dubbo服务
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(applicationConfig).registry(registryConfig).reference(reference).start();
// 获取泛化调用服务
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
// 创建并保存泛化工厂
knownGenericReferences.put(methodName, new GenericReferenceProxyFactory(genericService));
}

注册泛化调用服务接口方法,并保存到 GenericReferenceRegistry#knownGenericReferences 方法中。此处为 Mybatis 框架的实现结构,希望读者对星球中《手写Mybatis》的学习用到实际编码中,举一反三

3.2 获取

public IGenericReference getGenericReference(String methodName) {
GenericReferenceProxyFactory genericReferenceProxyFactory = knownGenericReferences.get(methodName);
if (genericReferenceProxyFactory == null) {
throw new RuntimeException(“Type “ + methodName + “ is not known to the GenericReferenceRegistry.”);
}
return genericReferenceProxyFactory.newInstance(methodName);
}

获取的操作直接获取后,并实例化即可。

  1. 会话服务调用

protected void session(ChannelHandlerContext ctx, final Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求 uri:{} method:{}”, request.uri(), request.method());
// 返回信息控制:简单处理
String methodName = request.uri().substring(1);
if (methodName.equals(“favicon.ico”)) return;

// 服务泛化调用
IGenericReference reference = configuration.getGenericReference("sayHi");
String result = reference.$invoke("test") + " " + System.currentTimeMillis();

  // 返回数据
channel.writeAndFlush(response);

}

在会话服务的处理中,现在可以根据用户请求的 HTTP 方法,从配置项中获取对应的 RPC 服务,在通过服务进行调用。现在调用的就是代理中对 RPC 的泛化调用调用处理。

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;
操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务

@Service(version = “1.0.0”)
public class ActivityBooth implements IActivityBooth {

@Override
public String sayHi(String str){
    return "hi " + str + " by api-gateway-test-provider";
}

}

启动日志

2022-08-20 14:31:32.151 INFO 40388 — [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@6107165
2022-08-20 14:31:32.161 INFO 40388 — [ main] o.a.c.f.imps.CuratorFrameworkImpl : Default schema
2022-08-20 14:31:32.162 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2022-08-20 14:31:32.167 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to localhost/127.0.0.1:2181, initiating session
2022-08-20 14:31:32.194 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x100001b6d3e0000, negotiated timeout = 40000
2022-08-20 14:31:32.200 INFO 40388 — [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2022-08-20 14:31:32.742 INFO 40388 — [ main] c.b.gateway.ApiGatewayTestApplication : Started ApiGatewayTestApplication in 3.195 seconds (JVM running for 4.041)

  1. 启动网关

public void test_GenericReference() throws InterruptedException, ExecutionException {
Configuration configuration = new Configuration();
configuration.addGenericReference(“api-gateway-test”, “cn.bugstack.gateway.rpc.IActivityBooth”, “sayHi”);
GenericReferenceSessionFactoryBuilder builder = new GenericReferenceSessionFactoryBuilder();
Future future = builder.build(configuration);
logger.info(“服务启动完成 {}”, future.get().id());
Thread.sleep(Long.MAX_VALUE);
}

14:34:26.935 [nioEventLoopGroup-4-2] INFO cn.bugstack.gateway.session.handlers.SessionServerHandler - 网关接收请求 uri:/sayHi method:GET
14:34:27.125 [nioEventLoopGroup-4-2] INFO cn.bugstack.gateway.session.handlers.SessionServerHandler - 网关接收请求 uri:/favicon.ico method:GET
14:34:36.858 [main-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for sessionid: 0x100001b6d3e0003 after 6ms
14:34:50.195 [main-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for sessionid: 0x100001b6d3e0003 after 4ms
14:35:03.535 [main-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for sessionid: 0x100001b6d3e0003 after 6ms

从测试结果看,目前我们已经可以通过网关HTTP请求调用到对应的 RPC 服务上,并返回调用的结果信息。

《API网关》第3章:分治处理会话流程

一、学习指引

你不可能一次性,把所有事情都做完,但你总有时间做完一件事!

关于网关的实现,也包括像星球中Lottery的开发、源码的学习手写Mybatis。这些内容最开始最难的那部分不是功能的实现,而是小傅哥会🤔去思考每一个章节拆分的颗粒度,希望在这条完整的学习线上,把架构、设计、重构、实现、思考等这些内容展示给你,并让你能吸收学习到核心的本质内容。这远比简单的堆砌些代码片段重要的多,所以也希望你在学习的过程中,多一些这方面的思考。

那么本章小傅哥就带着大家来完成这样一件事情。—— 注意:小傅哥不是要带着大家急匆匆的实现一个API网关系统,那样的直接以目的为导向,将失去学习意义。所以如果是在职的公司伙伴,着急用网关可以参考整体的架构设计和网络资料做开发实现,遇到问题可以对我发起提问,我会去回答。🍻

二、分治逻辑

分治是软件设计原则,康威定律第一条包括的内容,它可以帮助和指导我们去划分系统模块的职责,界限上下文关系。PS:很多伙伴没有划分清上下文的系统和模块,很可能造成循环依赖,即使是最新版本的 Spring 也并不推荐使用循环依赖,它只是为一个错误的设计做的补丁逻辑。

本章所体现的重点是重构,因为我们要继续扩展新的功能,而上一章功能实现的逻辑分层结构不易于扩展,所以需要进行重构处理。重构点包括;
img_5.png
拆分session包下会话业务逻辑部分和网络通信部分,做功能实现的隔离。
拆分bind包下代理和对RPC接口的泛化调用,这里你可以把RPC当做一种可连接资源,而这种连接资源也不是只有 RPC 一种,同时也因为 RPC 的泛化调用是一种通用方法,并不需要与逻辑绑定,所以它也应该被拆分出来。

三、重构实现

  1. 工程结构

api-gateway-03
└── src
├── main
│ └── java
│ └── cn.bugstack.gateway
│ ├── bind
│ │ ├── IGenericReference.java
│ │ ├── MapperMethod.java
│ │ ├── MapperProxy.java
│ │ ├── MapperProxyFactory.java
│ │ └── MapperRegistry.java
│ ├── mapping
│ │ ├── HttpCommandType.java
│ │ └── HttpStatement.java
│ ├── session
│ │ ├── defaults
│ │ │ ├── DefaultGatewaySession.java
│ │ │ └── DefaultGatewaySessionFactory.java
│ │ ├── Configuration.java
│ │ ├── GatewaySession.java
│ │ └── GatewaySessionFactory.java
│ ├── socket
│ │ └── handlers
│ │ └── GatewayServerHandler.java
│ ├── BaseHandler.java
│ ├── GatewayChannelInitializer.java
│ └── GatewaySocketServer.java
└── test
└── java
└── cn.bugstack.gateway.test
└── ApiTest.java

HTTP会话核心链路流程,如图3-2所示
img_6.png

整个工程结构分治设计包括;bind(绑定)、mapping(映射)、session(会话)、socket(网络)这4大块。
整个调用流程以 socket 网络处理协议转换后,获取会话 session 从 session 中得到映射器对象,并根据HTTP接口的 GET/POST 调用到不同的方法上。
因为这样的拆分可以方便在网络层做鉴权、限流、熔断、鉴权等功能,它们可以与 session 会话逻辑拆分。这也就是本章节要拆分结构的最核心目的。
另外一层关系是 bind 绑定层中,把 RPC 的泛化调用拆分出来,因为这里可以把 RPC 当成一种资源来看待,拆分后更有易于后续的池化、扩展和管理。

好,这里大家如果阅读了小傅哥的《手写MyBatis:渐进式源码实践》会发现这个结构和ORM框架很像,其实网关本身也是这样一个结构,你可以把调用数据库理解成调用 RPC,封装数据库操作结果,理解成封装 RPC 的请求结果。所以如果说尚未做过一些源码的深度学习,即使是有机会去做一些较复杂场景的框架开发,往往这部分设计思维也是不足的。但当然你也可以把网关的实现抽象成其他的结构,因为具体的代码落地,都是来自于你对抽象的设计。

  1. 拆分绑定和接口泛化调用
    2.1 MapperMethod

源码详见:cn.bugstack.gateway.bind.MapperMethod#execute

public Object execute(GatewaySession session, Object args) {
Object result = null;
switch (command) {
case GET:
result = session.get(uri, args);
break;
case POST:
break;
case PUT:
break;
case DELETE:
break;
default:
throw new RuntimeException(“Unknown execution method for: “ + command);
}
return result;
}

基于网关接口注册时的方法,GET、POST、PUT、DELETE 做不同逻辑的处理。

2.1 MapperProxy

源码详见:cn.bugstack.gateway.bind.MapperProxy

public class MapperProxy implements MethodInterceptor {

private GatewaySession gatewaySession;
private final String uri;

public MapperProxy(GatewaySession gatewaySession, String uri) {
    this.gatewaySession = gatewaySession;
    this.uri = uri;
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    MapperMethod linkMethod = new MapperMethod(uri, method, gatewaySession.getConfiguration());
    return linkMethod.execute(gatewaySession, args);
}

}

简化映射器代理,将原有的 RPC 泛化调用拆分。这个类中只完成代理部分,并调用映射器方法完成逻辑处理。

  1. 拆分会话和网络协议请求
    3.1 会话处理

源码详见:cn.bugstack.gateway.session.GatewaySession

public interface GatewaySession {

Object get(String uri, Object parameter);

IGenericReference getMapper(String uri);

Configuration getConfiguration();

}

源码详见:cn.bugstack.gateway.session.defaults.DefaultGatewaySession

public Object get(String uri, Object parameter) {
/* 以下这部分内容,后续拆到执行器中处理 */
// 配置信息
HttpStatement httpStatement = configuration.getHttpStatement(uri);
String application = httpStatement.getApplication();
String interfaceName = httpStatement.getInterfaceName();
// 获取基础服务(创建成本较高,内存存放获取)
ApplicationConfig applicationConfig = configuration.getApplicationConfig(application);
RegistryConfig registryConfig = configuration.getRegistryConfig(application);
ReferenceConfig reference = configuration.getReferenceConfig(interfaceName);
// 构建Dubbo服务
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(applicationConfig).registry(registryConfig).reference(reference).start();
// 获取泛化调用服务
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
return genericService.$invoke(httpStatement.getMethodName(), new String[]{“java.lang.String”}, new Object[]{“小傅哥”});
}

定义 GatewaySession 网关会话接口并做相应的默认实现。在实现类中你会看到这里获取了 RPC 泛化调用的配置,并做了 RPC 泛化调用的处理。这部分也就是设计图中的虚线部分,待后续放到执行器中处理。

3.2 网络服务

源码详见:cn.bugstack.gateway.socket.handlers.GatewayServerHandler

protected void session(ChannelHandlerContext ctx, final Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求 uri:{} method:{}”, request.uri(), request.method());
// 返回信息控制:简单处理
String uri = request.uri();
if (uri.equals(“/favicon.ico”)) return;
GatewaySession gatewaySession = gatewaySessionFactory.openSession();
IGenericReference reference = gatewaySession.getMapper(uri);
String result = reference.$invoke(“test”) + “ “ + System.currentTimeMillis();
// 返回信息处理
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
// 设置回写数据
response.content().writeBytes(JSON.toJSONBytes(result, SerializerFeature.PrettyFormat));

// ...

channel.writeAndFlush(response);

}

这部分代码的主要变化是在协议处理中通过会话工厂获取会话,并通过会话获取映射器以及调用对应的方法。你可以把这想象成调用了一个 DAO 接口,这样就好理解多了

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;

操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务

@Service(version = “1.0.0”)
public class ActivityBooth implements IActivityBooth {

@Override
public String sayHi(String str){
    return "hi " + str + " by api-gateway-test-provider";
}

}

启动日志

2022-08-20 14:31:32.151 INFO 40388 — [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@6107165
2022-08-20 14:31:32.161 INFO 40388 — [ main] o.a.c.f.imps.CuratorFrameworkImpl : Default schema
2022-08-20 14:31:32.162 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2022-08-20 14:31:32.167 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to localhost/127.0.0.1:2181, initiating session
2022-08-20 14:31:32.194 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x100001b6d3e0000, negotiated timeout = 40000
2022-08-20 14:31:32.200 INFO 40388 — [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2022-08-20 14:31:32.742 INFO 40388 — [ main] c.b.gateway.ApiGatewayTestApplication : Started ApiGatewayTestApplication in 3.195 seconds (JVM running for 4.041)

这部分与上一章一样,后续测试也都需要先启动 RPC 服务。

  1. 启动网关

public void test_gateway() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
Configuration configuration = new Configuration();
HttpStatement httpStatement = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“/wg/activity/sayHi”,
HttpCommandType.GET);
configuration.addMapper(httpStatement);

// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);

// 3. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(gatewaySessionFactory);
Future<Channel> future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException("netty server start error channel is null");
while (!channel.isActive()) {
    logger.info("netty server gateway start Ing ...");
    Thread.sleep(500);
}
logger.info("netty server gateway start Done! {}", channel.localAddress());

Thread.sleep(Long.MAX_VALUE);

}

这部分测试内容包括;创建配置信息加载注册、基于配置构建会话工厂、创建启动网关网络服务,注意这里注册的网关接口内容丰富了,包括:
服务;api-gateway-test
接口;cn.bugstack.gateway.rpc.IActivityBooth
方法;sayHi
映射;/wg/activity/sayHi —— 也就是 http://localhost:7397/wg/activity/sayHi 调用的部分

之后就是创建会话并把这个信息传递给创建网络服务中,方便在 GatewayServerHandler 中使用。

测试结果

地址:http://localhost:7397/wg/activity/sayHi
说明:启动RPC提供者服务和网关后,可以通过浏览器或者一些POSTMAN工具访问测试

17:02:06.272 [nioEventLoopGroup-3-2] INFO cn.bugstack.gateway.socket.handlers.GatewayServerHandler - 网关接收请求 uri:/wg/activity/sayHi method:GET
17:02:06.475 [nioEventLoopGroup-3-2] INFO cn.bugstack.gateway.socket.handlers.GatewayServerHandler - 网关接收请求 uri:/favicon.ico method:GET
17:02:14.046 [nioEventLoopGroup-3-1-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for sessionid: 0x1000000a21f000d after 6ms
17:02:27.383 [nioEventLoopGroup-3-1-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for sessionid: 0x1000000a21f000d after 6ms

通过测试结果可以看到,经过我们重构后已经可以顺利执行网络请求了。

《API网关》第4章:将连接(RPC\HTTP\其他)抽象为数据源
一、学习指引
你发现傅哥开发的套路了没?
在整个API网关的功能实现上,也包括之前的星球里的其他项目如Lottery抽奖等,小傅哥在实现的过程中都会先开发一个只要能满足功能的核心逻辑案例,之后对案例的中代码的职责进行拆解细分。也就是使用框架结构、设计原则和设计模式等手段,将不同的功能模块进行分区实现,构建出界限上下文。
从前期来看这样的实现过程要比仅使用一个类 if···else 所开发的代码交付速度要慢了不少,那么从架构设计和开发上来说,为什么这么干?因为长期来看代码的成本远不是最开始的开发阶段,而是在后期的维护、迭代、扩展,满足产品新增的差异化需求时,所投入的研发资源。所以对于有稳定长发展周期的项目来说,这样做是值得的。
那么本章小傅哥则是带着大家把API网关中关于 RPC 的泛化调用使用提炼出来,把它抽象为一种数据连接资源进行使用。这样也就方便我们在这个框架中扩展其他的连接资源,包括各个厂家的 RPC 实现、HTTP服务、WebService 调用等,并还可以通过SPI的方式进行自定义连接资源扩展,以适应不同场景的诉求。

二、抽象连接
API网关的实现对于RPC接口的泛化调用,类似于ORM框架中对数据库的调用。那么我们也可以把 RPC 抽象成一种连接资源,做数据源的管理和池化的实现。这样既可以方便我们扩展新的连接方式,比如各类厂商的 RPC 框架,或者 HTTP 服务,以及你提供的大数据原始接口服务,也都可以被这样包装处理。所以这是抽象连接为数据源的设计目的。
整个API网关的通信模型结构已经逐步的清晰,从网络协议转换->开启通信会话->获取映射关系->执行具体的请求方案,到本章要实现的抽象的数据源。把 RPC、HTTP 当做数据源来维护。
在这一系列内容的开发中,你将慢慢体会到职责的分离,功能模块的解耦,为了扩展预留出了哪些东西。
img_7.png
三、功能实现

  1. 工程结构
    数据源服务核心链路关系,如图4-2所示
    img_8.png

从 DefaultGatewaySession 默认网关会话服务到 RPC 调用,拆分出数据源的实现。以上的结构就是作为后续扩展各类通信服务的扩展点,例如:Dubbo、HTTP、GRPC等。
目前我们在这里不做过多的细节处理,包括池化的功能、HTTP的完善等,暂时以实现主体为主,后续在陆续完善。

  1. 数据源设计实现

这里小傅哥按照JDBC的模型结构,设计API网关中的数据源实现,提供 Connection 连接接口,每一个具体的服务 Dubbo、HTTP 都实现 Connection,再由 DataSource 数据源接口来管理。

2.1 服务连接

public interface Connection {

Object execute(String method, String[] parameterTypes, String[] parameterNames, Object[] args);

}

public class DubboConnection implements Connection {

private final GenericService genericService;

public DubboConnection(ApplicationConfig applicationConfig, RegistryConfig registryConfig, ReferenceConfig<GenericService> reference) {
    // 连接远程服务
    DubboBootstrap bootstrap = DubboBootstrap.getInstance();
    bootstrap.application(applicationConfig).registry(registryConfig).reference(reference).start();
    // 获取泛化接口
    ReferenceConfigCache cache = ReferenceConfigCache.getCache();
    genericService = cache.get(reference);
}

/**
 * Dubbo 泛化调用:https://dubbo.apache.org/zh/docsv2.7/user/examples/generic-reference/
 */
@Override
public Object execute(String method, String[] parameterTypes, String[] parameterNames, Object[] args) {
    return genericService.$invoke(method, parameterTypes, args);
}

}

这里小傅哥把 Dubbo 的泛化调用相关初始化和执行提取到连接的接口实现中,这就类似于你对 JDBC 的操作。
另外这里还有 HTTP 的实现,暂时不是本章节的主要重点,可以阅读下对应的源码。HTTPConnection

2.2 数据源管理
有了不同策略实现的连接类,那么接下来就是对这些连接的使用进行一个统一的数据源管理,同时将来还可以基于这样的连接信息做池化的处理,提供使用效率。
public interface DataSource {

Connection getConnection();

}

public class UnpooledDataSource implements DataSource {

private Configuration configuration;
private HttpStatement httpStatement;
private DataSourceType dataSourceType;

@Override
public Connection getConnection() {
    switch (dataSourceType) {
        case HTTP:
            // TODO 预留接口,暂时不需要实现
            break;
        case Dubbo:
            // 配置信息
            String application = httpStatement.getApplication();
            String interfaceName = httpStatement.getInterfaceName();
            // 获取服务
            ApplicationConfig applicationConfig = configuration.getApplicationConfig(application);
            RegistryConfig registryConfig = configuration.getRegistryConfig(application);
            ReferenceConfig<GenericService> reference = configuration.getReferenceConfig(interfaceName);
            return new DubboConnection(applicationConfig, registryConfig, reference);
        default:
            break;
    }
    throw new RuntimeException("DataSourceType:" + dataSourceType + "没有对应的数据源实现");
}

}

通过无池化的连接池对 DubboConnection、HTTPConnection 进行管理使用。
最后是通过 UnpooledDataSourceFactory 无池化的数据源工厂进行构建和获取。

  1. 会话使用数据源

在网关会话中需要对数据源进行创建使用,在 DefaultGatewaySessionFactory 工厂中构建数据源,在 DefaultGatewaySession 使用数据源。注意:不过这里后续还会扩展执行器,让会话的逻辑只是调用,而不是做具体的实现。

@Override
public GatewaySession openSession(String uri) {
// 获取数据源连接信息:这里把 Dubbo、HTTP 抽象为一种连接资源
DataSourceFactory dataSourceFactory = new UnpooledDataSourceFactory();
dataSourceFactory.setProperties(configuration, uri);
DataSource dataSource = dataSourceFactory.getDataSource();

return new DefaultGatewaySession(configuration, uri, dataSource);

}

通过 DataSourceFactory 构建数据源的服务。

@Override
public Object get(String methodName, Object parameter) {
Connection connection = dataSource.getConnection();
return connection.execute(methodName, new String[]{“java.lang.String”}, new String[]{“name”}, new Object[]{parameter});
}

现在在会话的逻辑实现中,已经非常简单了,只是调用数据源的一个执行处理。
不过你这里你还会注意到关于接口入参、参数名称等操作,这些内容后续会根据注册的接口信息,在执行器实现类中做统一的封装。

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;
操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务
@Service(version = “1.0.0”)
public class ActivityBooth implements IActivityBooth {

@Override
public String sayHi(String str){
    return "hi " + str + " by api-gateway-test-provider";
}

}

启动日志

2022-08-20 14:31:32.151 INFO 40388 — [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@6107165
2022-08-20 14:31:32.161 INFO 40388 — [ main] o.a.c.f.imps.CuratorFrameworkImpl : Default schema
2022-08-20 14:31:32.162 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2022-08-20 14:31:32.167 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to localhost/127.0.0.1:2181, initiating session
2022-08-20 14:31:32.194 INFO 40388 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x100001b6d3e0000, negotiated timeout = 40000
2022-08-20 14:31:32.200 INFO 40388 — [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2022-08-20 14:31:32.742 INFO 40388 — [ main] c.b.gateway.ApiGatewayTestApplication : Started ApiGatewayTestApplication in 3.195 seconds (JVM running for 4.041)

这部分与上一章一样,后续测试也都需要先启动 RPC 服务。

  1. 启动网关

public void test_gateway() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
Configuration configuration = new Configuration();
HttpStatement httpStatement = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“/wg/activity/sayHi”,
HttpCommandType.GET);
configuration.addMapper(httpStatement);

// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);

// 3. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(gatewaySessionFactory);
Future<Channel> future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException("netty server start error channel is null");
while (!channel.isActive()) {
    logger.info("netty server gateway start Ing ...");
    Thread.sleep(500);
}
logger.info("netty server gateway start Done! {}", channel.localAddress());

Thread.sleep(Long.MAX_VALUE);

}

这部分测试内容包括;创建配置信息加载注册、基于配置构建会话工厂、创建启动网关网络服务,注意这里注册的网关接口内容丰富了,包括:
服务;api-gateway-test
接口;cn.bugstack.gateway.rpc.IActivityBooth
方法;sayHi
映射;/wg/activity/sayHi —— 也就是 http://localhost:7397/wg/activity/sayHi 调用的部分

之后就是创建会话并把这个信息传递给创建网络服务中,方便在 GatewayServerHandler 中使用。

测试结果

地址:http://localhost:7397/wg/activity/sayHi
说明:启动RPC提供者服务和网关后,可以通过浏览器或者一些POSTMAN工具访问测试

10:29:33.398 [nioEventLoopGroup-3-2] INFO cn.bugstack.gateway.socket.handlers.GatewayServerHandler - 网关接收请求 uri:/wg/activity/sayHi method:GET
10:29:33.811 [nioEventLoopGroup-3-1-SendThread(localhost:2181)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply sessionid:0x1000000482e0002, packet:: clientPath:null serverPath:null finished:false header:: 22,3 replyHeader:: 22,136,-101 request:: ‘/dubbo/config/dubbo/api-gateway-test.tag-router,F response::

通过测试结果可以看到,通过重构后的数据源服务已经可以正常使用。

《API网关》第5章:HTTP请求参数解析

一、学习指引

准备好物料再开始做细节设计
当我们准备开发一个新的功能流程时,不要着急于直接上来就去打开累代码。你可以尝试着把需要开发的流程中的功能节点做一些案例。看看每个要实现的案例的入参、出参、调用形式都是什么样的,而这些信息的获取,将更好的帮助你更好的完成功能节点的串联。
就像本章如题一样我们要去解析 HTTP 的请求时所含带的参数信息,这包括GET请求、POST请求,也包括参数类型为 application/json 或者是 multipart/form-data 同时在执行 RPC 请求时,还可能因为参数为基本类型还是对象类型要做一些判断处理。所以类似这样的功能使用,最好都在开发前做一些案例测试,验证不同的功能是如何使用的,这样也可以帮助你从前到后的流程,设计和实现起来更加顺畅。

二、解析设计

HTTP 接口请求的参数需要解析成可以匹配到 RPC 接口的入参信息,所以通常为了方便控制一般只支持 RPC 接口单个对象入参,并且不允许同名不同参数的重载方法出现,这些会在 RPC 方法注册阶段进行报错提醒。

从网络请求到会话,需要对 GET/POST 的请求,以及请求的参数类型 Content-Type 做细化的参数解析操作。
同时按照 RPC 泛化调用的入参方式,将解析的参数封装处理。这部分内容在本章代码对应的 test 中有案例

三、功能实现

  1. 工程结构

数据源服务核心链路关系,如图5-2所示

本章的核心内容主要就在于,GatewayServerHandler 接收网络请求后,对参数信息的封装处理。

  1. 参数解析

按照不同类型的请求和 Content-Type 做对应的流程处理,如 GET 请求只需要用到 QueryStringDecoder 这个 Netty 提供的解析操作即可。POST 请求中有不同 Content-Type 一种是类似 FORM 表单的 multipart/form-data 类型,还有一种是大家非常常用的 application/json 类型,分别进行处理。

if (HttpMethod.GET == method) {
Map<String, Object> parameterMap = new HashMap<>();
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
decoder.parameters().forEach((key, value) -> parameterMap.put(key, value.get(0)));
return parameterMap;
} else if (HttpMethod.POST == method) {
switch (contentType) {
case “multipart/form-data”:
Map<String, Object> parameterMap = new HashMap<>();
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(request);
decoder.offer(request);
decoder.getBodyHttpDatas().forEach(data -> {
Attribute attr = (Attribute) data;
try {
parameterMap.put(data.getName(), attr.getValue());
} catch (IOException ignore) {
}
});
return parameterMap;
case “application/json”:
ByteBuf byteBuf = request.content().copy();
if (byteBuf.isReadable()) {
String content = byteBuf.toString(StandardCharsets.UTF_8);
return JSON.parseObject(content);
}
break;
default:
throw new RuntimeException(“未实现的协议类型 Content-Type:” + contentType);
}
}

首先通过 FullHttpRequest 获取请求的方法类型 GET/POST 当然还有其他的这里暂时不需要添加。
获取类型后,按照 GET/POST 分别做解析处理。其实在网关中最常用的就是 POST 请求 + application/json 的方式。同时会含有 token 信息一并传递,这些都是为了避免网关接口被外部滥用的情况。

  1. RPC 泛化调用

这里小傅哥要在给大家说明下 RPC 的泛化调用,因为对照不同的接口类型,其实是需要不同的参数传递的。接下来分别给大家举例说明。

3.1 基本类型 + 单参

接口

public String sayHi(String str) {
return “hi “ + str + “ by api-gateway-test-provider”;
}

调用

Object result = genericService.$invoke(“sayHi”, new String[]{“java.lang.String”}, new Object[]{“world”});

3.2 对象类型 + 单参

接口

public String insert(XReq req) {
return “hi “ + JSON.toJSONString(req) + “ by api-gateway-test-provider”;
}

调用

Map<String, Object> params = new HashMap<>();
params.put(“uid”, “10001”);
params.put(“name”, “小傅哥”);

Object user = genericService.$invoke(“insert”, new String[]{“cn.bugstack.gateway.rpc.dto.XReq”}, new Object[]{params});

3.3 对象类型 + 多参

接口

public String test(String str, XReq req) {
return “hi “ + str + JSON.toJSONString(req) + “ by api-gateway-test-provider”;
}

调用

Map<String, Object> params01 = new HashMap<>();
params01.put(“str”, “10001”);

Map<String, Object> params02 = new HashMap<>();
params02.put(“uid”, “10001”);
params02.put(“name”, “小傅哥”);

Object user = genericService.$invoke(“test”, new String[]{“java.lang.String”, “cn.bugstack.gateway.rpc.dto.XReq”}, new Object[]{params01.values().toArray()[0], params02});

通常这种多参数类型不会在网关中使用,但这种调用方式还是需要了解的。

  1. 会话中消息封装

源码详见:cn.bugstack.gateway.session.defaults.DefaultGatewaySession

public Object get(String methodName, Map<String, Object> params) {
Connection connection = dataSource.getConnection();
HttpStatement httpStatement = configuration.getHttpStatement(uri);
String parameterType = httpStatement.getParameterType();
/*

  • 调用服务
  • 封装参数 PS:为什么这样构建参数,可以参考测试案例;cn.bugstack.gateway.test.RPCTest
  • 01(允许):java.lang.String
  • 02(允许):cn.bugstack.gateway.rpc.dto.XReq
  • 03(拒绝):java.lang.String, cn.bugstack.gateway.rpc.dto.XReq —— 不提供多参数方法的处理
  • */
    return connection.execute(methodName,
    new String[]{parameterType},
    new String[]{“ignore”},
    SimpleTypeRegistry.isSimpleType(parameterType) ? params.values().toArray() : new Object[]{params});
    }

@Override
public Object post(String methodName, Map<String, Object> params) {
return get(methodName, params);
}

在网关会话的调用中,按照参数的类型封装请求信息,这就和 3 中提到的内容是相对应的。另外这里 post 请求暂时不做过多处理,只是调用get请求即可

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;
操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务

@Service(version = “1.0.0”)
public class ActivityBooth implements IActivityBooth {

@Override
public String sayHi(String str) {
    return "hi " + str + " by api-gateway-test-provider";
}

@Override
public String insert(XReq req) {
    return "hi " + JSON.toJSONString(req) + " by api-gateway-test-provider";
}

@Override
public String test(String str, XReq req) {
    return "hi " + str + JSON.toJSONString(req) + " by api-gateway-test-provider";
}

}

启动日志

2022-09-10 13:32:29.620 INFO 80357 — [ main] org.apache.zookeeper.ZooKeeper : Client environment:user.dir=/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-test-provider
2022-09-10 13:32:29.621 INFO 80357 — [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@6ea04618
2022-09-10 13:32:29.628 INFO 80357 — [ main] o.a.c.f.imps.CuratorFrameworkImpl : Default schema
2022-09-10 13:32:29.629 INFO 80357 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2022-09-10 13:32:29.634 INFO 80357 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to localhost/127.0.0.1:2181, initiating session
2022-09-10 13:32:29.641 INFO 80357 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x100000047570017, negotiated timeout = 40000
2022-09-10 13:32:29.646 INFO 80357 — [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2022-09-10 13:32:30.013 INFO 80357 — [ main] c.b.gateway.ApiGatewayTestApplication : Started ApiGatewayTestApplication in 2.344 seconds (JVM running for 2.769)

这一章节新增了2个额外的 RPC 接口方法,用于各类场景的测试验证。

  1. 启动网关

public void test_gateway() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
Configuration configuration = new Configuration();
HttpStatement httpStatement01 = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“java.lang.String”,
“/wg/activity/sayHi”,
HttpCommandType.GET);

HttpStatement httpStatement02 = new HttpStatement(
        "api-gateway-test",
        "cn.bugstack.gateway.rpc.IActivityBooth",
        "insert",
        "cn.bugstack.gateway.rpc.dto.XReq",
        "/wg/activity/insert",
        HttpCommandType.POST);

configuration.addMapper(httpStatement01);
configuration.addMapper(httpStatement02);

// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);
// 3. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(gatewaySessionFactory);
Future<Channel> future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException("netty server start error channel is null");
while (!channel.isActive()) {
    logger.info("netty server gateway start Ing ...");
    Thread.sleep(500);
}
logger.info("netty server gateway start Done! {}", channel.localAddress());
Thread.sleep(Long.MAX_VALUE);

}

这部分测试内容包括;创建配置信息加载注册、基于配置构建会话工厂、创建启动网关网络服务,注意这里注册的网关接口内容丰富了,包括:
服务;api-gateway-test
接口;cn.bugstack.gateway.rpc.IActivityBooth
方法;sayHi/insert
映射;/wg/activity/(sayHi/insert) —— 也就是 http://localhost:7397/wg/activity/(sayHi/insert) 调用的部分

之后就是创建会话并把这个信息传递给创建网络服务中,方便在 GatewayServerHandler 中使用。

GET测试

测试:
http://localhost:7397/wg/activity/sayHi
参数:
{
“str”: “10001”
}

GET 请求单入参测试,验证通过

POST测试

http://localhost:7397/wg/activity/index
参数:
{
“name”:”小傅哥”,
“uid”:”10001”
}

GET 请求对象入参测试,验证通过

《API网关》第6章:引入执行器封装服务调用

一、学习指引

继续拆分功能流程

如果你目前编写的代码还不多,可借鉴的优秀设计还不足,那么随之而来的你可能也没法意识到一堆逻辑实现的代码中,有哪些功能模块应该被拆解出来。而对流程的拆解能力,才是一个工程师进步的标志

就像我们整个实现的API网关工程一样,如果不考虑任何扩展性,那么它就是一段 if···else 堆砌出来的在一个类里的逻辑。虽然这样看似会很快的开发完成,但一个代码工程来说,在它的长周期迭代中后期的维护成本才是最大的。所以我们才会不断精湛技术,开发出更多优秀质量的系统工程。

二、封装设计

本章之前对于 HTTP 请求到网络协议转换后,就是会话流程的处理。但在会话流程中还有些内容过于冗余,这些内容就是对数据源(RPC)的调用时入参和出参的封装,它们应该被提取到一个专门的类来处理,这样才能更加方便的管理。会话的职责是负责串联上下文,执行器的职责是负责对数据源的调用信息处理

添加 executor 执行器模块,封装对数据源(RPC)的调用,以及处理相关的入参、出参信息。同时这里还会把网关的调用结果进行封装到一个标准的类中,类里包括了code码、info描述,以及 data 结果信息。
此外本章中还会简单的处理下,GatewayServerHandler 网络请求中的内容处理。让整个代码调用能看到主要干路,清晰化流程,方便后续维护。

三、封装实现

  1. 工程结构

执行器流程核心关系,如图6-2所示

本章的核心重点就在于在整个流程中添加上执行器以后,对流程的拆解和结果信息的封装处理。

  1. 实现执行器

2.1 定义接口

publicinterfaceExecutor{

GatewayResultexec(HttpStatementhttpStatement,Map<String,Object>params)throwsException;

}

定义一个执行器接口,所有的会话请求都通过执行器完成。

2.2 定义结果

publicclassGatewayResult{

privateStringcode;
privateStringinfo;
privateObjectdata;

protectedGatewayResult(Stringcode,Stringinfo,Objectdata){
this.code=code;
this.info=info;
this.data=data;
}

publicstaticGatewayResultbuildSuccess(Objectdata){
returnnewGatewayResult(“0000”,”调用成功”,data);
}

publicstaticGatewayResultbuildError(Objectdata){
returnnewGatewayResult(“0001”,”调用失败”,data);
}

}

如果大家在各中大厂参与项目开发时,关于接口的处理和使用,都是一个固定的返回格式,包括;code码、info信息,以及请求结果。
而这也是网关使用的一个特点,所以我们的网关实现中也需要封装一个这样的结果类,来告诉调用方调用情况和调用结果。

2.3 抽象封装

publicGatewayResultexec(HttpStatementhttpStatement,Map<String,Object>params)throwsException{
//参数处理;后续的一些参数校验也可以在这里封装。
StringmethodName=httpStatement.getMethodName();
StringparameterType=httpStatement.getParameterType();
String[]parameterTypes=newString[]{parameterType};
Object[]args=SimpleTypeRegistry.isSimpleType(parameterType)?params.values().toArray():newObject[]{params};
logger.info(“执行调用method:{}#{}.{}({})args:{}”,httpStatement.getApplication(),httpStatement.getInterfaceName(),httpStatement.getMethodName(),JSON.toJSONString(parameterTypes),JSON.toJSONString(args));
//抽象方法
try{
Objectdata=doExec(methodName,parameterTypes,args);
returnGatewayResult.buildSuccess(data);
}catch(Exceptione){
returnGatewayResult.buildError(e.getMessage());
}
}

这一部分调用就是上一章中在会话章节处理的信息,这里封装到执行器中来完成。

  1. 会话调用
    3.1 构建执行器

源码详见:cn.bugstack.gateway.session.defaults.DefaultGatewaySessionFactory

@Override
publicGatewaySessionopenSession(Stringuri){
//获取数据源连接信息:这里把Dubbo、HTTP抽象为一种连接资源
DataSourceFactorydataSourceFactory=newUnpooledDataSourceFactory();
dataSourceFactory.setProperties(configuration,uri);
DataSourcedataSource=dataSourceFactory.getDataSource();
//创建执行器
Executorexecutor=configuration.newExecutor(dataSource.getConnection());
//创建会话:DefaultGatewaySession
returnnewDefaultGatewaySession(configuration,uri,executor);
}

在开启会话时,创建对应的执行器,并把执行器传递给会话中使用。

3.2 会话调用执行器

源码详见:cn.bugstack.gateway.session.defaults.DefaultGatewaySession

publicObjectget(StringmethodName,Map<String,Object>params){
HttpStatementhttpStatement=configuration.getHttpStatement(uri);
try{
returnexecutor.exec(httpStatement,params);
}catch(Exceptione){
thrownewRuntimeException(“Errorexecget.Cause:”+e);
}
}

在会话方法中调用执行器提供的方法,返回结果。

  1. 网络调用

源码详见:cn.bugstack.gateway.socket.handlers.GatewayServerHandler

protectedvoidsession(ChannelHandlerContextctx,finalChannelchannel,FullHttpRequestrequest){
logger.info(“网关接收请求uri:{}method:{}”,request.uri(),request.method());
//1.解析请求参数
RequestParserrequestParser=newRequestParser(request);
Stringuri=requestParser.getUri();
if(null==uri)return;
Map<String,Object>args=newRequestParser(request).parse();
//2.调用会话服务
GatewaySessiongatewaySession=gatewaySessionFactory.openSession(uri);
IGenericReferencereference=gatewaySession.getMapper();
Objectresult=reference.$invoke(args);
//3.封装返回结果
DefaultFullHttpResponseresponse=newResponseParser().parse(result);
channel.writeAndFlush(response);
}

在网络请求中包括;解析请求参数、调用会话服务、封装返回结果,这三个大块。

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;

操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务

@Service(version=”1.0.0”)
publicclassActivityBoothimplementsIActivityBooth{

@Override
publicStringsayHi(Stringstr){
return”hi”+str+”byapi-gateway-test-provider”;
}

@Override
publicStringinsert(XReqreq){
return”hi”+JSON.toJSONString(req)+”byapi-gateway-test-provider”;
}

@Override
publicStringtest(Stringstr,XReqreq){
return”hi”+str+JSON.toJSONString(req)+”byapi-gateway-test-provider”;
}

}

启动日志

2022-09-1713:32:29.620INFO80357—[main]org.apache.zookeeper.ZooKeeper:Clientenvironment:user.dir=/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-test-provider
2022-09-1713:32:29.621INFO80357—[main]org.apache.zookeeper.ZooKeeper:Initiatingclientconnection,connectString=127.0.0.1:2181sessionTimeout=60000watcher=org.apache.curator.ConnectionState@6ea04618
2022-09-1713:32:29.628INFO80357—[main]o.a.c.f.imps.CuratorFrameworkImpl:Defaultschema
2022-09-1713:32:29.629INFO80357—[localhost:2181)]org.apache.zookeeper.ClientCnxn:Openingsocketconnectiontoserverlocalhost/127.0.0.1:2181.WillnotattempttoauthenticateusingSASL(unknownerror)
2022-09-1713:32:29.634INFO80357—[localhost:2181)]org.apache.zookeeper.ClientCnxn:Socketconnectionestablishedtolocalhost/127.0.0.1:2181,initiatingsession
2022-09-1713:32:29.641INFO80357—[localhost:2181)]org.apache.zookeeper.ClientCnxn:Sessionestablishmentcompleteonserverlocalhost/127.0.0.1:2181,sessionid=0x100000047570017,negotiatedtimeout=40000
2022-09-1713:32:29.646INFO80357—[ain-EventThread]o.a.c.f.state.ConnectionStateManager:Statechange:CONNECTED
2022-09-1713:32:30.013INFO80357—[main]c.b.gateway.ApiGatewayTestApplication:StartedApiGatewayTestApplicationin2.344seconds(JVMrunningfor2.769)

这一章节新增了2个额外的 RPC 接口方法,用于各类场景的测试验证。

  1. 启动网关

publicvoidtest_gateway()throwsInterruptedException,ExecutionException{
//1.创建配置信息加载注册
Configurationconfiguration=newConfiguration();
HttpStatementhttpStatement01=newHttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“java.lang.String”,
“/wg/activity/sayHi”,
HttpCommandType.GET);

HttpStatementhttpStatement02=newHttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“insert”,
“cn.bugstack.gateway.rpc.dto.XReq”,
“/wg/activity/insert”,
HttpCommandType.POST);

configuration.addMapper(httpStatement01);
configuration.addMapper(httpStatement02);

//2.基于配置构建会话工厂
DefaultGatewaySessionFactorygatewaySessionFactory=newDefaultGatewaySessionFactory(configuration);
//3.创建启动网关网络服务
GatewaySocketServerserver=newGatewaySocketServer(gatewaySessionFactory);
Futurefuture=Executors.newFixedThreadPool(2).submit(server);
Channelchannel=future.get();
if(null==channel)thrownewRuntimeException(“nettyserverstarterrorchannelisnull”);
while(!channel.isActive()){
logger.info(“nettyservergatewaystartIng…”);
Thread.sleep(500);
}
logger.info(“nettyservergatewaystartDone!{}”,channel.localAddress());
Thread.sleep(Long.MAX_VALUE);
}

这部分测试内容包括;创建配置信息加载注册、基于配置构建会话工厂、创建启动网关网络服务,注意这里注册的网关接口内容丰富了,包括:
服务;api-gateway-test
接口;cn.bugstack.gateway.rpc.IActivityBooth
方法;sayHi/insert
映射;/wg/activity/(sayHi/insert) —— 也就是 http://localhost:7397/wg/activity/(sayHi/insert) 调用的部分

之后就是创建会话并把这个信息传递给创建网络服务中,方便在 GatewayServerHandler 中使用。

GET测试

测试:
http://localhost:7397/wg/activity/sayHi
参数:
{
“str”:”10001”
}

GET 请求单入参测试,验证通过

POST测试

http://localhost:7397/wg/activity/index
参数:
{
“name”:”小傅哥”,
“uid”:”10001”
}

GET 请求对象入参测试,验证通过。同时返回结果已经被统一封装。

五、本章小结

本章执行器的加入是为了解决会话流程中的冗余内容,让各个类的职责更加清晰。这也是软件设计中很常用的手段。
到目前我们所示实现的功能,大家可以对网关的核心处理有了一个大概的轮廓,通过协议的解析到会话的启动,以及封装入参和出参结果。可以回顾一下
当你现在对有些流程的拆分还不是太理解时也没关系,毕竟这些也是经过经验积累的结果,慢慢吸收这些复杂场景的设计思考和落地案例,让它们成为自己的经验。

《API网关》第7章:权限认证组件(Shiro+Jwt)

一、学习指引

为什么你的工程做不到单元测试?

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。但在实际开发中大部分实际情况下,都没有做单元测试。做的只是为了测某个功能点,而启动的一整条功能测试。

那么之所以出现这样的情况,主要在于系统功能的设计和开发中没有对功能节点进行拆分,使很多流程的边界不清晰。就像把某一功能点直接编写在业务流程中,当你发现这一块功能点有问题的时候,却没法独立测试。只能启动整个链路的功能,并传入与一些与要关注核心模块无关的参数信息。

所以我们在做功能的设计和实现时,要注意这些模块的边界,并让它们尽可能的先保持独立运行后,依照设计原则编入整个流程中去。这样才能真的满足单元测试,否则你在做的都是整块的大的流程验证而已。

二、权限校验设计

在我们的实现的API网关中,当接收 HTTP 请求以后,开始调用对应的 RPC 接口前,其实还应该做一步权限验证。也就是说你当前调用的 HTTP 接口是否含带了我授予的 Token 信息,这个 Token 是否在有效期范围等控制,这样才能保证一个 HTTP 的调用和返回结果是安全可靠的。

关于网关中权限的校验会使用到 Shiro + Jwt,同时还要提供单独的 Handler 来处理 Netty 中的通信对信息的校验处理。但鉴于这部分属于两块功能,所以本章只先完成关于 Shiro + Jwt 部分。
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单。对比于 Spring Security,可能没有做的功能强大,但是在实际工作时并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。

三、权限校验实现

  1. 工程结构

执行器流程核心关系,如图7-2所示

Shiro、Jwt 是两套东西,Shiro 是安全验证框架,Jwt 是(JSON Web Tokens)是一套 JSON 网络令牌,一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。
https://jwt.io/
https://shiro.apache.org/

而本章所实现的功能就是简单的把这两套功能嵌入到网关服务中。

  1. Shiro 使用案例

这里我们做一个 Shiro 的小案例,方便大家理解。这个案例的目的在于验证用于的身份信息。

test-shiro.ini

[users]
xiaofuge=123
xiaobai=456

案例

// 1. 获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory(“classpath:test-shiro.ini”);

// 2. 得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

// 3. 得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();

// 4. 默认提供的验证方式;UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(“xiaofuge”, “1234”);

try {
//5.1、登录,即身份验证
subject.login(token);
} catch (AuthenticationException e) {
//5.2、身份验证失败
System.out.println(“身份验证失败”);
}
System.out.println(subject.isAuthenticated() ? “验证成功” : “验证失败”);

// 6. 退出
subject.logout();

这是一个简单的 Shrio 测试案例,用于验证用户 xiaofuge 的密码 123,如果是1234 会提示验证失败。

测试结果

14:08:48.497 [main] DEBUG org.apache.shiro.authc.credential.SimpleCredentialsMatcher - Both credentials arguments can be easily converted to byte arrays. Performing array equals comparison
身份验证失败
验证失败

Process finished with exit code 0

  1. Jwt 使用案例

3.1 编码

public static String encode(String issuer, long ttlMillis, Map<String, Object> claims) {
if (null == claims) {
claims = new HashMap<>();
}
// 签发时间(iat):荷载部分的标准字段之一
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 签发操作
JwtBuilder builder = Jwts.builder()
// 荷载部分
.setClaims(claims)
// 签发时间
.setIssuedAt(now)
// 签发人;类似 userId、userName
.setSubject(issuer)
// 设置生成签名的算法和秘钥
.signWith(SignatureAlgorithm.HS256, signingKey);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
// 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
builder.setExpiration(exp);
}
return builder.compact();
}

3.2 解码

public static Claims decode(String token) {
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(signingKey)
// 设置需要解析的 jwt
.parseClaimsJws(token)
.getBody();
}

3.3 测试

@Test
public void test_awt() {
String issuer = “xiaofuge”;
long ttlMillis = 7 * 24 * 60 * 60 * 1000L;
Map<String, Object> claims = new HashMap<>();
claims.put(“key”, “xiaofuge”);
// 编码
String token = JwtUtil.encode(issuer, ttlMillis, claims);
System.out.println(token);
// 解码
Claims parser = JwtUtil.decode(token);
System.out.println(parser.getSubject());
}

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4aWFvZnVnZSIsImV4cCI6MTY2NDY5MTAyNywiaWF0IjoxNjY0MDg2MjI3LCJrZXkiOiJ4aWFvZnVnZSJ9.AaoJLKIBy1nsyrm-wpnwKylSTDWlWNAnVBo6Z03XttM
xiaofuge

Process finished with exit code 0

Jwt 的创建核心在于提供一个解码的私钥,你需要保存好,只有你拿到这个私钥才能解开 token 获得里面的信息,否则其他人都是不可以解开的。

  1. 认证服务

因为我们这里不只是用户名密码验证,也会在后续扩展一些其他的网络请求信息或者授权操作,所以我们需要单独提供一个 AuthenticationToken 和 AuthorizingRealm 验证领域的实现类。两个的实现比较比较简单直接参考源码即可。这里主要体现下认证服务。

源码详见:cn.bugstack.gateway.authorization.auth.AuthService

public class AuthService implements IAuth {
private Subject subject;
public AuthService() {
// 1. 获取 SecurityManager 工厂,此处使用 shiro.ini 配置文件初始化 SecurityManager
Factory factory = new IniSecurityManagerFactory(“classpath:shiro.ini”);
// 2. 得到 SecurityManager 实例 并绑定给 SecurityUtils
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3. 得到 Subject 及 Token
this.subject = SecurityUtils.getSubject();
}

@Override
public boolean validate(String id, String token) {
    try {
        // 身份验证
        subject.login(new GatewayAuthorizingToken(id, token));
        // 返回结果
        return subject.isAuthenticated();
    } finally {
        // 退出
        subject.logout();
    }
}

}

验证服务的实现,主要是包装了Shiro 和 Jwt 两部分的能力。这样后面就可以把这个服务提供给 Netty 网络通信部分来使用了。

四、功能测试

单元测试

@Test
public void test_auth_service() {
IAuth auth = new AuthService();
boolean validate = auth.validate(“DPij8iUY”, “eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4aWFvZnVnZSIsImV4cCI6MTY2NDY3ODA5MCwiaWF0IjoxNjY0MDczMjkwLCJrZXkiOiJ4aWFvZnVnZSJ9.QXi2XnJGL_T0Q1vBsN8kGKV98p6xsfx6l4wAelXZp3I”);
System.out.println(validate ? “验证成功” : “验证失败”);
}

首先我们使用测试类生成一个 Jwt 的 Token 码,之后把这个信息传递给 validate 进行验证。可以尝试更改 Token 测试验证是否通过。

测试结果

验证成功

Process finished with exit code 0

到这里表示我们的功能模式是正常的,验证通过。

五、本章小结

本章的核心内容在于引入 Shiro 和 Jwt 对两部分内容进行包装处理。如果读者对 Shiro 不是太熟悉,没有使用过也可以尝试做下代码中提供的案例。另外可以看下Shiro官网案例;https://github.com/apache/shiro/blob/main/samples/quickstart/src/main/java/Quickstart.java
本章内容并不复杂,主要是把这部分功能独立出来,既可以满足一个功能的单元,也可以在后续扩展到其他逻辑中使用。关于 Netty 通信中接入 Shiro 服务,在下个章节完成。

《API网关》第8章:网关会话鉴权处理

一、学习指引

什么时候编程技能突飞猛进?

熟练、熟练、熟练,当你开始对一个技术的广度边界、深度认知有了很多的积累,并能驾驭这些工具时,就是编程技能突飞猛进的时候。

而这些工具的熟练,往往要来自于项目的学习,通过有深度的需求的解决方案,来积累编程经验。积累的越多也就越能游刃有余的处理各类问题。但这个过程需要很长时间的积累,远不是朝夕就能完成的,也不只是几篇文章就能把你带起飞的。

就像网关的实现,当你完全跟进学习一遍以后,在遇到类似场景时,也可以给出同类的处理手段。并且越是小傅哥在网关开发中添加越多的东西,越能让你有更多的积累。不过你要注意,千万不要只是简单的敲敲代码就完事了,因为思考和设计是代码的落地的指导方案,先把这些重视起来。

二、会话鉴权过程

在第7章引入了 Shiro + JWT 作为鉴权的工具,这个工具就是要用到本章中用于处理网络会话请求中对接口访问信息的一个鉴权处理。

这里你可以思考🤔,一次网络请求经过 Netty 处理可以分为三段;消息接收、请求鉴权、消息处理。这样就由原来我们只是在接收消息后直接把消息协议转换后请求到 RPC 服务,转换为多添加二层来处理简单的消息接收和请求鉴权。这里的请求鉴权就是基于引入的 Shrio + JWT 完成。

为了满足消息的处理和鉴权,本章需要再引入2个Netty消息处理的Handler;AuthorizationHandler、ProtocolDataHandler

【新增】AuthorizationHandler 做接口的鉴权
【新增】ProtocolDataHandler 做协议数据的处理
【修改】GatewayServerHandler 原有网络协议处理类,作为第一个Netty通信管道中的处理类,只做简单参数解析,保存到管道中后即可放行。—— 后续再有一些需要优先处理的逻辑也会放到这个 GatewayServerHandler 类里完成。

三、会话鉴权实现

  1. 工程结构

会话鉴权流程核心关系,如图8-2所示

核心的处理逻辑主要在于添加了 AuthorizationHandler、ProtocolDataHandler 两个处理模块,并对配置到 Configuration 的 IAuth 模块进行调用,以做到鉴权接口的目的。
另外本章在新增加类 AgreementConstants 添加了协议信息的定义,在网络请求处理后按照标准信息返回到接口。
_200(“200”,”访问成功”),
_400(“400”,”接收数据的数据类型不匹配”),
_403(“403”,”服务器拒绝请求”),
_404(“404”,”服务器找不到请求的网页,输入链接有误”),
_500(“500”,”服务器遇到错误,无法完成请求”),
_502(“502”,”服务器作为网关或代理,从上游服务器收到无效响应”);

  1. 配置定义

源码详见:cn.bugstack.gateway.mapping.HttpStatement

public class HttpStatement {

/** 应用名称; */
private String application;
/** 服务接口;RPC、其他 */
private String interfaceName;
/** 服务方法;RPC#method */
private String methodName;
/** 参数类型(RPC 限定单参数注册);new String[]{"java.lang.String"}、new String[]{"cn.bugstack.gateway.rpc.dto.XReq"} */
private String parameterType;
/** 网关接口 */
private String uri;
/** 接口类型;GET、POST、PUT、DELETE */
private HttpCommandType httpCommandType;
/** 是否鉴权;true = 是、false = 否 */
private boolean auth;

}

本章在 HttpStatement 中新添加字段 auth 用于判断是否需要鉴权。
因为在实际的网络请求中,有些接口是不需要鉴权的,只是简单的 Get 请求获取到页面信息即可,用户即使不登录也可以预览这些信息。所以他们不需要做鉴权处理。但像一个用户的个人账户信息,是必须要鉴权后才允许访问的。

  1. 协议解析

源码详见:cn.bugstack.gateway.mapping.GatewayServerHandler

protected void session(ChannelHandlerContext ctx, final Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求【全局】 uri:{} method:{}”, request.uri(), request.method());
try {
// 1. 解析参数
RequestParser requestParser = new RequestParser(request);
String uri = requestParser.getUri();
// 2. 保存信息;HttpStatement
HttpStatement httpStatement = configuration.getHttpStatement(uri);
channel.attr(AgreementConstants.HTTP_STATEMENT).set(httpStatement);
// 3. 放行服务
request.retain();
ctx.fireChannelRead(request);
} catch (Exception e) {
// 4. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._500.getCode(), “网关协议调用失败!” + e.getMessage()));
channel.writeAndFlush(response);
}
}

GatewayServerHandler 处理类由原来的处理数据,修改解析参数,获取 HttpStatement 操作。
因为获取 HttpStatement 后可以保存到管道的属性信息中,所有的这条通信链上都可以获取到,这样到鉴权处理中直接获取到信息就可以操作了。
这里不会获取到会话的信息(gatewaySessionFactory.openSession(uri)),避免如果鉴权都鉴权失败了,创建会话服务也是浪费资源。所以只需要在构造函数中传输 Configuration 即可,用于根据 URI 获取 HttpStatement 网关接口映射信息,方便拿到是否需要鉴权。

  1. 请求鉴权

源码详见:cn.bugstack.gateway.mapping.AuthorizationHandler

protected void session(ChannelHandlerContext ctx, Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求【鉴权】 uri:{} method:{}”, request.uri(), request.method());
try {
HttpStatement httpStatement = channel.attr(AgreementConstants.HTTP_STATEMENT).get();
if (httpStatement.isAuth()) {
try {
// 鉴权信息
String uId = request.headers().get(“uId”);
String token = request.headers().get(“token”);
// 鉴权判断
if (null == token || “”.equals(token)) {
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._400.getCode(), “对不起,你的 token 不合法!”));
channel.writeAndFlush(response);
}
// 鉴权处理;shiro + jwt
boolean status = configuration.authValidate(uId, token);
// 鉴权成功;直接放行
if (status) {
request.retain();
ctx.fireChannelRead(request);
}
// 鉴权失败
else {
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._403.getCode(), “对不起,你无权访问此接口!”));
channel.writeAndFlush(response);
}
} catch (Exception e) {
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._403.getCode(), “对不起,你的鉴权不合法!”));
channel.writeAndFlush(response);
}
}
// 不鉴权放行
else {
request.retain();
ctx.fireChannelRead(request);
}
} catch (Exception e) {
// 4. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._500.getCode(), “网关协议调用失败!” + e.getMessage()));
channel.writeAndFlush(response);
}
}

鉴权的操作首先要获取当前请求的 URI 是否配置了鉴权,如果不鉴权则直接放行。
那么需要鉴权的则调用在第7章首先的 Shiro + JWT 鉴权模型进行处理。不过这里把鉴权模块配置到了 Configuration 配置项中,做统一的管理。
鉴权成功则放行,鉴权失败则返回。

  1. 请求处理

源码详见:cn.bugstack.gateway.mapping.ProtocolDataHandler

protected void session(ChannelHandlerContext ctx, Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求【消息】 uri:{} method:{}”, request.uri(), request.method());
try {
// 1. 解析请求参数
RequestParser requestParser = new RequestParser(request);
String uri = requestParser.getUri();
if (null == uri) return;
Map<String, Object> args = requestParser.parse();
// 2. 调用会话服务
GatewaySession gatewaySession = gatewaySessionFactory.openSession(uri);
IGenericReference reference = gatewaySession.getMapper();
SessionResult result = reference.$invoke(args);
// 3. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(“0000”.equals(result.getCode()) ? GatewayResultMessage.buildSuccess(result.getData()) : GatewayResultMessage.buildError(AgreementConstants.ResponseCode._404.getCode(), “网关协议调用失败!”));
channel.writeAndFlush(response);
} catch (Exception e) {
// 4. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._502.getCode(), “网关协议调用失败!” + e.getMessage()));
channel.writeAndFlush(response);
}
}

ProtocolDataHandler 相当于把原来在 GatewayServerHandler 类中协议数据的处理,迁移到这里了。也就是前面的鉴权处理完毕,调用到这里。
另外这里添加了一些标准的CODE码,200、404、502

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;

操作:测试前先确保 api-gateway-test-provider 启动完成

RPC服务

@Service(version = “1.0.0”)
public class ActivityBooth implements IActivityBooth {

@Override
public String sayHi(String str) {
    return "hi " + str + " by api-gateway-test-provider";
}

@Override
public String insert(XReq req) {
    return "hi " + JSON.toJSONString(req) + " by api-gateway-test-provider";
}

@Override
public String test(String str, XReq req) {
    return "hi " + str + JSON.toJSONString(req) + " by api-gateway-test-provider";
}

}

启动日志

2022-10-15 10:15:52.559 INFO 90356 — [ main] org.apache.zookeeper.ZooKeeper : Client environment:user.name=fuzhengwei
2022-10-15 10:15:52.559 INFO 90356 — [ main] org.apache.zookeeper.ZooKeeper : Client environment:user.home=/Users/fuzhengwei
2022-10-15 10:15:52.559 INFO 90356 — [ main] org.apache.zookeeper.ZooKeeper : Client environment:user.dir=/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-test-provider
2022-10-15 10:15:52.560 INFO 90356 — [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@58ec7116
2022-10-15 10:15:52.567 INFO 90356 — [ main] o.a.c.f.imps.CuratorFrameworkImpl : Default schema
2022-10-15 10:15:52.568 INFO 90356 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2022-10-15 10:15:52.575 INFO 90356 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to localhost/127.0.0.1:2181, initiating session
2022-10-15 10:15:52.586 INFO 90356 — [localhost:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x10000004638000a, negotiated timeout = 40000
2022-10-15 10:15:52.591 INFO 90356 — [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2022-10-15 10:15:53.098 INFO 90356 — [ main] c.b.gateway.ApiGatewayTestApplication : Started ApiGatewayTestApplication in 3.161 seconds (JVM running for 4.338)

这一章节新增了2个额外的 RPC 接口方法,用于各类场景的测试验证。

  1. 启动网关

public void test_gateway() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
Configuration configuration = new Configuration();
configuration.registryConfig(“api-gateway-test”, “zookeeper://127.0.0.1:2181”, “cn.bugstack.gateway.rpc.IActivityBooth”, “1.0.0”);
HttpStatement httpStatement01 = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“java.lang.String”,
“/wg/activity/sayHi”,
HttpCommandType.GET,
false);
HttpStatement httpStatement02 = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“insert”,
“cn.bugstack.gateway.rpc.dto.XReq”,
“/wg/activity/insert”,
HttpCommandType.POST,
true);
configuration.addMapper(httpStatement01);
configuration.addMapper(httpStatement02);
// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);
// 3. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(configuration, gatewaySessionFactory);
Future future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException(“netty server start error channel is null”);
while (!channel.isActive()) {
logger.info(“netty server gateway start Ing …”);
Thread.sleep(500);
}
logger.info(“netty server gateway start Done! {}”, channel.localAddress());
Thread.sleep(Long.MAX_VALUE);
}

这部分测试内容包括;创建配置信息加载注册、基于配置构建会话工厂、创建启动网关网络服务,注意这里注册的网关接口内容丰富了,包括:
服务;api-gateway-test
接口;cn.bugstack.gateway.rpc.IActivityBooth
方法;sayHi/insert
映射;/wg/activity/(sayHi/insert) —— 也就是 http://localhost:7397/wg/activity/(sayHi/insert) 调用的部分
鉴权;true/fase —— 本章新增参数,用于处理是否鉴权。

之后就是创建会话并把这个信息传递给创建网络服务中,方便在 GatewayServerHandler 中使用。

  1. 功能测试
    3.1 鉴权

POST测试

http://localhost:7397/wg/activity/insert
参数:
{
“name”:”小傅哥”,
“uid”:”10001”
}

Headers:
{
token:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4aWFvZnVnZSIsImV4cCI6MTY2NjQwNDAxMiwiaWF0IjoxNjY1Nzk5MjEyLCJrZXkiOiJ4aWFvZnVnZSJ9.Vs-ObO5OF2pYr7jkt0N4goq0hErOZNdyqfacHzbkfHM
uId:xiaofuge
}

3.2 不鉴权

GET测试

测试:
http://localhost:7397/wg/activity/sayHi
参数:
{
“str”: “10001”
}

GET 请求单入参测试,验证通过

五、本章小结

本章处理了网关中必备的核心模块,鉴权校验。在开发实现的过程又是一次小的重构,不断的累加功能,也不断地拆解实现,让每一个类负责各自的职责,做到解耦。
其实这样细分来看,每一个章节的复杂度都不会有多高,只要大家跟着学习就都能学会。
另外在网关的实现中还有类似流量整形、半包粘包协议、熔断、降级等各类处理过程,也会随着后续开发不断的完善。

《API网关》第9章:网关注册中心服务初始创建

一、学习指引

别一条道跑到黑

当我们在做需求开发的时候,不能只是低着头一直写,还要抬头看看。就像刨地一样,别跑的太远了回头一看,已经歪的不成样子了。

所以当你要完成一个接口到一堆接口的时候,提前先给自己验证验证,再给前端用用。别都开发到最后了,前端和你说咱们接口定义的样子好像和你写的不一样。

二、流程梳理

本实现第1部分【通信】的1~8章后,其实已经开发出了一个出具模型的核心通信组件。那么现在你能想象出我们要怎么使用这个组件吗,接下来要完成什么,才能让串联起整个结构呢?这个就是本章要思考和架构设计的内容。—— 抬头看看

如图所示,api-gateway-core 是最核心的通信层。那么它还需要把注册的网关接口在通信核心服务中启动起来。那么怎么启动呢?
这个启动过程首先来自于 api-gateway-sdk 向 api-gateway-center 推送注册接口,之后在通过网关引擎 api-gateway-engin 拉取接口并在本地服务完成注册。
最后再调用到网关接口时,则是通过 api-gateway-core 调用到对应的 RPC 应用中。
那么 api-gateway-sdk 并不是主要工程,没有它的是可以通过 api-gateway-admin 配置。所以 在整个流程中 api-gateway-center、api-gateway-core 是两个核心工程,能更好的串联流程。

三、注册中心

接下来的一段时间里,小傅哥会带着大家来开发注册中心的功能。注册中心主要提供RPC接口的注册和接口的拉取使用。对于一些负载的控制也会在后续陆续完善。

那么本章节我们先来初始化这样一个工程,这个工程采用简单的DDD工程模型结构开发,对外提供 HTTP 接口。之所以提供 HTTP 接口是因为可以引入更少的组件,也能满足需求。当然你也可以使用 MQ、Socket、ZK 等方式进行处理。

  1. 工程结构

  2. 初始功能

本章主要搭建一个初始简单结构的 DDD 工程,并建一张简单的 API 映射数据库表,通过工程可以查询到表信息。

2.1 数据库表

CREATE TABLE http_statement (
id bigint(11) NOT NULL AUTO_INCREMENT,
application varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘应用名称’,
interface_name varchar(256) COLLATE utf8_bin NOT NULL COMMENT ‘服务接口;RPC、其他’,
method_name varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘ 服务方法;RPC#method’,
parameter_type varchar(256) COLLATE utf8_bin NOT NULL COMMENT ‘参数类型(RPC 限定单参数注册);new String[]{“java.lang.String”}、new String[]{“cn.bugstack.gateway.rpc.dto.XReq”}’,
uri varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘网关接口’,
http_command_type varchar(32) COLLATE utf8_bin NOT NULL COMMENT ‘接口类型;GET、POST、PUT、DELETE’,
auth int(4) NOT NULL DEFAULT ‘0’ COMMENT ‘true = 1是、false = 0否’,
create_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

2.2 查询数据

@RestController
@RequestMapping(“/api”)
public class ApiGatewayController {

private Logger logger = LoggerFactory.getLogger(ApiGatewayController.class);

@Resource
private IApiService apiService;

@GetMapping(value = "list", produces = "application/json;charset=utf-8")
public List<ApiData> getAnswerMap(){
    return apiService.queryHttpStatementList();
}

}

代码流程很简单,只是通过这样一个对外的 API 接口提供查询即可。这部分功能后续会陆续完善。

四、测试验证

启动 api-gateway-center

. ____ _ __ _ _
/\ / _ __ _ ()_ __ __ _ \ \ \
( ( )_
_ | ‘_ | ‘| | ‘ / ` | \ \ \
\/ _)| |)| | | | | || (_| | ) ) ) )
‘ |
| .|| ||| |_, | / / / /
=========|
|==============|_/=//_//
:: Spring Boot :: (v2.6.0)

2022-10-22 17:04:30.748 INFO 3893 — [ main] c.b.g.center.ApiGatewayApplication : Starting ApiGatewayApplication using Java 1.8.0_311 on MacBook-Pro.local with PID 3893 (/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/api-gateway-center-01/target/classes started by fuzhengwei in /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center)
2022-10-22 17:04:30.750 INFO 3893 — [ main] c.b.g.center.ApiGatewayApplication : No active profile set, falling back to default profiles: default
2

访问接口;curl http://localhost/api/list —— 你也可以放到浏览器访问

小傅哥在本章节留了个隐藏bug,如果没有找到,你将不能正常启动测试。

五、本章小结

本章工厂代码内容不多,重要在清楚整体的设计和明确接下来我们要完成的事情。这样在以后的内容实现中,才会更加清晰。
此外本章节工程是一个简单版DDD,它不对外提供 RPC 接口,整体工程模块更简单,也能减少包之间对象的转换,也是一个非常不错的结构。正好借着这样的一个功能实现,展示给大家,多一种搭建公司的思路。

《API网关》第10章:网关注册中心库表结构设计

一、学习指引

看库表结构就能看出代码好坏!

看一个系统开发的好坏,第一眼先去看这个系统的库表设计,如果库表设计的很好,那么基本代码逻辑不会差哪去。如果差,肯定设计库表的人与编写代码的人不是一个人!

这是为啥呢?因为库表的设计,其实就是数据结构的设计,所有的算法逻辑支撑都来自于数据结构的合理搭建。如果库表不清晰,那么所承接实现出来的代码也会非常别扭。要这个数据不好拿,要那个数据不好取,流程间耦合,服务间牵连。所以才说好的代码实现,要做好库表设计。

二、库表诉求

网关注册中心,是一个多边服务,管理的是RPC服务向网关通信层的关联注册。

这就像美团外面平台,一手管理着商家、一手服务着用户。让用户的订单又商家生产,再配送给用户。而我们的网关注册中心也是这样,一手管理着通信,一手管理着服务。让RPC注册到通信层,在用户调用HTTP接口时,可以把协议转换后调用到对应的RPC服务上。

基于以上这样的情况,所以我们要在注册中心维护;网关通信表、RPC服务表、以及两个表的关联表。

三、库表设计

整个系统目前所需的表为如图所示;

网关服务:指的是管理 api-gateway-core 通信组件的服务,这些服务被分为多组。例如公司中有交易组、账务组、营销组,按照不同组来分配对应的网关服务,各自在注册网关接口时可以做到压力分摊。
网关明细:指的的是每一组服务下,有N个通信核心服务,这N个服务需要把所有注册到自己组上的RPC接口,都拉取下来完成接口映射。当有HTTP访问的时候,可以均衡的打到这些服务上。
网关分配:那么每个应用该注册到哪个网关里,就由这个表来控制。当你注册应用的时候,会有一个选项,问你要注册到哪个网关上。这部分一般也可以在指定的范围内,动态分配
应用系统:描述一个RPC应用
应用接口:描述这个RPC应用下有多少个接口,以及明细
应用方法:描述这个RPC应用接口下的方法信息,包括;方法名称、入参信息、出参信息【可选】、请求类型、请求地址以及是否鉴权。因为网关返回的结果是 HTTP 方式,所以并不需要强对象描述信息,所以出参信息可选

四、库表语句

上图,有错误❎,你能发现吗?记录到你的作业里。

/*
Navicat Premium Data Transfer

Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 50639
Source Host : localhost:3306
Source Schema : api-gateway

Target Server Type : MySQL
Target Server Version : 50639
File Encoding : 65001

Date: 29/10/2022 17:09:33
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;


– Table structure for application_interface


DROP TABLE IF EXISTS application_interface;
CREATE TABLE application_interface (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
system_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统标识’,
interface_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘接口标识’,
interface_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘接口名称’,
interface_version varchar(16) COLLATE utf8_bin DEFAULT NULL COMMENT ‘接口版本’,
create_time datetime DEFAULT NULL COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of application_interface


BEGIN;
COMMIT;


– Table structure for application_interface_method


DROP TABLE IF EXISTS application_interface_method;
CREATE TABLE application_interface_method (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
system_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统标识’,
interface_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘接口标识’,
method_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘方法标识’,
method_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘方法名称’,
parameter_type varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT ‘参数类型;(RPC 限定单参数注册);new String[]{“java.lang.String”}、new String[]{“cn.bugstack.gateway.rpc.dto.XReq”}’,
uri varchar(126) COLLATE utf8_bin DEFAULT NULL COMMENT ‘网关接口’,
http_command_type varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT ‘接口类型;GET、POST、PUT、DELETE’,
auth int(4) DEFAULT NULL COMMENT ‘true = 1是、false = 0否’,
create_time datetime DEFAULT NULL COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of application_interface_method


BEGIN;
COMMIT;


– Table structure for application_system


DROP TABLE IF EXISTS application_system;
CREATE TABLE application_system (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
system_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统标识’,
system_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统名称’,
system_type varchar(4) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统类型;RPC、HTTP’,
system_registry varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘注册中心’,
create_time datetime DEFAULT NULL COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of application_system


BEGIN;
COMMIT;


– Table structure for gateway_distribution


DROP TABLE IF EXISTS gateway_distribution;
CREATE TABLE gateway_distribution (
id int(11) NOT NULL COMMENT ‘自增主键’,
group_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘分组标识’,
gateway_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘网关标识’,
system_id varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统标识’,
system_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘系统名称’,
create_time datetime DEFAULT NULL COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of gateway_distribution


BEGIN;
COMMIT;


– Table structure for gateway_server


DROP TABLE IF EXISTS gateway_server;
CREATE TABLE gateway_server (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
group_id varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT ‘分组标识’,
group_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘分组名称’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of gateway_server


BEGIN;
COMMIT;


– Table structure for gateway_server_detail


DROP TABLE IF EXISTS gateway_server_detail;
CREATE TABLE gateway_server_detail (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
gateway_id varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT ‘网关标识’,
gateway_name varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT ‘网关名称’,
gateway_address varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT ‘网关地址:127.0.0.1’,
status varchar(4) COLLATE utf8_bin DEFAULT NULL COMMENT ‘服务状态:0不可用、1可使用’,
create_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
PRIMARY KEY (id),
UNIQUE KEY idx_gateway_id (gateway_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


– Records of gateway_server_detail


BEGIN;
COMMIT;


– Table structure for http_statement


DROP TABLE IF EXISTS http_statement;
CREATE TABLE http_statement (
id bigint(11) NOT NULL AUTO_INCREMENT,
application varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘应用名称’,
interface_name varchar(256) COLLATE utf8_bin NOT NULL COMMENT ‘服务接口;RPC、其他’,
method_name varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘ 服务方法;RPC#method’,
parameter_type varchar(256) COLLATE utf8_bin NOT NULL COMMENT ‘参数类型(RPC 限定单参数注册);new String[]{“java.lang.String”}、new String[]{“cn.bugstack.gateway.rpc.dto.XReq”}’,
uri varchar(128) COLLATE utf8_bin NOT NULL COMMENT ‘网关接口’,
http_command_type varchar(32) COLLATE utf8_bin NOT NULL COMMENT ‘接口类型;GET、POST、PUT、DELETE’,
auth int(4) NOT NULL DEFAULT ‘0’ COMMENT ‘true = 1是、false = 0否’,
create_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘创建时间’,
update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

五、本章小结

库表的设计与要实现的领域服务其实两者并不是分开独立存在的,在设计库表时候也基本在头脑中过了一遍领域模块要实现哪些,怎么调用底层的数据来封装。所以读者在基于这样的方式以后设计自己的业务逻辑库表的时候,也可以思考下底层这些数据该如何支撑自己的业务逻辑。
目前本章节只设计了最核心所需要的库表,后续还会结合开发诉求在完善其他逻辑模块。例如对网关服务的探活检测,消息的推拉维护,本地维护文件在网关注册中心链接失败时候也可以正常启动等。

《API网关》第11章:网关注册算力节点领域服务实现

一、学习指引

编程开发就像乐高积木

从我们开始做 API 网关开发开始,就像是搭建乐高积木,把一个整体的诉求按照各个模块来逐步搭建处理。当你能把一整条链路的功能拆解成各个模块的组件来实现,其实就会变得非常简单、逻辑清晰、易于扩展。

二、服务注册

网关注册中心首先要接收来自各个网关服务的注册,任何一组用于处理 HTTP 协议请求的网关算力节点,都要注册到网关中心进行统一维护和管理。因为只有注册到网关中心才能把 RPC 服务分配到各个网关算力节点上进行使用。

网关中心维护网关算力节点的库表; gateway_server、gateway_server_detail 两个表来维护数据。
那么本章我们先来开发这样一块的功能接口,允许外部通过 HTTP 接口进行注册服务。
那么目前先不用引入 zookeeper 这样的注册中心,探活服务。后期功能地额外完成后再进行陆续补充。

三、功能实现

  1. 工程结构

库表:api-gateway-center-03/api-gateway.sql - 库表有字段更新

  1. 注册网关

源码详见:cn.bugstack.gateway.center.interfaces.GatewayConfigManage

/**

  • 注册网关服务节点
  • @param groupId 分组标识
  • @param gatewayId 网关标识
  • @param gatewayName 网关名称
  • @param gatewayAddress 网关地址
  • @return 注册状态
    */
    @PostMapping(value = “registerGateway”)
    public Result registerGatewayServerNode(@RequestParam String groupId, @RequestParam String gatewayId, @RequestParam String gatewayName, @RequestParam String gatewayAddress) {
    try {
    logger.info(“注册网关服务节点 gatewayId:{} gatewayName:{} gatewayAddress:{}”, gatewayId, gatewayName, gatewayAddress);
    boolean done = configManageService.registerGatewayServerNode(groupId, gatewayId, gatewayName, gatewayAddress);
    return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), done);
    } catch (Exception e) {
    logger.error(“注册网关服务节点异常”, e);
    return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
    }
    }

在 interfaces 接口层创建 GatewayConfigManage 类,提供网关配置管理的相关接口功能。
目前本章主要提供了网关服务配置的查询和注册网关节点的操作。代码流程比较简单,读者可以参考源码调试即可。

  1. 注册逻辑

源码详见:cn.bugstack.gateway.center.domain.manage.service.ConfigManageService

@Override
public boolean registerGatewayServerNode(String groupId, String gatewayId, String gatewayName, String gatewayAddress) {
GatewayServerDetailVO gatewayServerDetailVO = configManageRepository.queryGatewayServerDetail(gatewayId, gatewayAddress);
if (null == gatewayServerDetailVO) {
try {
return configManageRepository.registerGatewayServerNode(groupId, gatewayId, gatewayName, gatewayAddress, Constants.GatewayStatus.Available);
} catch (DuplicateKeyException e) {
return configManageRepository.updateGatewayStatus(gatewayId, gatewayAddress, Constants.GatewayStatus.Available);
}
} else {
return configManageRepository.updateGatewayStatus(gatewayId, gatewayAddress, Constants.GatewayStatus.Available);
}
}

网关主要要判断下当前网关是否注册过,如果注册过则进行更新。是否注册过根据网关ID和对应的IP一起做联合索引。
为了确保服务端重启注册故障,加了一段唯一索引判断,避免服务端发起连续调用。而数据库已经存在数据抛出索引冲突的异常。冲突后则直接更新。

四、测试验证

在测试之前要更新一下数据库表文件。库表:api-gateway-center-03/api-gateway.sql - 库表有字段更新
本章的测试目的在于单元测试中模拟网关注册信息,记录信息到数据库。如果网关算力节点信息已经存在,则进行状态更新。

单元测试

@Test
public void test_registerGatewayServerNode() {
configManageService.registerGatewayServerNode(“10001”, “api-gateway-g1”, “电商支付网关”, “127.0.0.196”);
configManageService.registerGatewayServerNode(“10001”, “api-gateway-g2”, “电商支付网关”, “127.0.0.197”);
configManageService.registerGatewayServerNode(“10001”, “api-gateway-g3”, “电商配送网关”, “127.0.0.198”);
}

通过测试向网关注册中心注册了3组网关服务,如果数据库已经存在数据则会进行更新操作。逻辑主要体现在 cn.bugstack.gateway.center.domain.manage.service.ConfigManageService#registerGatewayServerNode

五、本章小结

本章是对网关注册中心三个大块逻辑中的一个领域实现,三个大块逻辑包括;网关注册维护、系统服务注册和管理、网关与服务分配。后续我们陆续实现这些功能。
当网关这些基本的功能实现完成后,再陆续开发其他模块的功能,慢慢的把这一整套乐高的组件实现完成并组装出来。

《API网关》第12章:网关注册服务接口领域服务实现
一、学习指引

编程中大部分逻辑并不复杂

无论是框架源码,还是我们日常实现的业务需求,其实最核心的代码往往只占整个系统开发的20%,甚至可能更少。其他大部分代码都是为了支撑那20%顺利运行的辅助流程。

虽然那80%的代码并不那么核心,也并不复杂。但在维护成本上往往会超过那20%的代码量。就像家里的钱很重要,但你只要放在一个地方存好就可以了。而家里的一堆一堆的衣服、袜子、鞋子,却可能因为存放不当,让家里变得物品变得混乱。

二、接口注册

第11章我们实现了网关的服务注册,接下来在本章我们要提供用于RPC服务注册的接口。

在一个RPC的服务注册中,需要包括三个部分;RPC服务系统信息、这个服务下的所有接口信息、接口下的所有方法信息。分批的向网关中心完成注册操作。所有信息注册完成后,才能让网关算力服务与RPC的接口进行关联,也就是把RPC接口分配到处理的网关服务上去。这,就是注册中心的作用。

网关中心维护RPC服务注册的库表;application_system、application_interface、application_interface_method
那么本章我们先来开发这样一块的功能接口,允许外部通过 HTTP 接口进行注册服务。

三、功能实现

  1. 工程结构

库表:api-gateway-center-04/api-gateway.sql - 库表唯一索引更新

  1. 注册服务

在注册RPC服务中,分为;服务的系统信息、接口信息、方法信息,所以这里提供了3个接口。

源码详见:cn.bugstack.gateway.center.interfaces.RpcRegisterManage

@PostMapping(value = “registerApplication”, produces = “application/json;charset=utf-8”)
public Result registerApplication(@RequestParam String systemId,
@RequestParam String systemName,
@RequestParam String systemType,
@RequestParam String systemRegistry) {
try {
logger.info(“注册应用服务 systemId:{}”, systemId);
ApplicationSystemVO applicationSystemVO = new ApplicationSystemVO();
applicationSystemVO.setSystemId(systemId);
applicationSystemVO.setSystemName(systemName);
applicationSystemVO.setSystemType(systemType);
applicationSystemVO.setSystemRegistry(systemRegistry);
registerManageService.registerApplication(applicationSystemVO);
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), true);
} catch (DuplicateKeyException e) {
logger.warn(“注册应用服务重复 systemId:{}”, systemId, e);
return new Result<>(ResponseCode.INDEX_DUP.getCode(), e.getMessage(), true);
} catch (Exception e) {
logger.error(“注册应用服务失败 systemId:{}”, systemId, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
}
}
cn.bugstack.gateway.center.interfaces.RpcRegisterManage
@PostMapping(value = “registerApplicationInterface”, produces = “application/json;charset=utf-8”)
public Result registerApplicationInterface(@RequestParam String systemId,
@RequestParam String interfaceId,
@RequestParam String interfaceName,
@RequestParam String interfaceVersion) {
try {
logger.info(“注册应用接口 systemId:{} interfaceId:{}”, systemId, interfaceId);
ApplicationInterfaceVO applicationInterfaceVO = new ApplicationInterfaceVO();
applicationInterfaceVO.setSystemId(systemId);
applicationInterfaceVO.setInterfaceId(interfaceId);
applicationInterfaceVO.setInterfaceName(interfaceName);
applicationInterfaceVO.setInterfaceVersion(interfaceVersion);
registerManageService.registerApplicationInterface(applicationInterfaceVO);
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), true);
} catch (DuplicateKeyException e) {
logger.warn(“注册应用接口重复 systemId:{} interfaceId:{}”, systemId, interfaceId);
return new Result<>(ResponseCode.INDEX_DUP.getCode(), e.getMessage(), true);
} catch (Exception e) {
logger.error(“注册应用接口失败 systemId:{}”, systemId, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
}
}

@PostMapping(value = “registerApplicationInterfaceMethod”, produces = “application/json;charset=utf-8”)
public Result registerApplicationInterfaceMethod(@RequestParam String systemId,
@RequestParam String interfaceId,
@RequestParam String methodId,
@RequestParam String methodName,
@RequestParam String parameter_type,
@RequestParam String uri,
@RequestParam String httpCommandType,
@RequestParam Integer auth) {
try {
logger.info(“注册应用接口方法 systemId:{} interfaceId:{} methodId:{}”, systemId, interfaceId, methodId);
ApplicationInterfaceMethodVO applicationInterfaceVO = new ApplicationInterfaceMethodVO();
applicationInterfaceVO.setSystemId(systemId);
applicationInterfaceVO.setInterfaceId(interfaceId);
applicationInterfaceVO.setMethodId(methodId);
applicationInterfaceVO.setMethodName(methodName);
applicationInterfaceVO.setParameterType(parameter_type);
applicationInterfaceVO.setUri(uri);
applicationInterfaceVO.setHttpCommandType(httpCommandType);
applicationInterfaceVO.setAuth(auth);
registerManageService.registerApplicationInterfaceMethod(applicationInterfaceVO);
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), true);
} catch (DuplicateKeyException e) {
logger.warn(“注册应用接口重复 systemId:{} interfaceId:{}”, systemId, interfaceId);
return new Result<>(ResponseCode.INDEX_DUP.getCode(), e.getMessage(), true);
} catch (Exception e) {
logger.error(“注册应用接口失败 systemId:{}”, systemId, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
}
}

在 RpcRegisterManage,RPC 服务注册管理下提供了三个接口;registerApplication、registerApplicationInterface、registerApplicationInterfaceMethod,分别用于注册不同的信息。

四、测试验证

在测试之前要更新一下数据库表文件。库表:api-gateway-center-04/api-gateway.sql - 库表有索引更新
本章的测试目的在于单元测试中模拟RPC注册信息,记录信息到数据库。如果注册信息已存在,则会返回幂等。这里可以先做一次查询处理,减少撞库。但为了幂等体现,暂时先不做插入数据库的查询处理。

单元测试

@Test
public void test_registerApplicationInterfaceMethod() {
ApplicationInterfaceMethodVO applicationInterfaceVO01 = new ApplicationInterfaceMethodVO();
applicationInterfaceVO01.setSystemId(“api-gateway-test”);
applicationInterfaceVO01.setInterfaceId(“cn.bugstack.gateway.rpc.IActivityBooth”);
applicationInterfaceVO01.setMethodId(“sayHi”);
applicationInterfaceVO01.setMethodName(“测试方法”);
applicationInterfaceVO01.setParameterType(“java.lang.String”);
applicationInterfaceVO01.setUri(“/wg/activity/sayHi”);
applicationInterfaceVO01.setHttpCommandType(“GET”);
applicationInterfaceVO01.setAuth(0);
registerManageService.registerApplicationInterfaceMethod(applicationInterfaceVO01);

ApplicationInterfaceMethodVO applicationInterfaceVO02 = new ApplicationInterfaceMethodVO();
applicationInterfaceVO02.setSystemId("api-gateway-test");
applicationInterfaceVO02.setInterfaceId("cn.bugstack.gateway.rpc.IActivityBooth");
applicationInterfaceVO02.setMethodId("insert");
applicationInterfaceVO02.setMethodName("插入方法");
applicationInterfaceVO02.setParameterType("cn.bugstack.gateway.rpc.dto.XReq");
applicationInterfaceVO02.setUri("/wg/activity/insert");
applicationInterfaceVO02.setHttpCommandType("POST");
applicationInterfaceVO02.setAuth(1);
registerManageService.registerApplicationInterfaceMethod(applicationInterfaceVO02);

}

在单元测试中主要体现了注册接口方法的内容,除此之外还有注册服务和接口信息的方法,读者可以自行验证。

五、本章小结

本章是承接上一章对网关注册中心三个大块逻辑中的一个领域实现,三个大块逻辑包括;网关注册维护、系统服务注册和管理、网关与服务分配。后续我们陆续实现这些功能。
当网关这些基本的功能实现完成后,再陆续开发其他模块的功能,慢慢的把这一整套乐高的组件实现完成并组装出来。

《API网关》第13章:服务发现组件搭建和注册网关连接

一、学习指引

你不能把什么都扔给调用方去处理

哪有什么岁月静好,编程开发也一样。如果 Spring、MyBatis 等这些基础框架天天出问题,总是被迫让你升级改造,那你的公司项目将为此付出巨大的成本。就像大一些的互联网中升级一次xxx组件,最起码耗费上百万的人力成本。

而我们开发一个API网关也一样,如果把所有的服务都没有合理的封装,而是直接对外,那么可能前期确实开发的很快。但软件开发是一个长周期发展的事情,越到后期的维护和迭代成本将会是越大。—— 可能就是从十几行不起眼的烂代码,衍生到几十行的需求迭代,再到上千行打补丁贴膏药。

二、组件封装

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

api-gateway-core 是网关的算力服务,api-gateway-center 是网关的注册中心,那么为了把这块服务链接起来,中间则需要一套 api-gateway-engin 网关的引擎,用于启动网关的算力服务。
但由于启动网关的算力服务还需要一些功能的整合,来包装网关算力到注册中心的连接,所以这部分需要整合到 api-gateway-assist 这个辅助组件,它是一个 SpringBoot Starter 起到包装和连接的作用。

三、功能实现

  1. 工程结构

  2. 配置文件

源码详见:cn.bugstack.gateway.assist.config.GatewayServiceProperties

@ConfigurationProperties(“api-gateway”)
public class GatewayServiceProperties {

/** 网关地址 */
private String address;
/** 分组ID */
private String groupId;
/** 网关ID */
private String gatewayId;
/** 网关名称 */
private String gatewayName;
/** 网关地址 */
private String gatewayAddress;

// ... 省略get/set

}

在 SpringBoot Starter 的组件开发中,需要使用注解 @ConfigurationProperties(“api-gateway”) 标记出作为配置的文件类。在类中添加属性信息,这些属性信息就是最后的配置到 yml 中的配置属性。

如果你对 SpringBoot Starter 开发接触不多,可以阅读补充:https://bugstack.cn/md/assembly/middleware/2019-12-02-SpringBoot%E6%9C%8D%E5%8A%A1%E6%B2%BB%E7%90%86%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%B9%8B%E7%BB%9F%E4%B8%80%E7%99%BD%E5%90%8D%E5%8D%95%E9%AA%8C%E8%AF%81.html

  1. 注册调用

源码详见:cn.bugstack.gateway.assist.service.RegisterGatewayService

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);
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 提供的服务发现接口,向网关中心注册网关算力节点。这是第一步非常重要的关联作用,有了这块逻辑的处理,才能打通整个网关算力和网关注册中心

  1. 应用监听

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication

public class GatewayApplication implements ApplicationListener {

private GatewayServiceProperties properties;
private RegisterGatewayService registerGatewayService;

public GatewayApplication(GatewayServiceProperties properties, RegisterGatewayService registerGatewayService) {
    this.properties = properties;
    this.registerGatewayService = registerGatewayService;
}

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    // 1. 注册网关服务;每一个用于转换 HTTP 协议泛化调用到 RPC 接口的网关都是一个算力,这些算力需要注册网关配置中心
    registerGatewayService.doRegister(properties.getAddress(),
            properties.getGroupId(),
            properties.getGatewayId(),
            properties.getGatewayName(),
            properties.getGatewayAddress());
}

}

因为我们开发的是 SpringBoot Starter 组件,与 Spring 关联。所以这里会用到 Spring 提供的监听类 ApplicationListener 监听容器刷新实践后,则调用网关注册中心将网关服务注册上去。重复注册则是标记服务启动

四、测试验证

暂时我们还没有开发 api-gateway-engin 网关的启动引擎,所以这里我们先不能把 SpringBoot Starter 引入进去进行测试。
那么这里我们先进行单元测试,通过 api-gateway-assist 中的单元测试调用网关注册中心,模拟注册。
记得测试前要先启动 api-gateway-center-04

单元测试

@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, 350);
System.out.println(resultStr);
Result result = JSON.parseObject(resultStr, Result.class);
System.out.println(result.getCode());
}

测试结果

2022-11-20 16:04:34.454 INFO 93746 — [nio-8001-exec-4] c.b.g.c.interfaces.GatewayConfigManage : 注册网关服务节点 gatewayId:api-gateway-g4 gatewayName:电商配送网关 gatewayAddress:127.0.0.1
2022-11-20 16:20:48.614 INFO 93746 — [nio-8001-exec-6] c.b.g.c.interfaces.GatewayConfigManage : 注册网关服务节点 gatewayId:api-gateway-g4 gatewayName:电商配送网关 gatewayAddress:127.0.0.1

五、本章小结

本章是API网关开发中非常重要的一个衔接组件,也是 SpringBoot Starter 的锻炼内容(学会也可以丰富自己的简历)
通常像 MyBatis 的 ORM 框架、RPC 的 Dubbo 服务,在接入到 SpringBoot 都是有自己的 Starter 组件的。因为这些组件可以封装一些共性共用的逻辑处理,一次开发处处使用的目的。所以我们要做这样的处理,方便在网关引擎中启动网关算力。你可以想象这是把算力注入到引擎中的纽带开发

《API网关》第14章:网关映射聚合信息查询实现

一、学习指引

不要总想着把问题留到最后

很多时候我会把编程和生活类比,会发现编程的问题其实也是抽象后的生活问题。就像我们并不能在生活总是想着把问题放到最后处理,人无远虑必有近忧,总是把问题放到最后,最后也就处理不过来了。

对于编程开发又何尝不是,如果你不停的从这复制点代码,从那粘贴点逻辑,想当然的编写着意想能一把梭哈的逻辑。那么最后就会是一片片的报错。即使在好用的单元测试在这个时候也没有太大作用,你只能花费大量的时候去梳理,你可能已经忘记了一天前写的代码逻辑。

问题越小才越容易被理解和处理,生活如此,编程亦是如此。

二、聚合配置

本章内容属于注册中心所需提供的接口,但小傅哥是放在了第13章后来实现的这部分内容。因为在第14章中服务发现模块中需要从网关注册中心拉取服务配置。这个服务配置其实就是各个RPC服务配置【系统、接口、方法】把这些信息拉取下来,注册到网关算力中,完成RPC映射的过程。那么本章我们就先来实现这样一个接口。

首先通过 gateway_distribution 表,把网关和RPC应用服务关联起来,方便知道哪个网关算力处理哪些RPC映射管理。有了这个映射关系后,就可以把对应的 application_interface、application_interface_method、application_system 三个表维护应用的配置信息关联起来了。

三、功能实现

  1. 工程结构

  2. 聚合查询

源码详见:cn.bugstack.gateway.center.interfaces.GatewayConfigManage

@PostMapping(value = “queryApplicationSystemRichInfo”, produces = “application/json;charset=utf-8”)
public Result queryApplicationSystemRichInfo(@RequestParam String gatewayId) {
try {
logger.info(“查询分配到网关下的待注册系统信息(系统、接口、方法) gatewayId:{}”, gatewayId);
ApplicationSystemRichInfo applicationSystemRichInfo = configManageService.queryApplicationSystemRichInfo(gatewayId);
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), applicationSystemRichInfo);
} catch (Exception e) {
logger.error(“查询分配到网关下的待注册系统信息(系统、接口、方法)异常 gatewayId:{}”, gatewayId, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), null);
}
}

整个查询网关待注册系统信息的实现还是比较简单的,只是一些CRUD操作。

四、测试验证

单元测试

@Test
public void test_queryApplicationSystemRichInfo(){
ApplicationSystemRichInfo result = configManageService.queryApplicationSystemRichInfo(“api-gateway-g4”);
logger.info(“测试结果:{}”, JSON.toJSONString(result));
}

谷歌浏览器有一款插件:JSON-handle 使用起来比较方便,有需要的伙伴可以自行安装。
整个 JSON 列表就是网关算力需要从网关注册中心拉取的配置,到本地完成注册的内容。HTTP->RPC

五、本章小结

网关注册中心除了一些核心的权重分配算法以外,其实大部分都在完成数据的接收写入库中和从库中读取配置到各个组件中进行使用。
后续还会有其他的接口随着不同模块的功能实现而不断的被开发处理。最早小傅哥也想着都把这些接口写完在写后续的内容,但这样就缺少一种互动性。能看到因,才能明确果。有了这样的一个,才知道需求从哪里来。

《API网关》第15章:服务配置拉取和组件使用验证

一、学习指引

开发成本在哪?

其实大部分研发人员并没有关注到一个软件的研发成本问题,总是思考着在当下需求里越简单实现越好,但往往因为前面没有做太多的思考和设计,越往后遗留下来的问题就越多。而正因为软件开发是一个长周期的事情,所以只有有更多经验的研发才能关注到这样一个问题。—— 就像 MyBatis 源码,如果不是设计的如此抽象和分治,也不会有如此的生命力。

二、服务处理

结合着第13章,网关算力的助手组件初步实现,本章需要进行扩展以及验证使用。在本章需要完成从第14章中提供的网关聚合配置信息,拉取到网关算力中,打通这部分的接口调用。后续章节再处理映射操作,因为映射还需要把网关核心算力引入到助手组件中进行包装使用。

在本章除了开发新的组件助手功能外,还需要完成测试工程的使用。
另外对于从注册中心调用的接口,还需要做一些包装处理,方便统一管理。

三、功能实现

  1. 工程结构

  2. 重构服务

网关中心的接口操作以及我们需要在助手类添加的服务,都需要被统一的管理,否则这个工程中杜宇这部分的处理就会很混乱。读者可以对照 api-gateway-assist-01 和 api-gateway-assist-02 的代码块,关注重构内容。

assist-01/service 包拆分到 domain 领域层,把所需的服务处理都封装到这里来提供服务。这样拆分后,以后只要看这部分代码,就知道 domain 就是处理服务的,不用在乱找了。—— 你要知道,你的代码只要与你半年没见,你们就是一对新人。

  1. 领域服务

源码详见:cn.bugstack.gateway.assist.domain.service.GatewayCenterService

public class GatewayCenterService {

private Logger logger = LoggerFactory.getLogger(GatewayCenterService.class);

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);
    String resultStr = HttpUtil.post(address + "/wg/admin/config/registerGateway", 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 + "]");
}

public ApplicationSystemRichInfo pullApplicationSystemRichInfo(String address, String gatewayId) {
    Map<String, Object> paramMap = new HashMap<>();
    paramMap.put("gatewayId", gatewayId);
    String resultStr = HttpUtil.post(address + "/wg/admin/config/queryApplicationSystemRichInfo", paramMap, 350);
    Result<ApplicationSystemRichInfo> result = JSON.parseObject(resultStr, new TypeReference<Result<ApplicationSystemRichInfo>>(){});
    logger.info("从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:{}", gatewayId);
    if (!"0000".equals(result.getCode()))
        throw new GatewayException("从网关中心拉取应用服务和接口的配置信息到本地完成注册异常 [gatewayId:" + gatewayId + "]");
    return result.getData();
}

}

在 domain 模块下,service 包下的实现类中,有这么2个接口;一个是注册网关信息,另外一个是拉取配置服务。
另外读者在这里思考🤔下。小傅哥这里是用的 HTTP 因为不希望引入太多的通信模型,因为更多的内容会让大家忘记主干是哪里,而这些分支的补充大家在学习后也可以补充。比如你可以把MQ、Socket 等通信方式引入进来,让系统不仅可以通过 HTTP 与网关中心进行通信。

  1. 服务使用

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication

public class GatewayApplication implements ApplicationListener {

private GatewayServiceProperties properties;
private GatewayCenterService gatewayCenterService;

public GatewayApplication(GatewayServiceProperties properties, GatewayCenterService gatewayCenterService) {
    this.properties = properties;
    this.gatewayCenterService = gatewayCenterService;
}

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    // 1. 注册网关服务;每一个用于转换 HTTP 协议泛化调用到 RPC 接口的网关都是一个算力,这些算力需要注册网关配置中心
    gatewayCenterService.doRegister(properties.getAddress(),
            properties.getGroupId(),
            properties.getGatewayId(),
            properties.getGatewayName(),
            properties.getGatewayAddress());

    // 2. 拉取网关配置;每个网关算力都会在注册中心分配上需要映射的RPC服务信息,包括;系统、接口、方法
    ApplicationSystemRichInfo applicationSystemRichInfo = gatewayCenterService.pullApplicationSystemRichInfo(properties.getAddress(), properties.getGatewayId());
    System.out.println(JSON.toJSONString(applicationSystemRichInfo));
}

}

这是上一章中我们实现的 Spring 接口 ApplicationListener 在 onApplicationEvent 方法中添加的实现内容。
本章相当于添加了一个额外的扩展调用,等下个章节继续完成调用后的注册映射处理。HTTP->RPC

四、测试验证

本章新增加了 api-gateway-assist-00 的测试工程,它是一个 SpringBoot 的应用工程,用于测试 api-gateway-assist 组件实现。
注意这个工厂的 pom 配置中需要引入 api-gateway-assist

cn.bugstack.gateway api-gateway-assist-02 1.0-SNAPSHOT

随着工程的不断开发,api-gateway-assist-02、03、04… 会不断的调整,注意下。

小傅哥在本章节留了个隐藏bug,如果没有找到,你将不能正常启动测试。

在a pi-gateway-assist-00 的 application.yml 中添加网关配置

server:
port: 8002

api-gateway:
address: http://localhost:8001
groupId: 10001
gatewayId: api-gateway-g4
gatewayName: 电商配送网关
gatewayAddress: 127.0.0.1

在网关配置中,描述了注册中心地址和本地网关的信息。

完成以后步骤后,接下来你就可以启动 Application 了,并在调试中把断点打到 api-gateway-assist-02 工程下的 GatewayApplication 中,观察启动后的测试效果。

启动工程

. ____ _ __ _ _
/\ / _ __ _ ()_ __ __ _ \ \ \
( ( )_
_ | ‘_ | ‘| | ‘ / ` | \ \ \
\/ _)| |)| | | | | || (_| | ) ) ) )
‘ |
| .|| ||| |_, | / / / /
=========|
|==============|_/=//_//
:: Spring Boot :: (v2.6.0)

2022-11-26 17:26:55.999 INFO 51869 — [ main] cn.bugstack.gateway.assist.Application : Starting Application using Java 1.8.0_311 on MacBook-Pro.local with PID 51869 (/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-assist/api-gateway-assist-00/target/classes started by fuzhengwei in /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-assist)
2022-11-26 17:26:56.003 INFO 51869 — [ main] cn.bugstack.gateway.assist.Application : No active profile set, falling back to default profiles: default
2022-11-26 17:26:57.259 INFO 51869 — [ main] c.b.g.a.d.service.GatewayCenterService : 向网关中心注册网关算力服务 gatewayId:api-gateway-g4 gatewayName:电商配送网关 gatewayAddress:127.0.0.1 注册结果:{“code”:”0000”,”info”:”成功”,”data”:true}
2022-11-26 17:26:57.302 INFO 51869 — [ main] c.b.g.a.d.service.GatewayCenterService : 从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:api-gateway-g4
{“applicationSystemVOList”:[{“interfaceList”:[{“interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”interfaceName”:”活动接口”,”interfaceVersion”:”v1.0.0”,”methodList”:[{“auth”:1,”httpCommandType”:”POST”,”interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”methodId”:”insert”,”methodName”:”插入方法”,”parameterType”:”cn.bugstack.gateway.rpc.dto.XReq”,”systemId”:”api-gateway-test”,”uri”:”/wg/activity/insert”},{“auth”:0,”httpCommandType”:”GET”,”interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”methodId”:”sayHi”,”methodName”:”测试方法”,”parameterType”:”java.lang.String”,”systemId”:”api-gateway-test”,”uri”:”/wg/activity/sayHi”}],”systemId”:”api-gateway-test”}],”systemId”:”api-gateway-test”,”systemName”:”网关测试系统”,”systemRegistry”:”127.0.0.1”,”systemType”:”RPC”}],”gatewayId”:”api-gateway-g4”}
2022-11-26 17:27:28.930 INFO 51869 — [ main] cn.bugstack.gateway.assist.Application : Started Application in 33.686 seconds (JVM running for 34.507)
Disconnected from the target VM, address: ‘127.0.0.1:61587’, transport: ‘socket’

Process finished with exit code 0

测试截图

好啦,到这里我们本章的内容就完成了。可以连接到注册中心拉取接口,也可以放到 SpringBoot 应用中完成启动处理。

五、本章小结

本章的内容开始有些逐步增多,你开始注意到你现在要关注了3个工程【注册中心、网关助手、测试应用】才能完成测试。当我们再把网关算力、应用SDK等都引入后,会增加更多的内容。所以在学习过程中,一定要跟住内容,看看每个工程组件都在做什么。
每章中都是尽可能拆解到独立负责的一个模块或者领域,虽然可能这样会写很多文字内容来表述,但对读者来说,这是有好处的。可以随时查阅这些信息以便补充和使用相关的技术内容。

《API网关》第16章:网络通信配置提取

一、学习指引

技能 + 思想 + 熟练 = 牛逼的研发!

当我看到越来越多的程序员👨🏻‍💻转向架构师以后,开始逐步脱离编程开发。我知道,他完了。 一个PPT架构师是不能给出符合真实场景的优秀设计的,当你脱离细节以后你所给出的内容都是只是自我主观判断下的思想层面内容。但这份没有经过自己落地的思想,其实怎么表达都是虚的,因为编程中那些琐碎的细节才是真正的问题所在。

二、配置提取

对于第16章内容的处理,主要来自于在第17章开发的 api-gateway-assist-03 网关通信助手组件,对于网关通信服务的启动,可以指定配置下 Netty 服务的IP地址和端口信息。

目前这部分配置在 api-gateway-core-08 中是写死在 GatewaySocketServer 类中的,所以要对这部分内容进行处理。此外由于我们需要对 api-gateway-core 进行组件打包 Jar 给 api-gateway-assist 引入使用,那么还需要优化下 POM 配置,避免每次打包都做对 test 进行操作(这部分读者可以验证打包日志输出内容 install 打包)。

这里还有一个细节点,api-gateway-assist 引入 api-gateway-core 以后,如果希望把这个 Jar 包就打入到 api-gateway-assist 中。那么 api-gateway-core 的包结构最好是有一个区分的。但目前是 cn.bugstack.gateway 之后就是各个分层功能了,所以我们调整为 cn.bugstack.gateway.core 的结构来处理。

如图所示,修改分层结构。之后对 socket 通信提取配置,放到 session 会话的 Configuration 类中维护即可。

三、功能实现

  1. 工程结构

在工程中新添加一个 core 包,把代码迁移过去就可以了。

  1. 提取配置

这里我们要把 ewaySocketServer 类中的一些配置信息,包括;服务启动IP、服务端口、线程数,进行提取。

源码详见:cn.bugstack.gateway.core.socket.GatewaySocketServer

配置内容提取后,就是放到 Configuration 中进行统一维护。这里不要再增加其他的配置文件或者说通过构造入参的方式进行处理,因为这样你会失去对一个配置的统一管理。

源码详见:cn.bugstack.gateway.core.session.Configuration

public class Configuration {

// 网关 Netty 服务地址
private String hostName = "127.0.0.1";
// 网关 Netty 服务端口
private int port = 7397;
// 网关 Netty 服务线程数配置
private int bossNThreads = 1;
private int workNThreads = 4;

// ... 省略部分代码

}

在 Configuration 类中统一维护网关的一些配置信息。

  1. 禁止打包测试内容
org.apache.maven.plugins maven-surefire-plugin true

这里其实就是一个配置问题,避免我们打包的时候把 test 单测内容启动喽。

四、功能测试

  1. 前置条件

本章节涉及了 RPC 服务的使用,所以需要把 RPC 测试工程下载到启动;

操作:测试前先确保 api-gateway-test-provider 启动完成
这部分内容已经在 api-gateway-core 中讲解过,可以对照学习。

  1. 单元测试

@Test
public void test_gateway() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
Configuration configuration = new Configuration();
configuration.setHostName(“127.0.0.1”);
configuration.setPort(7397);
// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);
// 3. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(configuration, gatewaySessionFactory);
Future future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException(“netty server start error channel is null”);
while (!channel.isActive()) {
logger.info(“netty server gateway start Ing …”);
Thread.sleep(500);
}
logger.info(“netty server gateway start Done! {}”, channel.localAddress());
// 4. 注册接口
configuration.registryConfig(“api-gateway-test”, “zookeeper://127.0.0.1:2181”, “cn.bugstack.gateway.rpc.IActivityBooth”, “1.0.0”);
HttpStatement httpStatement01 = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“sayHi”,
“java.lang.String”,
“/wg/activity/sayHi”,
HttpCommandType.GET,
false);
HttpStatement httpStatement02 = new HttpStatement(
“api-gateway-test”,
“cn.bugstack.gateway.rpc.IActivityBooth”,
“insert”,
“cn.bugstack.gateway.rpc.dto.XReq”,
“/wg/activity/insert”,
HttpCommandType.POST,
true);
configuration.addMapper(httpStatement01);
configuration.addMapper(httpStatement02);
Thread.sleep(Long.MAX_VALUE);
}

小傅哥这里有意把注册接口映射的动作放到服务启动之后,其实映射你可以放到任何时候。也就是说我们的网关通信组件启动完成前后你都可以添加映射接口,也就是当有新的接口注册上来以后,也可以随时映射到网关中。
好啦,其余内容就一致了,这里就不做过多的演示了。你只要确保你的代码调整是可以顺利测试的即可。

五、本章小结

本章都是为了满足下一个章节所做的完善补充,这些补充其实都是留出一些扩展的口子给外部去操作。
类似的场景后续我们还需要留出类似 MyBatis 框架中对外的插件服务,这样可以让外部做更多的扩展处理。

《API网关》第17章:核心通信组件管理和处理服务映射

一、学习指引

为什么你搭建不出那些优秀的架构?

MVC 是我们通常的业务类开发架构,但当你逐步的接触到一些源码时,会发现这些源码并不是 MVC 架构,甚至很多时候这些分层会感觉很陌生。但要去分析梳理又发现,这些分层结构真的做的很优秀。

可为什么让你开发一个通用的技术类项目的时候,你脑袋里并没有这样的思维结构呢。总感觉自己要下手写代码就是创建一个 service 包,之后不断地累加逻辑。这是因为吸收优秀框架源码的架构太少了,脑袋里的印象只有MVC,自己的编程开发视野根本没打开,所以才没有可以模仿的分层结构。

所以为了提升这方面的技能,一定要多学习不同类型的源码架构并不断的运用和实践,才能提升你的能力。

二、组件管理

第17章是在第15章的基础上继续完善服务发现的相关功能,把从注册中心拉取的网关映射信息【系统、接口、方法】映射到本地通信组件中。这样就算完成了注册中心到本地服务的一个打通处理,映射完成后就可以通过HTTP请求到网关通信层,完成对RPC的泛化调用。

首先在实现中要引入 api-gateway-core-09【本章止最新版本】到 api-gateway-assist 中,通过 GatewayAutoConfig 配置类对网关通信组件进行 Bean 对象的创建和启动。
之后就可以在 GatewayApplication 中处理从网关注册中心拉取到的接口信息进行注册操作。也就是把接口向 api-gateway-core 完成注册映射操作。可参考 api-gateway-core-09 ApiTest 测试类

三、功能实现

  1. 工程结构

  2. 引入服务

cn.bugstack.gateway api-gateway-core-09 1.0-SNAPSHOT

在 api-gateway-core-09 中进行 install 打包,之后引入到 api-gateway-assist-03 中进行使用。

另外这里还有一个配置是让 api-gateway-core-09 可以打包到 api-gateway-assist 中的配置。如下;

cn.bugstack.gateway:api-gateway-core-09:jar:

这个操作主要是因为将来把 api-gateway-assist 给外部使用时,不用在额外引用 api-gateway-core 了。

  1. 服务创建

源码详见:cn.bugstack.gateway.assist.config.GatewayAutoConfig

@Bean
public cn.bugstack.gateway.core.session.Configuration gatewayCoreConfiguration(GatewayServiceProperties properties) {
cn.bugstack.gateway.core.session.Configuration configuration = new cn.bugstack.gateway.core.session.Configuration();
String[] split = properties.getAddress().split(“:”);
configuration.setHostName(split[0].trim());
configuration.setPort(Integer.parseInt(split[1].trim()));
return configuration;
}

@Bean
public Channel initGateway(cn.bugstack.gateway.core.session.Configuration configuration) throws ExecutionException, InterruptedException {
// 1. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);
// 2. 创建启动网关网络服务
GatewaySocketServer server = new GatewaySocketServer(configuration, gatewaySessionFactory);
Future future = Executors.newFixedThreadPool(2).submit(server);
Channel channel = future.get();
if (null == channel) throw new RuntimeException(“api gateway core netty server start error channel is null”);
while (!channel.isActive()) {
logger.info(“api gateway core netty server gateway start Ing …”);
Thread.sleep(500);
}
logger.info(“api gateway core netty server gateway start Done! {}”, channel.localAddress());
return channel;
}

在 GatewayAutoConfig 自动创建 Bean 对象类中,创建 cn.bugstack.gateway.core.session.Configuration 配置,以及启动网关服务。启动后就可以接收服务接口的注册操作了。

  1. 注册接口

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 1. 注册网关服务;每一个用于转换 HTTP 协议泛化调用到 RPC 接口的网关都是一个算力,这些算力需要注册网关配置中心
gatewayCenterService.doRegister(properties.getAddress(),
properties.getGroupId(),
properties.getGatewayId(),
properties.getGatewayName(),
properties.getGatewayAddress());

// 2. 拉取网关配置;每个网关算力都会在注册中心分配上需要映射的RPC服务信息,包括;系统、接口、方法
ApplicationSystemRichInfo applicationSystemRichInfo = gatewayCenterService.pullApplicationSystemRichInfo(properties.getAddress(), properties.getGatewayId());
List<ApplicationSystemVO> applicationSystemVOList = applicationSystemRichInfo.getApplicationSystemVOList();
for (ApplicationSystemVO system : applicationSystemVOList) {
    List<ApplicationInterfaceVO> interfaceList = system.getInterfaceList();
    for (ApplicationInterfaceVO itf : interfaceList) {
        // 2.1 创建配置信息加载注册
        configuration.registryConfig(system.getSystemId(), system.getSystemRegistry(), itf.getInterfaceId(), itf.getInterfaceVersion());
        List<ApplicationInterfaceMethodVO> methodList = itf.getMethodList();
        // 2.2 注册系统服务接口信息
        for (ApplicationInterfaceMethodVO method : methodList) {
            HttpStatement httpStatement = new HttpStatement(
                    system.getSystemId(),
                    itf.getInterfaceId(),
                    method.getMethodId(),
                    method.getParameterType(),
                    method.getUri(),
                    HttpCommandType.valueOf(method.getHttpCommandType()),
                    method.isAuth());
            configuration.addMapper(httpStatement);
            logger.info("网关服务注册映射 系统:{} 接口:{} 方法:{}", system.getSystemId(), itf.getInterfaceId(), method.getMethodId());
        }
    }
}

}

onApplicationEvent 方法中的第2步,就是从注册中心获取的属于当前网关需要完成映射操作的系统信息。包括这个系统的接口和方法。通过循环遍历查询到的信息进行注册处理。
在这里大家也可以提前思考🤔下,如果网关服务已经启动后,新增的接口怎么添加到注册里。

四、测试验证

  1. 前置条件

你需要先对 api-gateway-core-09 进行构建,否则 api-gateway-assist-03 会缺失包报错。
启动 Docker 和 ZK,也就是 Dubbo 的注册中心。
在2的步骤上,启动 api-gateway-test-provider 观察日志,确保 RPC 服务启动成功。
启动 api-gateway-center 注册中心,这样 api-gateway-assist-03 才能从注册中心拉取接口。
修改 api-gateway-assist-00 pom 引入 api-gateway-assist-03
使用 api-gateway-core-09 的 ShiroTest 生成 token 用于测试接口:http://localhost:7397/wg/activity/insert 所需的参数
这里我预留了一个小bug,如果你不能发现,那么将不能测试到正确的结果。

  1. 网关助手

启动工程:api-gateway-assist-00 确保 pom 引入的是最新的 api-gateway-assist-03

2022-12-03 18:48:52.356 INFO 42011 — [*cketServer : socket server start done.
2022-12-03 18:48:52.356 INFO 42011 — [*AutoConfig : api gateway core netty server gateway start Done! /127.0.0.1:7397
2022-12-03 18:48:52.475 INFO 42011 — [catWebServer : Tomcat started on port(s): 8002 (http) with context path ‘’
2022-12-03 18:48:52.765 INFO 42011 — [nterService : 向网关中心注册网关算力服务 gatewayId:api-gateway-g4 gatewayName:电商配送网关 gatewayAddress:127.0.0.1:7397 注册结果:{“code”:”0000”,”info”:”成功”,”data”:true}
2022-12-03 18:48:52.806 INFO 42011 — [nterService : 从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:api-gateway-g4
log4j:WARN No appenders could be found fo
gerFactory).
log4j:WARN Please initialize the log4j sy

log4j:WARN See http://logging.apache.org/

2022-12-03 18:48:52.994 INFO 42011 — [*Application : 网关服务注册映射 系统:api-gateway-test 接口:cn.bugstack.gateway.rpc.IActivityBooth 方法:insert
2022-12-03 18:48:52.994 INFO 42011 — [*Application : 网关服务注册映射 系统:api-gateway-test 接口:cn.bugstack.gateway.rpc.IActivityBooth 方法:sayHi
2022-12-03 18:48:52.997 INFO 42011 — [*Application : Started Application in 3.211 seconds (JVM running for 3.75)

启动的过程包括启动网关服务,注册网关信息,拉取接口映射配置等操作。
接下来我们访问下网关提供的接口信息。

访问接口:http://localhost:7397/wg/activity/insert - 记得生成 Token 你之前的可能已经过期了。

五、本章小结

本章内容主要在网关助手中扩展了对服务的创建、启动和注册操作。在这样的一个类中完成了注册中心、通信服务、接口调用等内容的链接处理。
其实这就是网关最核心的主干流程的内容,只要你把到这章节的内容搞清楚,基本就清楚了网关是如何工作的1/2核心内容。
路还很长,我们一起刚刚看到了一些照进来的曙光,一起加油!

《API网关》第18章:容器关闭监听和异常管理

一、学习指引

有些代码你根本不愿意接下来?

当你有一天有更强的能力驾驭出合理的架构和编写出高质量的代码以后,其实你会特别厌烦去接手那些随意编写出来的代码。因为这样的代码有太多的开发问题,包括;可读性差、维护性弱、迭代成本高,要说这不是可以重构吗?但其实这一片片像贴膏药一样的代码,根本不具有重构的价值,只能铲了重写。

但可能有一天连这样的膏药代码都不用你编写了,以后越来越多的类似这样的基础工作会被机器和AI取代,研发要做的是有思想高度的设计和复杂逻辑的落地。对于个人来说如果总是重复自己的编写一些 if…else 可能没到35岁就被一些智能化的编码工具取代了。所以,尽可能的做一些让自己能积累到知识和经验的编程工作。

二、动作监听

按照网关设计的架构图,api-gateway-assist 助手工程会被引入到 api-gateway-engine 网关引擎中启动。那么在 api-gateway-assist 启动的过程中,我们希望它所发生的一些动作,包括启动中的异常、拉取接口信息的失败以及容器关闭后优雅的处理网关通信的关闭。那么这些内容,就是本章需要完成的事情。

结合于 api-gateway-assist-03 的工程内容,这里需要把网关的注册和拉取配置操作,放到 ApplicationContextAware 接口对应的 setApplicationContext 方法中。这样可以在注册服务以及拉取配置的过程中出现失败情况时,则直接抛异常关闭容器。
另外这里还需要做一个容器关闭的监听动作 ApplicationListener 容器关闭时则把网关中的通信模块下的 Netty 服务也一起关闭掉。

三、功能实现

  1. 工程结构

  2. 异常管理

详见源码:cn.bugstack.gateway.assist.domain.service.GatewayCenterService#doRegister

try {
resultStr = HttpUtil.post(address + “/wg/admin/config/registerGateway”, paramMap, 550);
} catch (Exception e) {
logger.error(“网关服务注册异常,链接资源不可用:{}”, address + “/wg/admin/config/registerGateway”);
throw e;
}

详见源码:cn.bugstack.gateway.assist.domain.service.GatewayCenterService#pullApplicationSystemRichInfo

try {
resultStr = HttpUtil.post(address + “/wg/admin/config/queryApplicationSystemRichInfo”, paramMap, 550);
} catch (Exception e) {
logger.error(“网关服务拉取异常,链接资源不可用:{}”, address + “/wg/admin/config/queryApplicationSystemRichInfo”);
throw e;
}

GatewayCenterService 类所提供的服务需要从注册中心的接口拉取,如果注册中心接口暂时不可用,那么则需要抛出异常。这个异常就是给容器启动过程中的一个通知,以此来决定是否关闭服务。

  1. 感知容器

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
try {
// 1. 注册网关服务;每一个用于转换 HTTP 协议泛化调用到 RPC 接口的网关都是一个算力,这些算力需要注册网关配置中心

    // 2. 拉取网关配置;每个网关算力都会在注册中心分配上需要映射的RPC服务信息,包括;系统、接口、方法
} catch (Exception e) {
    logger.error("网关服务启动失败,停止服务。{}", e.getMessage(), e);
    throw e;
}

}

在 setApplicationContext 方法中步骤1和步骤2的代码相对于前面章节并没有改变,只是这部分代码从以前的监听类中放到了这里来管理,方便异常时直接关闭容器。

  1. 监听动作

在 GatewayApplication 类中新增加了一个属性信息 Channel gatewaySocketServerChannel; 这个属性从构造函数中设置进来,为的就是在容器关闭的监听中可以把 Netty 服务关闭掉。

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication#onApplicationEvent

public void onApplicationEvent(ContextClosedEvent event) {
try {
if (gatewaySocketServerChannel.isActive()) {
logger.info(“应用容器关闭,Api网关服务关闭。localAddress:{}”, gatewaySocketServerChannel.localAddress());
gatewaySocketServerChannel.close();
}
} catch (Exception e) {
logger.error(“应用容器关闭,Api网关服务关闭失败”, e);
}
}

监听到 Spring 的容器关闭通知时,则把 Netty 服务关闭掉。

四、测试验证

本章的测试重点在于服务的启动链接注册中心异常时的容器关闭动作,以及应用程序退出时,对网关中的 Netty 服务进行关闭。

  1. 前置条件

你需要先对 api-gateway-core-09 进行构建,否则 api-gateway-assist-03 会缺失包报错。
启动 Docker 和 ZK,也就是 Dubbo 的注册中心。
在2的步骤上,启动 api-gateway-test-provider 观察日志,确保 RPC 服务启动成功。
启动 api-gateway-center 注册中心,这样 api-gateway-assist-04 才能从注册中心拉取接口。【测试的时候可以先关闭掉,这样才能测试到异常拦截】
修改 api-gateway-assist-00 pom 引入 api-gateway-assist-03
使用 api-gateway-core-09 的 ShiroTest 生成 token 用于测试接口:http://localhost:7397/wg/activity/insert 所需的参数
更新 api-gateway.sql 为最新的文件,记得把 SQL 更新到库后,一些配置【IP、应用、接口】修改为你的本地配置。SQL文件在api-gateway-assist-04工程下

  1. 网关助手

启动工程:api-gateway-assist-00 确保 pom 引入的是最新的 api-gateway-assist-04

如截图则是异常时服务的通知调试,读者也可以验证下。

五、本章小结

本章的内容主要就是对服务的优化处理,完善监听的异常操作处理,让整个流程可以更加顺畅一些。
本章节也是为下一章节服务的整合进行扩展的,这样在后续部署到 Docker 容器中也会更加方便的看到这样的日志,便于调试和处理。

《API网关》第19章:网关引擎打包镜像部署

一、学习指引

SpringBoot工程是打包War包还是Jar包?

可能大部分伙伴经常看到工程中有的是配置 war 有的是配置 jar 这是咋回事呢,自己要配置啥呢?

其实早在以前,大家对于 Spring 的 Web 工程,为了把一些资源类的文件包括 WEB 等都进行打包,通常是默认配置 war 包,并把打包后的文件放到 Tomcat 中进行部署。部署时会进行自动解压缩。而随着后来前后端分离以及容器化的兴起,通常会把 SpringBoot 打包成 Jar 包,也就是打包成单一可执行的程序【镜像】,放到 Docker 中启动。

那么本章我们也会把网关引擎工程 api-gateway-engine 打包成 Jar 放到 Docker 中启动。如果你还没有使用过Docker,那么可以学习下小傅哥之前的文章;《部署Docker》 —— 包括:在云服务器上部署 Docker,并安装 Portainer 运维面板以及汉化。汉化包已放到 Lotter/doc/asserts/Portainer-CN

二、引擎工程

api-gateway-engine 是一个用于启动网关算力服务的引擎工程,它的代码内容几乎没有多少,主要负责的是工程的启动操作。因为镜像的打包也是从这个工程中处理。

api-gateway-core 核心通信模块,处理网关的对接口的协议转换和映射操作以及泛化调用对应的 RPC 接口。
api-gateway-assist 将 api-gateway-core 包装,提供简化的使用方式。
那么现在 api-gateway-engin 则是一个打包的执行引擎工程,打包后提供可部署到容器的 Jar 包。

三、功能实现

  1. 工程结构
cn.bugstack.gateway api-gateway-assist-04 1.0-SNAPSHOT

在 POM 中引用 api-gateway-assist-04 的 Jar 包。—— 引入前记得你已经完成了 Install,并且是配置在同一个 Maven 仓库下。

  1. Dockerfile

基础镜像

FROM openjdk:8-jre-slim

作者

MAINTAINER xiaofuge

配置

ENV PARAMS=””

时区

ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

添加应用

ADD target/api-gateway-engine-*.jar /api-gateway-engine.jar

执行镜像

ENTRYPOINT [“sh”,”-c”,”java -jar $JAVA_OPTS /api-gateway-engine.jar $PARAMS”]

这是一个用于打包工程的 Dockerfile 文件,最主要的就是添加你的应用 ADD target/api-gateway-engine-*.jar /api-gateway-engine.jar

  1. 构建文件

docker build -f ./Dockerfile -t api-gateway-engine:1.0.1 .

  1. 启动脚本

docker run -p 8004:8004 -p 7397:7397 –name api-gateway-engine -d api-gateway-engine:1.0.1

这里有2个端口映射,一个是 8004 应用的端口,还有一个 7397 网关 Netty 服务的端口。

四、测试验证

  1. 前置条件

因为网关需要部署到 Docker 那么就不能指定IP,因为这里的IP是自动分配的。所以需要把 api-gateway-core-09.GatewaySocketServer#bind方法处理下,只提供端口即可。b.bind(configuration.getPort())
启动 Docker 容器,因为打包镜像时候需要。
启动 ZK 注册中心
在3的步骤上,启动 api-gateway-test-provider 观察日志,确保 RPC 服务启动成功。
启动 api-gateway-center 注册中心,这样才能从注册中心拉取接口。
确保 api-gateway-core-09、api-gateway-assist-04、api-gateway-engine-01 都以此 clean 后并 install 打包了。—— 可以看下工程下的引用Jar里的信息。
更新 api-gateway.sql 为最新的文件,记得把 SQL 更新到库后,一些配置【IP、应用、接口】修改为你的本地配置。SQL文件在api-gateway-engine-01工程下

  1. 网关镜像

你需要按照步骤分别进行打包工程、构建镜像,以及做镜像的部署操作。分别完成后就可以在 Portainer Docker 的管理后台看到 api-gateway-engine 的启动情况了,并且可以进行调用。

  1. 测试调用

访问网关接口:http://localhost:7397/wg/activity/sayHi?str=10001

当你访问网关接口的时候,就可以在 Docker 的管理后台的 api-gateway-engine 下查看日志。这个日志就是你访问网关的日志。

五、本章小结

本章的重点在于把 api-gateway-core、api-gateway-assit、api-gateway-engine 三个工程串联起来使用打包镜像的方式进行部署使用。
那么这样一条线的串联也是把整个网关中最核心通信部分的内容给完整的体现出来了,所有的网络请求都会被这样一组工程给处理掉。
但目前我们所完成的只是最核心的主干链路的,这里的一些分支细节功能。如网关的动态拉取、插件等功能还没有实现,这部分内容需要后续再进行开发完善。

《API网关》第20章:服务注册组件搭建采集接口信息

一、学习指引

你觉得使用设计模式复杂?

日常做一些代码评审时,经常看到一个的实现方式是。一个接口对应一个实现类,一个实现类可能一千行代码。但在这些代码实现中除了定义个接口写实现类,基本看不到任何的设计模式,也没有所谓的职责分离。

而没有使用设计模式的主要问题是说:“使用设计模式浪费时间”,但其实以我的经验来看。往往不是设计模式浪费时间,而是代码编写者不具有设计模式的使用能力,缺少设计模式的运用经验。所以导致没法使用设计模式。当你真的用设计模式来编写一些代码以后,你会发现没有什么比这更爽的了,因为你的代码会非常清晰,也很利于调试和后续的迭代。

二、注册组件

首先我们知道网关的注册中心维护着网关和RPC接口的信息,用于把RPC接口分配到网关算力上使用。那么前面的章节已经实现了网关算力的自动注册,同样RPC接口也需要自动注册,否则都是人工手动维护这个成本还是非常大的。

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

开发 api-gateway-sdk 组件,获取 Spring Bean 对象的注册结果。并对已经使用注解标记了的接口进行拦截提取接口和方法信息。
本章暂时只完成这部分组件的开发,后续章节再完善向网关中心注册的操作。

三、功能实现

  1. 工程结构

  2. 定义注解

这里需要定义2个自定义注解,用于对注册到网关的接口进行标记。因为标记后就可以在读取到 Bean 对象以后判断是否需要提取信息并注册。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ApiProducerClazz {

/** 接口名称 */
String interfaceName() default "";

/** 接口版本 */
String interfaceVersion() default "";

}

@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;

}

ApiProducerClazz 注解作用到类上,ApiProducerMethod 注解作用到方法上。

  1. 应用配置

此处的应用配置,主要是为了更加灵活的配置必要的信息,包括;网关注册中心、系统标识、系统名称、RPC注册中心。

源码详见:cn.bugstack.gateway.sdk.config.GatewaySDKServiceProperties

@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;

// ... get/set

}

一般在实际使用中这些信息在公司内部往往能都是固定的,所以基本都采用默认值即可,就不需要再单独配置了。而系统ID一般是使用系统的注册在上线系统中获取【保持统一】,这样也可以减少配置。

  1. 信息提取

接下来我们需要获取 Spring 容器中 Bean 对象,并对这些对象判断是否有注解并做提取动作。

源码详见:cn.bugstack.gateway.sdk.application.GatewaySDKApplication#postProcessAfterInitialization

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
ApiProducerClazz apiProducerClazz = bean.getClass().getAnnotation(ApiProducerClazz.class);
if (null == apiProducerClazz) return bean;
// 1. 系统信息
logger.info(“\n应用注册:系统信息 \nsystemId: {} \nsystemName: {} \nsystemType: {} \nsystemRegistry: {}”, properties.getSystemId(), properties.getSystemName(), “RPC”, properties.getSystemRegistry());
// 2. 接口信息
logger.info(“\n应用注册:接口信息 \nsystemId: {} \ninterfaceId: {} \ninterfaceName: {} \ninterfaceVersion: {}”, properties.getSystemId(), bean.getClass().getName(), apiProducerClazz.interfaceName(), apiProducerClazz.interfaceVersion());
// 3. 方法信息
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
ApiProducerMethod apiProducerMethod = method.getAnnotation(ApiProducerMethod.class);
if (apiProducerMethod == null) continue;
// 解析参数
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 bean;
}

目前这里只是输出了需要注册到注册中心的日志信息。
bean.getClass().getAnnotation(ApiProducerClazz.class); 获取有ApiProducerClazz 注解的 Bean 对象,之后对这个 Bean 对象以及对象中的方法信息进行提取。

四、测试验证

  1. 前置条件

install 打包构建 api-gateway-sdk-01 组件。
api-gateway-sdk-00 是一个对应的测试服务工程,在工程中需要引入 api-gateway-sdk-01 的POM 配置。同时在 application.yml 中配置 address、systemId、systemName、systemRegistry

  1. 启动测试

. ____ _ __ _ _
/\ / _ __ _ ()_ __ __ _ \ \ \
( ( )_
_ | ‘_ | ‘| | ‘ / ` | \ \ \
\/ _)| |)| | | | | || (_| | ) ) ) )
‘ |
| .|| ||| |_, | / / / /
=========|
|==============|_/=//_//
:: Spring Boot :: (v2.6.0)

2022-12-17 16:58:19.583 INFO 47196 — [ main] c.b.g.s.a.GatewaySDKApplication :
应用注册:系统信息
systemId: api-gateway-sdk-00
systemName: 网关sdk测试工程
systemType: RPC
systemRegistry: zookeeper://192.168.1.105:2181

2022-12-17 16:58:19.586 INFO 47196 — [ main] c.b.g.s.a.GatewaySDKApplication :
应用注册:接口信息
systemId: api-gateway-sdk-00
interfaceId: cn.bugstack.gateway.sdk.interfaces.UserService
interfaceName: 用户服务
interfaceVersion: 1.0.0

2022-12-17 16:59:01.846 INFO 47196 — [ main] c.b.g.s.a.GatewaySDKApplication :
应用注册:方法信息
systemId: api-gateway-sdk-00
interfaceId: cn.bugstack.gateway.sdk.interfaces.UserService
methodId: hi
methodName: 探测
parameterType: java.lang.String
uri: /wg/user/hi
httpCommandType: GET
auth: 1

从测试的截图截图和输出结果可以看到,我们已经可以采集到系统、接口和方法的信息。

五、本章小结

本章的内容主要涉及了一些关于组件开发的使用以及获取Java对象的一些属性信息,整起来说内容不算复杂。
如果你已经开发了不少的 SpringBoot Starter 组件,那么你对于这样的内容实现将会感觉到蛮轻松。如果接触的不多,则需要补充不少的细节知识。不过我们在星球中已经有了挺多的项目都涉及到了 SpringBoot Starter 的组件开发,也可以补充学习。
在日常使用网关服务时,我们通常也是在本地引用一个网关提供好的 SDK 引入后简单配置就可以完成接口到网关的注册。如果再有一些其他的需求,则可以到网关的控制后台页面上进行修改。

《API网关》第21章:应用服务接口注册到注册中心
一、学习指引

驾驭工程设计的想法你是否有?

当我从接触编程开始,就不断的把自己学习的新技术用到一个个项目开发学习上。当然最开始也都是些自己搞的小项目,甚至很多时候也想不出做什么项目,就把之前的项目再用所学到的新技术再写一遍。

一遍一遍、一次一次,从上学到工作,一边是自己的技术积累,一边是不断地接触新需求。总是想着要用更多的架构思想和积累的设计经验,把到自己手里的项目都给做到符合当下的最好。也就是这样一点点积累了自己的编程能力。

二、服务注册

每一个做为提供HTTP接口的RPC应用服务,都需要基于引入的SDK组件,采集自身的接口向网关中心注册。因为每一个RPC服务本身是在RPC注册中心维护的,具备负载均衡的能力。所以通常向网关中心注册的都是 RPC 的接口描述信息,不过网关中心可以在这个过程记录上 RPC 接口的总数以及IP信息。这里暂时不需要,所以不提供注册

结合着第20章采集到的服务信息,这里把这些服务信息,向网关中心注册。
通常提供服务接口的实现类,只会有一个接口以及对应的实现类。如果有多个接口会抛出异常提醒。

三、功能实现

  1. 工程结构

  2. 接口注册

首先需要提供一个网关中心服务的实现类 GatewayCenterService 来包装注册应用、注册接口、注册方法。用于向网关中心注册服务信息。

源码详见:cn.bugstack.gateway.sdk.application.GatewaySDKApplication

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
ApiProducerClazz apiProducerClazz = bean.getClass().getAnnotation(ApiProducerClazz.class);
if (null == apiProducerClazz) return bean;
// 1. 系统信息
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());

// 2. 接口信息
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());
        
// 3. 方法信息
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
    ApiProducerMethod apiProducerMethod = method.getAnnotation(ApiProducerMethod.class);
    if (apiProducerMethod == null) continue;
    // 解析参数
    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(),
            interfaceId,
            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 bean;

}

实现 Spring BeanPostProcessor 接口的实现类,每采集到一个 Bean 对象的信息,就把相关的信息注册到网关中心。

四、测试验证

  1. 前置条件

启动 Docker、ZK、api-gateway-center-05 确保服务启动成功【事件通知如果存在可以注释掉】
api-gateway 数据库可以清空下 api-gateway-center、api-gateway-center、api-gateway-center 表数据,因为这几个表会被注册上来应用数据。可以清空,观察数据注册
install 打包构建 api-gateway-sdk-02 组件。
在 api-gateway-test-provider RPC服务提供者的测试类中需要引入 api-gateway-sdk-02 组件,同时需要添加上配置信息【yml类和ActivityBooth类上的注解信息】。这样在 api-gateway-test-provider 启动时候就可以把接口信息注册到服务了。
api-gateway-engine 已经在前面章节打包镜像部署到了 Docker 了。这里可以读者可以参考第19章来部署,也可以自己本地测试。

  1. 启动测试

2022-12-24 11:40:21.876 INFO 8 — [ntLoopGroup-3-2] c.b.g.c.s.handlers.GatewayServerHandler : 网关接收请求【全局】 uri:/wg/activity/sayHi?str=10001 method:GET
2022-12-24 11:40:21.877 INFO 8 — [ntLoopGroup-3-2] c.b.g.c.s.handlers.AuthorizationHandler : 网关接收请求【鉴权】 uri:/wg/activity/sayHi?str=10001 method:GET
2022-12-24 11:40:21.877 INFO 8 — [ntLoopGroup-3-2] c.b.g.c.s.handlers.ProtocolDataHandler : 网关接收请求【消息】 uri:/wg/activity/sayHi?str=10001 method:GET
2022-12-24 11:40:21.880 INFO 8 — [ntLoopGroup-3-2] c.b.gateway.core.executor.BaseExecutor : 执行调用 method:api-gateway-test-provider#cn.bugstack.gateway.rpc.IActivityBooth.sayHi([“java.lang.String”]) args:[“10001”]

当访问接口:http://localhost:7397/wg/activity/sayHi?str=10001 时候可以看到 Docker 部署的 api-gateway-engine 在打印网关请求的日志信息。

五、本章小结

本章以完成服务接口向网关中心注册为目的,注册后在数据库中就有了对应的应用信息,也会随着网关算力的启动把配置到当下网关的服务接口拉取到自己本身进行注册使用。
另外这里需要注意,目前如果是网关算力启动完成后,如果有新增加的服务接口注册上来,这个时候是没法直接映射到网关算力的。所以这部分后续需要进行处理,读者也可以先想想应该怎么处理。

《API网关》第22章:订阅服务注册消息驱动网关映射

一、前言

设计的目的就是要解决很多细碎的点

当我们开发一个软件服务或者应用时,如果有一些很小点的没有处理好,那么将要面对的是和所有的使用者进行信息沟通,同时所有的使用者也会因为你的设计而增加他们的工作量。

例如你开发了一个组件,但这个组件中有一个需要注意的配置事项。配置错了系统就没法正常运行,可能最开始开发这个组件并没有意识到,就一个配置即使错了和你沟通也没有多少成本,但随着组件被越来越多人使用时,那么就是把你的一个设计错误,分摊到所有使用者身上去。而这样带来的成本,将是非常庞大的。

二、消息监听

这一章节的开发有一个需求的背景,我们的网关算力服务在启动过程中会拉取注册中心的接口信息,到网关算法上进行注册操作。通过这样的一个步骤,才能让我们在访问网关接口的时候,泛化调用到对应的 RPC 服务。

那么,这里需要思考🤔。当网关算力服务已经是部署在Docker容器中后,再有新的服务或者接口注册到网关注册中心的时候,那么网关算力该如何把这些信息获取到并完成网关的映射呢?

其实,你可以通过几个方案来处理;

接口的轮训,在网关算力 api-gateway-assist 助手服务中通过不断的像网关中心请求接口的方式,拉取到所有需要被注册的接口。这里可以在已经拉取的服务接口上,在Redis中做数据的记录,减少重复拉取。不过这样的方式会给网关中心带来不小的压力。
服务的连接,在网关助手类与网关中心建立一个 Netty 的服务,由网关中心接收到新的接口注册时候进行信息通知。但这样的长链接,已经会占用不少的资源。
事件的通知,其实通过事件来通知是一个比较合适的方式,比如你可以使用 MQ、ZK等方式,也可以使用 Redis 的发布和订阅。在有接口变化的时候,可以通过消息的推送,让网关算力获取到变化的接口信息进行注册处理。那么本文就是通过 Redis 的方式进行处理。

带有颜色标记的工程;api-gateway-assist、api-gateway-center、api-gateway-sdk,都是本章节中所涉及要改动的工程;

在 api-gateway-center 工程中添加 Redis 发布消息模块,并提供应用服务注册后的事件通知操作。这个通知只会通知给对应的网关算力服务,不会全局通知。
在 api-gateway-assist 工程中开发 Redis 订阅消息模块,当收到注册中心的消息推送时,则根据系统的标识信息进行拉取服务。注意这里你可以进行细化,只把变更的信息一条条推送给网关注册,减少接口的拉取
在 api-gateway-sdk 工程中添加对网关注册中心接口的调用,当所有的服务注册完成后,调用接口进行通知。

三、注册中心

  1. 工程结构

  2. 消息发布

在 api-gateway-center 注册中心服务中提供 Redis 的发布模块,并在应用服务注册接口中调用该方法。

POM配置

org.springframework.boot spring-boot-starter-data-redis 2.0.6.RELEASE

yml配置

redis:
host: 192.168.1.105
port: 6379

2.1 消息发布

源码详见:cn.bugstack.gateway.center.domain.message.Publisher

@Service
public class Publisher {

private final RedisTemplate<String, Object> redisMessageTemplate;

@Autowired
public Publisher(RedisTemplate<String, Object> redisMessageTemplate) {
    this.redisMessageTemplate = redisMessageTemplate;
}

public void pushMessage(String topic, Object message) {
    redisMessageTemplate.convertAndSend(topic, message);
}

}

在 domain/message 包下,提供了 Redis 的消息发布功能模块。当有注册服务接口变更的时候,可以通过消息发布模块推送出去。

2.2 事件通知

源码详见:cn.bugstack.gateway.center.interfaces.RpcRegisterManage

@PostMapping(value = “registerEvent”, produces = “application/json;charset=utf-8”)
public Result registerEvent(@RequestParam String systemId) {
try {
logger.info(“应用信息注册完成通知 systemId:{}”, systemId);
// 推送注册消息
String gatewayId = configManageService.queryGatewayDistribution(systemId);
messageService.pushMessage(gatewayId, systemId);
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), true);
} catch (Exception e) {
logger.error(“应用信息注册完成通知失败 systemId:{}”, systemId, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
}
}

这里通了一个注册事件,既可以被 api-gateway-sdk 使用,也可以被运用平台调用,进行消息推送,让网关服务拉取最新的系统信息。

四、网关助手

  1. 工程结构

  2. 连接服务

为了能收到来自注册中心的消息推送,那么网关助手服务也需要连接到对应的 Redis 服务上。那么这里并不需要用户再网关助手类中配置 Redis 的连接信息,而是通过优化的方式从注册中心拉取 Redis 连接信息,到网关助手中进行注册。因为只有这样,才能减少用户的配置,并做到统一的管理。

源码详见:cn.bugstack.gateway.assist.config.GatewayAutoConfig

@Bean
public RedisConnectionFactory redisConnectionFactory(GatewayServiceProperties properties, GatewayCenterService gatewayCenterService) {
// 1. 拉取注册中心的 Redis 配置信息
Map<String, String> redisConfig = gatewayCenterService.queryRedisConfig(properties.getAddress());
// 2. 构建 Redis 服务
RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
standaloneConfig.setHostName(redisConfig.get(“host”));
standaloneConfig.setPort(Integer.parseInt(redisConfig.get(“port”)));
// 3. 默认配置信息;一般这些配置可以被抽取出来
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxWaitMillis(30 * 1000);
poolConfig.setMinIdle(20);
poolConfig.setMaxIdle(40);
poolConfig.setTestWhileIdle(true);
// 4. 创建 Redis 配置
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
.connectTimeout(Duration.ofSeconds(2))
.clientName(“api-gateway-assist-redis-“ + properties.getGatewayId())
.usePooling().poolConfig(poolConfig).build();
// 5. 实例化 Redis 链接对象
return new JedisConnectionFactory(standaloneConfig, clientConfig);
}

在 GatewayAutoConfig 网关服务配置类中,创建一个 Redis 链接的 Bean 对象。只不过这个对象在创建的过程中,需要先从网关注册中心拉取 Redis 配置信息,之后完成注册操作。
注意这里关于 JedisPoolConfig 是固定的配置,一般这些配置也可以从网关注册中心进行拉取。

  1. 事件订阅

当网关服务助手接收到 Redis 订阅的消息以后,则根据信息进行拉取对应的系统数据。注意这里小傅哥在网关注册中心迭代了拉取服务的接口,提供了通过系统ID进行定向拉取。

源码详见:cn.bugstack.gateway.assist.application.GatewayApplication

public void receiveMessage(Object message) {
logger.info(“【事件通知】接收注册中心推送消息 message:{}”, message);
addMappers(message.toString().substring(1, message.toString().length() - 1));
}

public void addMappers(String systemId) {
// 2. 拉取网关配置;每个网关算力都会在注册中心分配上需要映射的RPC服务信息,包括;系统、接口、方法
ApplicationSystemRichInfo applicationSystemRichInfo = gatewayCenterService.pullApplicationSystemRichInfo(properties.getAddress(), properties.getGatewayId(), systemId);
List applicationSystemVOList = applicationSystemRichInfo.getApplicationSystemVOList();
for (ApplicationSystemVO system : applicationSystemVOList) {
List interfaceList = system.getInterfaceList();
for (ApplicationInterfaceVO itf : interfaceList) {
// 2.1 创建配置信息加载注册
configuration.registryConfig(system.getSystemId(), system.getSystemRegistry(), itf.getInterfaceId(), itf.getInterfaceVersion());
List methodList = itf.getMethodList();
// 2.2 注册系统服务接口信息
for (ApplicationInterfaceMethodVO method : methodList) {
HttpStatement httpStatement = new HttpStatement(
system.getSystemId(),
itf.getInterfaceId(),
method.getMethodId(),
method.getParameterType(),
method.getUri(),
HttpCommandType.valueOf(method.getHttpCommandType()),
method.isAuth());
configuration.addMapper(httpStatement);
logger.info(“网关服务注册映射 系统:{} 接口:{} 方法:{}”, system.getSystemId(), itf.getInterfaceId(), method.getMethodId());
}
}
}
}

通过这样的方式,现在我们拉取接口一方面是网关助手系统启动的时候进行拉取,另外一种是已经在运行时候,通过事件的通知进行拉取。

四、测试验证

本章的测试重点在于网关助手服务已经启动后,有应用接口注册到网关中心,这个时候需要在网关助手服务中完成新增接口的映射操作。

  1. 前置条件

启动 Docker 容器,启动 ZK、启动 Redis 服务
清空数据库 DELETE FROM application_system;DELETE FROM application_interface;DELETE FROM application_interface_method; —— 为的就是在 api-gateway-engine 启动的时候,不让它拉取到任何接口信息,当后续注册新的接口信息时候,在拉取。
启动 api-gateway-center 注册中心,保证后续的流程可以启动。
打包最新 api-gateway-assist-05 镜像文件,部署到 Docker 容器。这个时候部署,数据库数据记得清理。
启动 api-gateway-test-provider 这个时候观察 Docker 中 api-gateway-engine 的日志信息,是否有映射完成操作。

  1. 网关服务

2022-12-31 17:28:43.632 INFO 7 — [ main] c.b.g.a.d.service.GatewayCenterService : 从网关中心拉取Redis配置信息完成。result:{“code”:”0000”,”info”:”成功”,”data”:{“port”:”6379”,”host”:”192.168.1.105”}},
2022-12-31 17:28:44.016 INFO 7 — [ main] o.a.s.config.IniSecurityManagerFactory : Realms have been explicitly set on the SecurityManager instance - auto-setting of realms will not occur.,
2022-12-31 17:28:44.439 INFO 7 — [ main] c.b.g.assist.config.GatewayAutoConfig : api gateway core netty server gateway start Done! /0.0.0.0:7397,
2022-12-31 17:28:44.437 INFO 7 — [pool-1-thread-1] c.b.g.core.socket.GatewaySocketServer : socket server start done.,
2022-12-31 17:28:44.648 INFO 7 — [ main] c.b.g.a.d.service.GatewayCenterService : 向网关中心注册网关算力服务 gatewayId:api-gateway-g4 gatewayName:电商配送网关 gatewayAddress:192.168.1.105:7397 注册结果:{“code”:”0000”,”info”:”成功”,”data”:true},
2022-12-31 17:28:44.702 INFO 7 — [ main] c.b.g.a.d.service.GatewayCenterService : 从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:api-gateway-g4,
2022-12-31 17:28:45.616 INFO 7 — [ main] cn.bugstack.gateway.engin.Application : Started Application in 7.794 seconds (JVM running for 9.002),
2022-12-31 17:28:45.527 INFO 7 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8006 (http) with context path ‘’,
2022-12-31 17:29:19.011 INFO 7 — [ container-2] c.b.g.a.d.service.GatewayCenterService : 从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:api-gateway-g4,
2022-12-31 17:30:15.112 INFO 7 — [ container-3] c.b.g.a.d.service.GatewayCenterService : 从网关中心拉取应用服务和接口的配置信息到本地完成注册。gatewayId:api-gateway-g4,
log4j:WARN No appenders could be found for logger (org.apache.dubbo.common.logger.LoggerFactory).,
2022-12-31 17:30:15.591 INFO 7 — [ container-3] c.b.g.a.application.GatewayApplication : 网关服务注册映射 系统:api-gateway-test-provider 接口:cn.bugstack.gateway.rpc.IActivityBooth 方法:sayHi,
log4j:WARN Please initialize the log4j system properly.,
2022-12-31 17:30:15.592 INFO 7 — [ container-3] c.b.g.a.application.GatewayApplication : 网关服务注册映射 系统:api-gateway-test-provider 接口:cn.bugstack.gateway.rpc.IActivityBooth 方法:insert,
2022-12-31 17:30:15.592 INFO 7 — [ container-3] c.b.g.a.application.GatewayApplication : 网关服务注册映射 系统:api-gateway-test-provider 接口:cn.bugstack.gateway.rpc.IActivityBooth 方法:test,
2022-12-31 17:29:18.996 INFO 7 — [ container-2] c.b.g.a.application.GatewayApplication : 接收注册中心推送消息 message:”api-gateway-test-provider”,
2022-12-31 17:30:15.091 INFO 7 — [ container-3] c.b.g.a.application.GatewayApplication : 接收注册中心推送消息 message:”api-gateway-test-provider”,
2022-12-31 17:30:23.725 INFO 7 — [ntLoopGroup-3-2] c.b.g.c.s.handlers.GatewayServerHandler : 网关接收请求【全局】 uri:/wg/activity/sayHi?str=10001 method:GET

好啦,现在到这,我们的网关服务就可以通过Redis的发布和订阅,完成新增接口的注册了。

五、本章小结

本章节使用到了 Redis 发布和订阅的功能,开发了新增接口时候事件通知和处理的操作,让我们可以在不重启服务的情况下把新的接口注册到网关服务中。其实类似这样的不重启服务完成处理的思想也可以作用到自己的业务场景中。
另外在本章的功能实现中,已经把接口的生产者到注册中心到接口的消费者全部连接起来了,读者认真学习后,定能感受到这样一个流程,在自己的大脑中逐步构建出来。

《API网关》第23章:网关运营管理后台框架搭建

一、学习指引

通常在我们的系统开发中,都有一个用于管理整个应用配置和检索数据的管理后台,便于运营使用。在早期没有vue出现之前,大家通常使用类似 Layui 这样的框架搭建管理后台,虽然它已经停更了但不影响它的使用也真的非常好用。

不过在互联网公司很多运营后台都是由前端研发提供好vue框架并负责开发,或者一些简单的页面也可以由后端研发进行处理。类似这样的 vue 运营管理框架包括:

vue-element-admin - 一个基于 vue2.0 和 Eelement 的控制面板 UI 框架,这是使用vue技术栈开发的前端程序员的首选管理系统模板,模板以及非常的成熟了,并且有相关的社区和维护人员,开发时候遇到问题也不要慌。
ant-design-vue-pro - 阿里背书,蚂蚁家族的。
iview-admin - iView admin 是基于 iView 的 Vue 2.0 控制面板。搭配使用 iView UI 组件库形成的一套后台集成解决方案。
d2-admin - D2Admin 是一个完全 开源免费 的企业中后台产品前端集成方案,基于 vue.js 和 ElementUI 的管理系统前端解决方案 ,小于 60kb 的本地首屏 js 加载
vuestic-admin - vuestic-admin,一款免费而美妙Vue.js管理模板包括38以上个定制用户界面组件,像地图,聊天,个人资料卡,图标,进度条、登录和注册的预建页面等等。
Vue Admin - Vue Admin 是一个基于 Vue 2.0 & Bulma 0.3 的后台管理面板(管理系统),相当于是 Vue 版本的 Bootstrap 管理系统,提供了一组通用的后台界面 UI 和组件,其中还有丰富的图表组件,开箱即用。
vue-manage-system - 基于Vue.js 2.x系列 + Element UI 的后台管理系统解决方案 ,弥补了element中缺少图片裁剪上传、富文本编辑器、图表等这些在后台管理系统中很常见的功能。适用于绝大部分的后台管理系统(Web Management System)开发。

以上这些框架都可以搭建出运营管理后台,小傅哥这里也做了一些测试验证,虽然有些像 vue-element-admin 老牌非常强大,但整体框架也不小,开发模式会复杂一些适合更大的系统和专业的vue人员使用。我们这里为了让大家既能学习到vue,也能简单的入门,所以小傅哥在测试后选择了 vue-manage-system 进行使用。

二、后台展示

npm install 是一个初始化操作,用于构建vue工程的。构建完成后就可以启动工程了,命令如下;

npm install // 安装项目依赖,等待安装完成之后,安装失败可用 cnpm 或 yarn
npm run dev // 运行
npm run build // 执行构建命令,生成的dist文件夹放在服务器下即可访问

访问:http://localhost:5173/

接下来小傅哥就讲解下这样的框架是如何搭建和配置的。

三、框架搭建

首先你要确保你本地已经安装了 node 且在 14.18+ 的版本,这是一个基本的环境要求,如果没有可以去安装 node 环境。

拉取代码:git clone https://github.com/lin-xin/vue-manage-system.git
进入模板:cd vue-manage-system - 你也可以把这里的文件复制到工程根目录下,但要记得在 IDEA 中复制,避免因为文件件中看不到有些隐藏文件。如果放到根目录,记得看下 App.vue 中的文件路径。src/assets/css/main.css -> /src/assets/css/main.css
构建依赖:npm install - 通常 IDEA 会提示运行,如果没有也可以在 IDEA 的 Terminal 中运行
启动运行:npm run dev - 正常运行后你会看到一个
构建文件:npm run build - 这一步是帮你生成一个 dist 文件夹,里面放一些html、js、imgs等静态文件,便于放到服务器部署的。

四、文件配置

在vue运营后台页面下,你需要关注的重要文件夹;views、router、components、api、public;

views:存放vue页面。
router:是一个访问的路径路由,比如访问某一个地址,会被转换到具体的vue页面。
components:组件层,比如运营后台的标题头、左侧菜单还有标签显示。
api:vue页面中的调用的API接口都在这里统一管理。
public:是存放模板和数据的,比如在api里查询的 json mock 数据,就可以放到这里来使用。如果你调用了 HTTP 接口,那么就不走 public 的 json mock 数据了。

  1. vue页面开发

源码详见:gateway_server.vue

一个 vue 页面分为三部分,包括;template、script、style,也就是最上面的 template 写html代码、script写js代码、style写css样式代码。—— 是不是有点感觉像是一个包下负责一个领域,有点和DDD思想类似。

  1. 页面API调用

在 gateway_server.vue 的 JS 代码块部分 gatewayServerData() 执行的是 api 包下 index.ts 中的内容。—— 注意 api 包下可以有多个 index.ts 你可以按照不同领域来提供,并在页面中引入即可。

源码详见:api/index.ts

export const gatewayServerData = () => {
return request({
url: ‘./gateway_server.json’,
method: ‘get’
});
};

注意这里的 request url 请求的是我们在框架中 mock 的 json 数据,后续会从网关中心查询。

mock数据:public/gateway_server.json

{
“list”: [
{
“groupId”: “10001”,
“groupName”: “廊坊网关”
},
{
“groupId”: “10002”,
“groupName”: “亦庄网关”
},
{
“groupId”: “10003”,
“groupName”: “东丽网关”
}
],
“pageTotal”: 3
}

这里的数据就是从网关中心里获取的。通常前后端做开发的时候,也是需要后端提供好接口的 mock 数据,让前端去处理页面逻辑。

  1. 路由配置

源码详见:router/index.ts

{
path: ‘/gateway_server_detail’,
name: ‘gateway_server_detail’,
meta: {
title: ‘网关节点’,
permiss: ‘2’,
},
component: () => import(/* webpackChunkName: “table” */ ‘../views/gateway_server_detail.vue’),
},

在路由ts中,有一块是专门把 vue 和路径关联起来的配置,如果没有做这部分配置你的页面会访问失败。

  1. 菜单配置

以上我们走了一个新增加页面的流程,接下来就是把这个页面配置到菜单中。

源码详见:components/sidebar.vue

{
icon: ‘Calendar’,
index: ‘1’,
title: ‘网关配置’,
permiss: ‘2’,
subs: [
{
index: ‘/gateway_server’,
title: ‘网关分组’,
permiss: ‘2’,
},
{
index: ‘/gateway_server_detail’,
title: ‘算力节点’,
permiss: ‘2’,
},
{
index: ‘/gateway_distribution’,
title: ‘网关映射’,
permiss: ‘2’,
},
],
},

这里的链接就是路由中配置的链接,也就映射到了具体的页面中。

五、本章小结

本章节我们基于 vue 搭建了网关的运营后台管理页面,这里涉及到了一些vue的知识,以及 html、css、js 技术。如果你有这些知识的积累,那么学习起来也不会太难。

《API网关》第24章:前后端分离应用的跨域接口调用

一、学习指引

在使用前后端分离的方式构建运营后台应用系统以后,你会遇到一个非常常见的问题,就是跨域访问。那么什么是跨域访问呢?

跨域访问是指当一个网页从一个域名(或端口)请求另一个域名(或端口)的资源时,由于浏览器的同源策略限制,请求会被拒绝。跨域访问是一种常见的安全限制,用于防止网页在不受信任的域中访问敏感信息。

那么我们本章节就会遇到这样的一个前端应用访问后端接口所出现的跨域问题。

其实跨越问题并不是一个很大的问题,但对于很多新人来说没经历过前后端分离的项目,在面试时被问题还是会很晕的。另外除此之外,那么多接口页面接口需要查询,出参入参该如何设计,也是一个技术点。

二、后台接口

创建新的模块 api-gateway-center-07 在 interfaces 中添加类 DataOperationManage 类,专门对 ERP 工程提供数据查询接口服务。一般如果服务工程较大,有更多的开发人员可提供开发和维护的情况,可以单独开发一个系统与 api-gateway-center 做对接。

  1. 跨域配置

@CrossOrigin 是一个跨域配置的注解,可以指定可访问接口的跨越域名和端口,如果什么都不配置,那么默认就是都可以访问。

另外关于跨域还有其他的解决方案;

使用代理服务器,将请求转发到目标域。
使用 JSONP 技术,通过动态创建 script 标签来请求数据。
使用 CORS(Cross-Origin Resource Sharing)技术,通过在服务器端设置 HTTP 头来允许跨域访问。也就是咱们使用的注解方式。
使用服务器端框架或库,例如 Spring 或 Apache Shiro,提供跨域访问支持。
使用 Nginx 代理服务器,在服务器端配置跨域访问。
使用WebSocket,建立双向通信,消除跨域问题

  1. 接口设计

源码详见:cn.bugstack.gateway.center.interfaces.DataOperationManage#queryApplicationSystem

@GetMapping(value = “queryApplicationSystem”, produces = “application/json;charset=utf-8”)
public OperationResult queryApplicationSystem(@RequestParam String systemId,
@RequestParam String systemName,
@RequestParam String page,
@RequestParam String limit) {
try {
logger.info(“查询应用系统信息开始 systemId:{} systemName:{} page:{} limit:{}”, systemId, systemName, page, limit);
OperationRequest req = new OperationRequest<>(page, limit);
req.setData(new ApplicationSystemDataVO(systemId, systemName));
OperationResult operationResult = dataOperationManageService.queryApplicationSystem(req);
logger.info(“查询应用系统信息完成 operationResult:{}”, JSON.toJSONString(operationResult));
return operationResult;
} catch (Exception e) {
logger.error(“查询应用系统信息异常 systemId:{} systemName:{}”, systemId, systemId, e);
return new OperationResult<>(0, null);
}
}

这里小傅哥展示了其中的一个应用系统信息查询的代码块。其他的几个接口实现可以参照源码。
很多的查询其实都是 CRUD 操作,这也是大部分刚开始进入公司做项目时完成的一些开发内容,另外像这样的开发现在有些时候会采用低代码的配置生成方式提供查询服务。

此外,大家可以注意下 OperationResult、OperationRequest 两个类,这2个是标准的出入参类,所有的提供给 ERP 的接口都用这样的出入参进行管理。

源码详见:cn.bugstack.gateway.center.infrastructure.common.OperationResult

public class OperationResult {

private int pageTotal;
private List<T> list;

public OperationResult() {
}

public OperationResult(int pageTotal, List<T> list) {
    this.pageTotal = pageTotal;
    this.list = list;
}

// ... get/set

}

源码详见:cn.bugstack.gateway.center.infrastructure.common.OperationRequest

public class OperationRequest {

private int pageStart = 0;  //开始 limit 第一个参数
private int pageEnd = 0;    //结束 limit 第二个参数

private int pageIndex;   //页数
private int pageSize;   //行数

private T data;

public OperationRequest() {
}

public OperationRequest(String page, String rows) {
    this.pageIndex = StringUtils.isEmpty(page) ? 1 : Integer.parseInt(page);
    this.pageSize = StringUtils.isEmpty(page) ? 10 : Integer.parseInt(rows);
    if (0 == this.pageIndex) {
        this.pageIndex = 1;
    }
    this.pageStart = (this.pageIndex - 1) * this.pageSize;
    this.pageEnd = this.pageSize;
}

public OperationRequest(int page, int rows) {
    this.pageIndex = 0 == page ? 1 : page;
    this.pageSize = 0 == rows ? 10 : rows;
    this.pageStart = (this.pageIndex - 1) * this.pageSize;
    this.pageEnd = this.pageSize;
}

public void setPage(String page, String rows) {
    this.pageIndex = StringUtils.isEmpty(page) ? 1 : Integer.parseInt(page);
    this.pageSize = StringUtils.isEmpty(page) ? 10 : Integer.parseInt(rows);
    if (0 == this.pageIndex) {
        this.pageIndex = 1;
    }
    this.pageStart = (this.pageIndex - 1) * this.pageSize;
    this.pageEnd = this.pageSize;
}

// ... get/set

}

通常给 ERP 提供的数据展示查询操作都包括了分页信息,所以这里做了简单的封装。这样就可以很方便的在 MyBatis 中使用。如下;

因为 OperationRequest 中含有泛型对象,所以这里相当于是获取 data 下的属性信息。
在分页的时候直接使用 pageStart、pageEnd 两个字段进行处理。

以上就是一个流程接口的设计和开发,其他的接口也都是类似。这部分代码量较大,但没有什么复杂度。

三、页面配置

以 api-gateway-admin 运营后台的 application_system.vue 页面举例,来配置一个接口的使用;

在 template 模块下,找到 应用标识、应用名称 所对应的标签,这里是配置搜索框信息的。
在 script 模块下,找到 const query,这里是用于定义搜索字段的。这里添加了 systemId、systemName 两个字段与搜索框的字段是对应的。
打开 index.ts 文件,找到 applicationSystemData 把 url: ‘./application_system.json’, 替换为 url: ‘http://localhost:8001/wg/admin/data/queryApplicationSystem?systemId=‘ + query.systemId + ‘&systemName=’ + query.systemName + ‘&page=’ + query.pageIndex + ‘&limit=’ + query.pageSize,。这样就可以直接访问到我们提供好的 URL 接口了。另外如果不使用拼装参数,也可以直接把 query 参数通过 params: query 方式传递到后台。
回到 application_system.vue 页面,找到 applicationSystemData 添加入参配置

const getData = () => {
applicationSystemData(query).then(res => {
tableData.value = res.data.list;
pageTotal.value = res.data.pageTotal || 50;
});
};

综上就是一个 vue 页面开发配置使用接口的过程,你也可以结合自己的诉求添加其他内容。

四、运营测试

启动 api-gateway-center-07 SpringBoot 工程,这样接口就可以被访问了。
在 IDEA 窗口 Terminal 通过命名 npm run dev 启动页面

访问地址:http://localhost:5173/

2023-01-26 16:27:28.548 INFO 84388 — [nio-8001-exec-1] c.*e : HikariPool-1 - Starting…
2023-01-26 16:27:28.739 INFO 84388 — [nio-8001-exec-1] c.*e : HikariPool-1 - Start completed.
2023-01-26 16:27:28.779 INFO 84388 — [nio-8001-exec-1] c.*anage : 查询网关服务数据完成 operationResult:{“list”:[{“groupId”:”10003”,”groupName”:”东丽网关”,”id”:3},{“groupId”:”10002”,”groupName”:”亦庄网关”,”id”:2},{“groupId”:”10001”,”groupName”:”廊坊网关”,”id”:1}],”pageTotal”:3}
2023-01-26 16:27:30.122 INFO 84388 — [nio-8001-exec-2] c.*anage : 查询网关分配数据开始 groupId: gatewayId: page:1 limit:10
2023-01-26 16:27:30.139 INFO 84388 — [nio-8001-exec-2] c.*anage : 查询网关分配数据完成 operationResult:{“list”:[{“createTime”:1669439580000,”gatewayId”:”api-gateway-g4”,”groupId”:”10001”,”id”:1,”systemId”:”api-gateway-test-provider”,”systemName”:”测试工程”,”updateTime”:1669439580000}],”pageTotal”:1}
2023-01-26 16:27:31.854 INFO 84388 — [nio-8001-exec-3] c.*anage : 查询应用系统信息开始 systemId: systemName: page:1 limit:10
2023-01-26 16:27:31.865 INFO 84388 — [nio-8001-exec-3] c.*anage : 查询应用系统信息完成 operationResult:{“list”:[{“systemId”:”api-gateway-test-provider”,”systemName”:”网关sdk测试工程”,”systemRegistry”:”zookeeper://192.168.1.105:2181”,”systemType”:”RPC”}],”pageTotal”:1}
2023-01-26 16:27:32.975 INFO 84388 — [nio-8001-exec-4] c.*anage : 查询应用接口信息开始 systemId: interfaceId: page:1 limit:10
2023-01-26 16:27:32.983 INFO 84388 — [nio-8001-exec-4] c.*anage : 查询应用接口信息完成 operationResult:{“list”:[{“interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”interfaceName”:”活动服务”,”interfaceVersion”:”1.0.0”,”systemId”:”api-gateway-test-provider”}],”pageTotal”:1}
2023-01-26 16:27:33.870 INFO 84388 — [nio-8001-exec-5] c.*anage : 查询应用接口方法信息开始 systemId: interfaceId: page:1 limit:10
2023-01-26 16:27:33.879 INFO 84388 — [nio-8001-exec-5] c.*anage : 查询应用接口方法信息完成 operationResult:{“list”:[{“auth”:0,”httpCommandType”:”POST”,”interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”methodId”:”test”,”methodName”:”测试方法”,”parameterType”:”java.lang.String,cn.bugstack.gateway.rpc.dto.XReq”,”systemId”:”api-gateway-test-provider”,”uri”:”/wg/activity/test”},{“auth”:1,”httpCommandType”:”POST”,”interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”methodId”:”insert”,”methodName”:”插入方法”,”parameterType”:”cn.bugstack.gateway.rpc.dto.XReq”,”systemId”:”api-gateway-test-provider”,”uri”:”/wg/activity/insert”},{“auth”:0,”httpCommandType”:”GET”,”interfaceId”:”cn.bugstack.gateway.rpc.IActivityBooth”,”methodId”:”sayHi”,”methodName”:”探活方法”,”parameterType”:”java.lang.String”,”systemId”:”api-gateway-test-provider”,”uri”:”/wg/activity/sayHi”}],”pageTotal”:3}

运行成功,以及一些后台日志。

五、本章小结

本章节主要涉及到了运营后台的接口设计和实现,以及在前后端分离项目中如何处理跨越接口访问的问题。此外读者也可以尝试其他方式解决跨越,比如非常常见的 JSONP 方式。

《API网关》第25章:网关Nginx负载模型配置

一、学习指引

API 网关是用于支撑分布式 RPC 接口协议转换提供 HTTP 调用的一套服务,那么 API 网关系统就需要可横向扩展来满足系统的吞吐量诉求。所以这里需要让 API 网关来支持分布式架构部署,提供负载均衡的能力。

那么在这方面有一套非常成熟的模式就是基于 Nginx 以及 LVS、F5 相关的配置构建出负载均衡服务。在这里同样我们的 API 网关也可以被这样的方式进行处理,来满足部署需求。

本节小傅哥会先给大家介绍基于 Nginx 如何构建出一套负载均衡的网络请求模型,方便让更多的读者理解这样一个过程。

二、负载模型

首先我们知道,API网关是根据 HTTP 协议请求的地址转换为对应映射泛化调用的 RPC 框架。这部分请求地址被配置到数据库中。如图;

wg 是一个固定开头的地址,转换后面紧跟着所访问的具体方法。在前面章节中已经实现过 uri 映射到具体的 RPC 上。所以当我们通过在浏览器进行 HTTP 访问接口接口 http://localhost:8090/wg/activity/sayHi 时,则会访问的到对应的 api 协议转换通信服务上,完成对应的 RPC 调用和结果封装。

那么现在我们需要的是根据一个 URL 地址所访问路径的差异,访问到不同的 api 协议转换通信服务上,这样就可以完成一个负载调用的过程了。如图

那么本章节我们就先对 Nginx 配置操作处理负载,同时本章先不用把所有的 API 网关应用都启动起来。在小傅哥的学习经验中,往往我们要处理一个小问题时,先不要引入过多的条件项来干扰结果。所以我们先通过 Socket 工具模拟网关的方式进行处理。

三、Nginx 负载部署

必要条件:

在本地安装 Dokcer,如果你有云服务器也可以把所有的内容放到云服务器操作。
已安装 Docker 容器、对容器使用有基本了解、可以使用简单的 Linux 命令。

  1. 首次部署 nginx

docker run
–name Nginx
-d
-p 8090:80
nginx

Nginx 的名称你可以自己起
8090 是映射的端口,Docker 容器配置的是 80 端口。访问时候可以;http://localhost:8090

  1. 拷贝配置

首次部署 nginx 后,其实我们还不好操作配置文件。也就是 Nginx 的配置文件,因为只有拿到配置文件我们才能自己管理它。

那么这里我们需要使用命令把配置文件拷贝到我们本地,之后重启启动 Nginx 服务。在拷贝文件之后启动Nginx之前,记得要删掉之前的 Nginx 服务。你可以使用 docker rm 命令,也可以在 portainer 的可视化界面中操作。

docker container cp Nginx:/etc/nginx/nginx.conf /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/doc/data/nginx

此外还需要一个 html 内容放到本地;

Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

小傅哥已经把配置放到 api-gateway-center/doc 下,方便大家拷贝使用。

  1. 重新安装

docker run
–name Nginx
-d
-v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/doc/data/html:/usr/share/nginx/html
-v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/doc/data/nginx/nginx.conf:/etc/nginx/nginx.conf
-p 8090:80
nginx

这次安装就要把你的本地路径给配置上了,这样才能关联到你的配置文件。后面修改自己本地的配置文件后重新启动即可。

四、Nginx 负载配置

Socket 模拟;

Mac:Socket Debugger —— 需要付费购买,也可以搜到免费的
Windows:网络调试助手 NetAssist 3.8.2 —— 已经放入 api-gateway-center/doc 中

  1. 网络调试助手模拟HTTP服务

这里小傅哥配置了三个 HTTP 服务端,以及配置对应的访问路径。如图

配置完成后,可以尝试访问地址:http://192.168.1.102:9003/ 查看请求结果

  1. Nginx 配置负载调用

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

access_log  /var/log/nginx/access.log  main;

sendfile        on;
#tcp_nopush     on;

keepalive_timeout  65;

#gzip  on;

include /etc/nginx/conf.d/*.conf;

# 设定负载均衡的服务器列表
upstream api01 {

    least_conn;

    server 192.168.1.102:9001;
    server 192.168.1.102:9002;
}

# 设定负载均衡的服务器列表
upstream api02 {
    server 192.168.1.102:9003;
}

# HTTP服务器
server {
    # 监听80端口,用于HTTP协议
    listen  80;

    # 定义使用IP/域名访问
    server_name 192.168.1.102;

    # 首页
    index index.html;

    # 反向代理的路径(upstream绑定),location 后面设置映射的路径
    location / {
        proxy_pass http://192.168.1.102:9001;
    }

    location /api01/ {
        proxy_pass http://api01;
    }

     location /api02/ {
        proxy_pass http://api02;
    }
}

}

重点在 server 中 location 的配置,分别调度用到 api01、api02,另外在 upstream api01 中通过连接数进行负载。在 Nginx 中还有 IP 哈希负载。可参考 Nginx 配置文档:https://dunwu.github.io/nginx-tutorial/#/nginx-quickstart

五、测试效果

访问Nginx接口:http://192.168.1.102:8090/api01/http://192.168.1.102:8090/api02/

通过不同的地址访问以及刷新可以看到我们的服务被负载轮训请求了。说明 Nginx 已经配置完成。

六、本章小结

本章节我们初步的学习了 Nginx 的负载模型配置,来调用模拟的网关应用。下一章节将完善这部分内容的使用,在注册中心通过配置的方式调用到 API 网关层。

《API网关》第26章:动态刷新网关Nginx负载均衡配置
一、学习指引

用于承载 HTTP 请求转换协议泛化调用 RPC 服务的网关算力不可能只有一组服务,而是一个网关算力集群化的设计实现。而对于这样一个诉求的实现,基本的核心模型结构就是负载的配置和轮训策略的使用。而这也是为什么我们会在第25章先进行 Nginx 的负载模型构建。那么在本章小傅哥将继续带着大家扩展这部分内容,让Nginx可以被动态的变更并重载配置文件。这样就可以满足当有新的网关注册、下线、调整时可以自动的生效Nginx配置。

二、方案设计

动态刷新的负载配置策略的方案也会根据服务的部署方式有所不同,本文是以服务在Docker容器化部署,通过Java调用容器指令的方式进行刷新,为案例进行讲解。以下是方案设计;

对于一个网关的算力的动态配置和刷新,要根据服务的注册动态变更Nginx配置文件并生效。那么这里就会牵扯到Nginx的配置文件变更和刷新,如何通过Java程序进行控制等问题。
那么以当前服务部署到Docker容器场景为例,Docker 是嵌入到 Linux 服务器内的,每个镜像实例的部署也都是隔离的,那么这个时候该怎么完成配置文件的互通和指令调用就成了本章要解决的核心问题。

三、方案实现

对于方案设计中提到的问题,我们做出对应的实现;

问题1:Nginx 的配置文件如何被其他应用程序获取并修改,这里需要用到文件挂载操作。我们把存放在 Linux 服务器上的 nginx.conf 所在的文件夹📂挂在到 api-gateway-center 上,之后把 nginx.conf 配置文件挂在 Nginx 服务上。这样就打通第一个要解决的配置文件问题。之后再 api-gateway-center 服务上就可以通过 IO 流更新 nginx.conf 文件。
问题2:如何通过 api-gateway-center 程序对 Nginx 进行调用。这里因为我们是在 Docker 场景下,所以需要通过 Java 控制并获取 Docker 容器中 Nginx 的服务,并对其进行操作。这里还会涉及到网络的问题,否则在容器中服务1不能调用服务2。

  1. Nginx 配置服务

docker run
–name Nginx
-d
-v /Users/fuzhengwei1/Documents/develop/tmp/nginx/html:/usr/share/nginx/html
-v /Users/fuzhengwei1/Documents/develop/tmp/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
-p 8090:80
nginx

Nginx 配置哎25章已经讲解过文件的挂载,可以回到对应章节参考。

  1. 刷新文件建模

更新Nginx的配置,需要先通过Java程序创建或者更新Nginx配置,之后再启动刷新指令。这段功能逻辑代码非常服务模板模式,来定义标准的执行流程,并做实现。

public abstract class AbstractLoadBalancing implements ILoadBalancingService {

private Logger logger = LoggerFactory.getLogger(AbstractLoadBalancing.class);

@Override
public void updateNginxConfig(NginxConfig nginxConfig) throws Exception {
    // 1. 创建 Nginx 配置文件
    String containerFilePath = createNginxConfigFile(nginxConfig);
    logger.info("步骤1:创建 Nginx 配置文件 containerFilePath:{}", containerFilePath);
    // 2. 复制 Nginx 配置文件
    // copyDockerFile(nginxConfig.getApplicationName(), containerFilePath, nginxConfig.getLocalNginxPath());
    // logger.info("步骤2:拷贝 Nginx 配置文件 localPath:{}", nginxConfig.getLocalNginxPath());
    // 3. 刷新 Nginx 配置文件
    refreshNginxConfig(nginxConfig.getNginxName());
    logger.info("步骤2:刷新 Nginx 配置文件 Done!");
}

protected abstract String createNginxConfigFile(NginxConfig nginxConfig) throws IOException;

protected abstract void copyDockerFile(String applicationName, String containerFilePath, String localNginxPath) throws InterruptedException, IOException;

protected abstract void refreshNginxConfig(String nginxName) throws InterruptedException, IOException;

}

实现流程分为3块,包括;文件的创建、文件的复制、文件的刷新。但鉴于我们已经通过文件的挂在到相同目录下,解决了复制Docker应用程序创建出来的 Nginx 配置文件到本地,所以这里就不需要自己在复制了。
文件刷新配置会涉及到镜像容器部署的一些指令,在下文中会有体现。

  1. 刷新文件实现

本身部署在 Docker 容器的 Nginx 文件,在刷新时候需要通过指令;docker exec Nginx nginx -s rolad 那么这里我们通过 Java 程序控制,就需要连接 Docker 容器,找到对应的服务,在执行容器指令。

protected void refreshNginxConfig(String nginxName) throws InterruptedException, IOException {
// Docker client
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost(“unix:///var/run/docker.sock”).build();

DockerClient dockerClient = DockerClientBuilder.getInstance(config).build();
String containerId = dockerClient.listContainersCmd()
        .withNameFilter(new ArrayList<String>() {{
                add(nginxName);
            }})
        .exec()
        .get(0)
        .getId();
        
ExecCreateCmdResponse execCreateCmdResponse = dockerClient
        .execCreateCmd(containerId)
        .withCmd("nginx", "-s", "reload")
        .exec();
        
dockerClient.execStartCmd(execCreateCmdResponse.getId())
        .exec(new ResultCallback.Adapter<>()).awaitCompletion();
        
dockerClient.close();

}

这一段代码中包括了连接的创建、服务的查找、指令的执行和关闭连接的操作。你也可以只做单元测试进行。NginxTest 类中有对应的代码。

  1. 应用容器部署

这里需要注意,为了开启Docker容器中的Java应用连接Docker,需要开启对应的网络服务。这个服务在配置容器时即可开启;

docker run -p 8001:8001 -v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/doc/data/nginx:/data/nginx -v /var/run/docker.sock:/var/run/docker.sock –name api-gateway-center -d api-gateway-center:1.0.5 CP4-LISTEN:8001,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock TCP4-LISTEN:8001,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock

-v 设置对应的挂在文件夹和docker的映射文件
另外还需要开启监听端口,否则不能在docker容器中的java应用对另外一个服务操作。

四、测试验证

准备事项;

使用 Socket Debugger 模拟3组 API 服务,与25章相同。个人测试注意IP变更
在 Docker 容器配置好 Nginx 服务,并确定挂载文件 nginx.conf
在 Docker 容器,打包、部署 api-gateway-center 服务,记得要做先构建再打包再部署。部署的时候使用上文提到的指令。

  1. 3套 API 模拟服务

  2. Nginx 初始配置

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

access_log  /var/log/nginx/access.log  main;

sendfile        on;
#tcp_nopush     on;

keepalive_timeout  65;

#gzip  on;

# 设定负载均衡的服务器列表

upstream api01 {
least_conn;

server 172.20.10.12:9001;
#server 172.20.10.12:9002;
}

upstream api02 {
least_conn;

server 172.20.10.12:9003;
}

# HTTP服务器
server {
    # 监听80端口,用于HTTP协议
    listen  80;

    # 定义使用IP/域名访问
    server_name 172.20.10.12;

    # 首页
    index index.html;

    # 反向代理的路径(upstream绑定),location 后面设置映射的路径
    location / {
        proxy_pass http://172.20.10.12:9001;
    }

location /api01/ {
proxy_pass http://api01;
}

location /api02/ {
proxy_pass http://api02;
}

}

}

upstream api01 首次配置中只生效了一组 IP 此时访问;http://172.20.10.12:8090/api01/ 都只会返回 api101 success

  1. 刷新配置

在 api-gateway-center 中提供了接口:http://172.20.10.12:8001/wg/admin/load/updateNginxConfig

/**

  • http://localhost:8001/wg/admin/load/updateNginxConfig
    */
    @GetMapping(value = “updateNginxConfig”, produces = “application/json;charset=utf-8”)
    public void updateNginxConfig() {
    List upstreamList = new ArrayList<>();
    upstreamList.add(new UpstreamVO(“api01”, “least_conn;”, Arrays.asList(“192.168.1.102:9001;”, “192.168.1.102:9002;”)));
    upstreamList.add(new UpstreamVO(“api02”, “least_conn;”, Arrays.asList(“192.168.1.102:9003;”)));

    List locationList = new ArrayList<>();
    locationList.add(new LocationVO(“/api01/“, “http://api01;”));
    locationList.add(new LocationVO(“/api02/“, “http://api02;”));
    NginxConfig nginxConfig = new NginxConfig(upstreamList, locationList);

    try {
    logger.info(“刷新Nginx配置文件开始 nginxConfig:{}”, JSON.toJSONString(nginxConfig));
    loadBalancingService.updateNginxConfig(nginxConfig);
    logger.info(“刷新Nginx配置文件完成”);
    } catch (Exception e) {
    logger.error(“刷新Nginx配置文件失败”, e);
    }
    }

通过此配置来动态调整Nginx的负载均衡配置,也就是模拟当有 API 服务变更时,来动态变更 Nginx 负载信息。
此时访问接口:http://172.20.10.12:8001/wg/admin/load/updateNginxConfig Nginx 文件会发生变更以及刷新操作。

五、扩展介绍

如果我们前面所提到,因为使用了文件挂在的方式所以不需要在通过指令cp文件的操作。但在 Docker 中是有对应的代码来操作文件的拷贝的;

@Test
public void test_nginx_file() throws InterruptedException, IOException {
// Docker client
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost(“unix:///var/run/docker.sock”).build();
DockerClient dockerClient = DockerClientBuilder.getInstance(config).build();
// Copy file from container
try (TarArchiveInputStream tarStream = new TarArchiveInputStream(
dockerClient.copyArchiveFromContainerCmd(“api-gateway-center”,
“/nginx.conf”).exec())) {
unTar(tarStream, new File(“/Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway-center/doc/data/nginx/nginx.conf”));
}
dockerClient.close();

此内容来自于 docker-java https://github.com/docker-java/docker-java/issues/991 可以扩展学习

六、本章小结

本章节我们处理了如何挂在文件,怎么让另个程序访问到一个文件,以及怎么通过Java指令来动态操作另外一个在 Docker 容器中的服务。
其实类似这样智能化的操作,在大厂中有非常多,尤其是服务上线,很多都是 DevOps 流水线操作。通过简单的配置即可完成所有的操作,算是解放研发的双手了。但如果研发真的不会这些核心底层是如何处理的,那么以后只能越来越成为工具人。

九、扩展内容

如果不是部署到 Docker 容器,那么通过Java程序操作Nginx会更简单;

  1. 在 Java 程序中读取 Nginx 的配置文件,比如 “/etc/nginx/nginx.conf” 文件。

File configFile = new File(“/etc/nginx/nginx.conf”);
String configContent = new String(Files.readAllBytes(configFile.toPath()));

  1. 修改配置文件内容,比如添加一条新的反向代理配置。

String newConfig = configContent + “\n” +
“location /app {\n” +
“ proxy_pass http://localhost:8080/app;\n“ +
“}”;

  1. 将修改后的配置文件写入到 Nginx 配置目录下的一个新文件中,比如 “/etc/nginx/nginx.conf” 文件。

File newConfigFile = new File(“/etc/nginx/nginx.conf.new”);
Files.write(newConfigFile.toPath(), newConfig.getBytes());

  1. 通过执行操作系统命令来重载 Nginx 配置。

ProcessBuilder pb = new ProcessBuilder(“nginx”, “-t”);
pb.redirectErrorStream(true);
Process process = pb.start();
process.waitFor();

if (process.exitValue() == 0) {
ProcessBuilder pbReload = new ProcessBuilder(“nginx”, “-s”, “reload”);
pbReload.redirectErrorStream(true);
Process processReload = pbReload.start();
processReload.waitFor();
}

以上代码中,首先使用 “nginx -t” 命令来检查新的配置文件是否有语法错误,如果检查通过,则使用 “nginx -s reload” 命令来重载 Nginx 配置。注意,这些命令需要在执行 Java 程序的操作系统中已经安装了 Nginx。

  1. 最后,您可以将新的配置文件重命名为原来的文件名,以覆盖原来的配置文件。

configFile.delete();
newConfigFile.renameTo(configFile);

通过以上步骤,就可以通过 Java 程序更新 Nginx 配置文件并重载配置了。当然,这只是一个简单的实现,您还需要考虑一些异常情况,比如在写入新配置文件时出现错误等等。

《API网关》第27章:实现网关算力节点动态负载功能
一、学习指引

基于第25章Nginx的负载模型,第26章动态刷新Nginx的配置和实现,终于在27章可以把整个流程串联起来,完成网关算力节点的动态负载功能实现了。

这里我们要实现的核心目的就是当有网关算力节点注册到注册中心的时候,可以被动态的配置到Nginx负载中,这样就可以实现动态的变更操作了。

二、方案设计

在不使用Nginx代理的时候,前面章节使用网关都是通过直接方式的方式操作,如 http://172.20.10.12:7397/wg/activity/sayHi?str=1 那么现在因为有负载的设计,希望把来自于不同URL的请求负载到不同的网关算力上去,所以这里的访问地址将变更为:http://172.20.10.12:8090/10001/wg/activity/sayHi?str=10001。

从第1个地址到第2个地址来看,变化的点主要是端口由原来的访问网关算力节点到访问Nginx,同时多了一个 10001 的路径。这个 10001 就是数据库中 group_id 网关分组的配置。我们也是用这个配置来区分访问哪一组网关。所以整体设计如图;

api-gateway-center 管理着网关算力的注册,并把注册的配置信息动态刷新到 Nginx 配置中。
同时在 Nginx 的配置中会重新URL,也就是把 10001 这个根目录路径给去掉,让它的功能只是负责路由即可,剩下的与原有直接访问网关算力不变。这样即使以后不需要做负载也可以直接访问网关算力节点。

三、功能实现

改动点;

GatewayConfigManage#registerGatewayServerNode:在网关算力节点注册的时候,调用在第26章实现的动态刷新Nginx配置接口。此外注意本章节新添加了Nginx路径重新配置,以及在yml配置文件中添加了 Nginx IP 的配置。
ConfigManageService#queryApplicationSystemRichInfo:bug 优化,null 判断字符串,改为 if (StringUtils.isEmpty(systemId)) 这样更准确。

源码实现:cn.bugstack.gateway.center.interfaces.GatewayConfigManage#registerGatewayServerNode

@PostMapping(value = “registerGateway”, produces = “application/json;charset=utf-8”)
public Result registerGatewayServerNode(@RequestParam String groupId, @RequestParam String gatewayId, @RequestParam String gatewayName, @RequestParam String gatewayAddress) {
try {
logger.info(“注册网关服务节点 gatewayId:{} gatewayName:{} gatewayAddress:{}”, gatewayId, gatewayName, gatewayAddress);
// 1. 注册&更新网关算力信息
boolean done = configManageService.registerGatewayServerNode(groupId, gatewayId, gatewayName, gatewayAddress);
// 2. 读取最新网关算力数据【由于可能来自于多套注册中心,所以从数据库或者Redis中获取,更为准确】
List gatewayServerDetailVOList = configManageService.queryGatewayServerDetailList();
// 3. 组装Nginx网关刷新配置信息
Map<String, List> gatewayServerDetailMap = gatewayServerDetailVOList.stream()
.collect(Collectors.groupingBy(GatewayServerDetailVO::getGroupId));
Set uniqueGroupIdList = gatewayServerDetailMap.keySet();
// 3.1 Location 信息
List locationList = new ArrayList<>();
for (String name : uniqueGroupIdList) {
// location /api01/ {
// rewrite ^/api01/(.*)$ /$1 break;
// proxy_pass http://api01;
// }
locationList.add(new LocationVO(“/“ + name + “/“, “http://“ + name + “;”));
}
// 3.2 Upstream 信息
List upstreamList = new ArrayList<>();
for (String name : uniqueGroupIdList) {
// upstream api01 {
// least_conn;
// server 172.20.10.12:9001;
// #server 172.20.10.12:9002;
// }
List servers = gatewayServerDetailMap.get(name).stream()
.map(GatewayServerDetailVO::getGatewayAddress)
.collect(Collectors.toList());
upstreamList.add(new UpstreamVO(name, “least_conn;”, servers));
}
// 4. 刷新Nginx配置
loadBalancingService.updateNginxConfig(new NginxConfig(upstreamList, locationList));
return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), done);
} catch (Exception e) {
logger.error(“注册网关服务节点异常”, e);
return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
}
}

此处代码的改动主要在于网关算力节点注册完成以后,开始封装Nginx刷新配置信息,并执行刷新操作。
把这个流程做完,也就是把算力刷新到Nginx中负载了。

四、功能测试

本章的测试需要启动 api-gateway-center、api-gateway-engine、api-gateway-test-provider,三个服务。

注意开启服务;Docker、zookeeper、Nginx、redis,并按照网关所需启动以及修改对应的IP信息。如果相关内容失败,也可以修改后再启动。
api-gateway-center 提供注册中心服务
api-gateway-engine 启动的时候会由引擎下的 assist 助手组件拉取注册中心所归属此算力节点的配置信息,并在本地完成HTTP和RPC的映射。
api-gateway-test-provider 提供RPC接口测试服务
api-gateway.sql 最新的SQL脚本已经放到 api-gateway-center-09 下,可以自己执行更新,并修改IP为自己本地IP
如果你使用PostMan、APIPost等工具,可以直接把 api-gateway-center-09\API 网关:中间件设计和实践.postman_collection.json 导入修改下IP即可使用。

以上服务启动完成后,就可以调用接口测试了;在测试过程中,我们先通过直连网关不走Nginx代理,确保网关可用。之后再走Nginx接口访问。

日志信息:

2023-03-04 13:20:24.070 INFO 37340 — [3-1-EventThread] o.a.c.f.state.ConnectionStateManager : State change: RECONNECTED
2023-03-04 13:23:39.915 INFO 37340 — [ntLoopGroup-3-4] c.b.g.c.s.handlers.GatewayServerHandler : 网关接收请求【全局】 uri:/wg/activity/sayHi?str=1 method:GET
2023-03-04 13:23:39.916 INFO 37340 — [ntLoopGroup-3-4] c.b.g.c.s.handlers.AuthorizationHandler : 网关接收请求【鉴权】 uri:/wg/activity/sayHi?str=1 method:GET
2023-03-04 13:23:39.917 INFO 37340 — [ntLoopGroup-3-4] c.b.g.c.s.handlers.ProtocolDataHandler : 网关接收请求【消息】 uri:/wg/activity/sayHi?str=1 method:GET
2023-03-04 13:23:39.956 INFO 37340 — [ntLoopGroup-3-4] c.b.gateway.core.executor.BaseExecutor : 执行调用 method:api-gateway-test-provider#cn.bugstack.gateway.rpc.IActivityBooth.sayHi([“java.lang.String”]) args:[“1”]
2023-03-04 13:23:43.436 INFO 37340 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.GatewayServerHandler : 网关接收请求【全局】 uri:/wg/activity/sayHi?str=10001 method:GET
2023-03-04 13:23:43.436 INFO 37340 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.AuthorizationHandler : 网关接收请求【鉴权】 uri:/wg/activity/sayHi?str=10001 method:GET
2023-03-04 13:23:43.436 INFO 37340 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.ProtocolDataHandler : 网关接收请求【消息】 uri:/wg/activity/sayHi?str=10001 method:GET
2023-03-04 13:23:43.437 INFO 37340 — [ntLoopGroup-3-1] c.b.gateway.core.executor.BaseExecutor : 执行调用 method:api-gateway-test-provider#cn.bugstack.gateway.rpc.IActivityBooth.sayHi([“java.lang.String”]) args:[“10001”]

以上就是整个关于网关算力的Nginx负载均衡设计和实现的全过程,也打通整个服务的调用关系。

六、本章小结

本章通过将前面25、26两个章节内容整合,到本章完成了网关算力动态注册和刷新的整个功能逻辑。这里的设计是网关算力动态负载的其中一种方案,也是比较常见的方案和非常重要的处理手段。一般在大厂中这类功能服务,也都是动态化处理的,无论是流量的管控、服务的拉起、应用的冷启动等,都需要这样的动态的管理,才能做到降本增效。

《API网关》第28章:网关组件工程模块合并

一、学习指引

截止到本章整个API网关的核心流程就已经全部开发完成了,并可以完成基本测试调用。从本章开始将是对网关功能的细节迭代,因为这些内容会涉及到对网关六个模块【admin、center、core、assist、engine、sdk】的开发。所以到本章开始把整个工程合并,后续的章节将按照创建分支的方式进行开发。

前面的教学方式为模块的渐进迭代,主要为了帮助小白在学习过程中可以逐个模块的验证,不用切换分支,更方便理解和学习。那么在有了前面的学习基础之上,后续再通过拉分支迭代开发就更容易理解了。分支迭代也是各大互联网公司最为标准的需求迭代方式,所以读者也是有必要学习、理解和掌握的。

二、模块服务

到本章大家可以回想下,我们一共开发了几个微服务模块工程,来支持网关的整体服务。其实目前网关的这六个模块工程,主要分为3个大的部分在运行。如图所示;

第一组:网关算力,由 api-gateway-core、api-gateway-assit、api-gateway-engine 组成,core 提供算力、assist 处理封装、engine 镜像打包和启动。
第二组:管理中心,由 api-gateway-admin、api-gateway-center 组成,admin 后台运营、center 注册中心。
第三组:接口上报,由 api-gateway-sdk 提供,它被应用系统引入,在应用系统中以注解的方式摘取应用RPC接口信息并向注册中心发送。

那么现在,为了后续可以更好的迭代和维护,我们把几组系统进行模块的工程合并。

三、工程合并

  1. 工程结构

合并的方式是通过 IDEA 创建出一个多模块工程,再分别把咱们网关的各个模块代码复制进去。
admin-管理后台、assist-助手、center-注册中心、core-核心通信、engine-引擎、sdk-上报接口、test-rpc测试工程
doc 包含了最新的 sql文件、nginx配置、postman接口等内容,后续一些必备的配置也会放到这里。
此外,api-gateway-center、api-gateway-engine,都是Java的部署工程,已经在工程下提供了好了 Docker 配置,非常容易上手部署。

  1. 部署文件
    2.1 api-gateway-center

打包镜像 - Dockerfile

基础镜像

FROM openjdk:8-jre-slim

作者

MAINTAINER xiaofuge

配置

ENV PARAMS=””

时区

ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

添加应用

ADD target/api-gateway-center.jar /api-gateway-center.jar

在镜像运行为容器后执行的命令

ENTRYPOINT [“sh”,”-c”,”java -jar $JAVA_OPTS /api-gateway-center.jar $PARAMS”]

部署镜像 - start.sh

docker run -p 8901:8901
-v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway/doc/data/nginx:/data/nginx
-v /var/run/docker.sock:/var/run/docker.sock
–name api-gateway-center
-d api-gateway-center:1.0.0 CP4-LISTEN:8001,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock TCP4-LISTEN:8001,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock

2.2 api-gateway-engine

打包镜像 - Dockerfile

基础镜像

FROM openjdk:8-jre-slim

作者

MAINTAINER xiaofuge

配置

ENV PARAMS=””

时区

ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

添加应用

ADD target/api-gateway-engine.jar /api-gateway-engine.jar

执行镜像

ENTRYPOINT [“sh”,”-c”,”java -jar $JAVA_OPTS /api-gateway-engine.jar $PARAMS”]

部署镜像 - start-01.sh

docker run -p 8902:8902 -p 7397:7397
-e PARAMS=”
–sever.port=8902
–api-gateway.address=http://192.168.1.102:8901
–api-gateway.groupId=10001
–api-gateway.gatewayId=api-gateway-g4
–api-gateway.gatewayName=电商配送网关
–api-gateway.gatewayAddress=192.168.1.102:7397”
–name api-gateway-engine-01 -d api-gateway-engine:1.0.0

网关算力可以部署多套,所以这里直接提供了 start-01.sh、 start-02.sh 两个部署文件,方便测试网关算力的集群能力。

2.2 Nginx

docker run
–name Nginx
-d
-v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway/doc/data/html:/usr/share/nginx/html
-v /Users/fuzhengwei/1024/KnowledgePlanet/api-gateway/api-gateway/doc/data/nginx/nginx.conf:/etc/nginx/nginx.conf
-p 8090:80
nginx

因为工程重新整理,那么写配置信息也会随着修改。比如 Nginx 的 doc 被配置到了 api-gateway/doc 下。

四、容器部署

整个API网关所需的基础环境包括:MySQL、zookeeper、Nginx、Redis;

再有了基础配置以外,就可以通过 api-gateway 各个模块下的容器部署配置,来启动工程了。

五、测试验证

2023-03-11 17:09:08.275 INFO 8 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.GatewayServerHandler : 网关接收请求【全局】 uri:/wg/activity/sayHi?str=10001 method:GET

2023-03-11 17:09:08.275 INFO 8 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.AuthorizationHandler : 网关接收请求【鉴权】 uri:/wg/activity/sayHi?str=10001 method:GET

2023-03-11 17:09:08.275 INFO 8 — [ntLoopGroup-3-1] c.b.g.c.s.handlers.ProtocolDataHandler : 网关接收请求【消息】 uri:/wg/activity/sayHi?str=10001 method:GET

2023-03-11 17:09:08.277 INFO 8 — [ntLoopGroup-3-1] c.b.gateway.core.executor.BaseExecutor : 执行调用 method:api-gateway-test-provider#cn.bugstack.gateway.rpc.IActivityBooth.sayHi([“java.lang.String”]) args:[“10001”]

访问接口:http://192.168.1.102:8090/10001/wg/activity/sayHi?str=10001 测试通过。

六、本章小结

通过多模块工程的结构管理 API网关各个微服务模块,通过这样的方式为后续的功能扩展做好铺垫。本章学习中多测试验证,确保每个模块的搭配使用都是正常的。

《API网关》第29章:功能完善(算力关联、接口上报、调用反馈)

一、学习指引

从本章开始小傅哥会带着大家一起完善网关服务的使用细节,让网关系统逐步趋于完善。在这个过程中读者也可以尝试自己添加一下功能模块,来满足你在使用其他某些网关时候所遇到的一些功能。通过这样的方式锻炼自己对系统的理解、使用和扩展迭代。

二、扩展功能

本章的扩展功能主要从几个方面来考虑;

网关的注册中心需要提供一个网关算力与RPC服务的分配关系。groupId –1vn–> gatewayId –1vn–> systemId
10001 -> api-gateway-g3 -> api-gateway-test-01-provider
10001 -> api-gateway-g3 -> api-gateway-test-02-provider
10001 -> api-gateway-g4 -> api-gateway-test-03-provider
10001 -> api-gateway-g4 -> api-gateway-test-04-provider
10002 -> api-gateway-g5 -> api-gateway-test-05-provider
10002 -> api-gateway-g5 -> api-gateway-test-06-provider
RPC 应用上报的 SDK 中需要添加一个开关,是否允许上报。这样可以更方便的从应用中摘除 SDK 的功能,可以更便于的测试。
核心通信组件 CORE 中需要在返回的通信协议中携带上是哪台网关算力处理的HTTP协议,这样可以更方便的看到负载起的作用。

以上这些内容的开发并不难,如果你已经跟着做了前面的网关内容,那么可以尝试自己扩展后再参考源码。

三、功能实现

  1. 注册中心提供匹配接口

/**

  • 网关算力与系统挂载配置
  • groupId –1vn–> gatewayId –1vn–> systemId
  • 10001 -> api-gateway-g3 -> api-gateway-test-01-provider
  • 10001 -> api-gateway-g3 -> api-gateway-test-02-provider
  • 10001 -> api-gateway-g4 -> api-gateway-test-03-provider
  • 10001 -> api-gateway-g4 -> api-gateway-test-04-provider
  • 10002 -> api-gateway-g5 -> api-gateway-test-05-provider
  • 10002 -> api-gateway-g5 -> api-gateway-test-06-provider
    */
    @PostMapping(value = “distributionGatewayServerNode”, produces = “application/json;charset=utf-8”)
    public Result distributionGatewayServerNode(@RequestParam String groupId, @RequestParam String gatewayId, @RequestParam String systemId) {
    try {
    logger.info(“网关算力与系统挂载配置。groupId:{} gatewayId:{} systemId:{}”, groupId, gatewayId, systemId);
    configManageService.distributionGatewayServerNode(groupId, gatewayId, systemId);
    return new Result<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo(), true);
    } catch (DuplicateKeyException e) {
    logger.warn(“查询应用服务配置项信息失败,唯一索引冲突。groupId:{} gatewayId:{} systemId:{}”, groupId, gatewayId, systemId, e);
    return new Result<>(ResponseCode.INDEX_DUP.getCode(), e.getMessage(), true);
    } catch (Exception e) {
    logger.error(“网关算力与系统挂载配置。groupId:{} gatewayId:{} systemId:{}”, groupId, gatewayId, systemId, e);
    return new Result<>(ResponseCode.UN_ERROR.getCode(), e.getMessage(), false);
    }
    }

这里功能实现并不难,主要需要注意一个分组下是有多组算力的,一套算力下可以管理多套 RPC 服务。所以是这样一套 1vnvn的关系结构。

  1. SDK 上报开关

源码详见:cn.bugstack.gateway.sdk.config.GatewaySDKServiceProperties

@ConfigurationProperties(prefix = “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;
/** 程序是否启用 */
private boolean enabled = true;

}

添加 enabled 属性信息,enabled = true 为可用。

源码详见:cn.bugstack.gateway.sdk.config.GatewaySDKAutoConfig

@Configuration
@EnableConfigurationProperties(GatewaySDKServiceProperties.class)
@ConditionalOnProperty(
prefix = “api-gateway-sdk”,
name = “enabled”,
havingValue = “true”,
matchIfMissing = true
)
public class GatewaySDKAutoConfig {

@Bean
public GatewayCenterService gatewayCenterService() {
    return new GatewayCenterService();
}

@Bean
public GatewaySDKApplication gatewaySDKApplication(GatewaySDKServiceProperties properties, GatewayCenterService gatewayCenterService) {
    return new GatewaySDKApplication(properties, gatewayCenterService);
}

}

配置 enabled 的生效处理。之后就可以使用这个注解进行启停控制了。

api-gateway-sdk:
enabled: true
address: http://192.168.1.102:8901 # 注册中心;从这里获取接口信息以及完成注册网关操作
systemId: api-gateway-test-provider
systemName: 网关sdk测试工程
systemRegistry: zookeeper://192.168.1.102:2181

这样就可以配置是否允许 RPC 服务上报接口。

  1. 通信层添加反馈信息

@Override
protected void session(ChannelHandlerContext ctx, Channel channel, FullHttpRequest request) {
logger.info(“网关接收请求【消息】 uri:{} method:{}”, request.uri(), request.method());
try {
// 1. 解析请求参数
RequestParser requestParser = new RequestParser(request);
String uri = requestParser.getUri();
if (null == uri) return;
Map<String, Object> args = requestParser.parse();
// 2. 调用会话服务
GatewaySession gatewaySession = gatewaySessionFactory.openSession(uri);
IGenericReference reference = gatewaySession.getMapper();
SessionResult result = reference.$invoke(args);
// 3. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(“0000”.equals(result.getCode()) ? GatewayResultMessage.buildSuccess(result.getData()).setNode(node()) : GatewayResultMessage.buildError(AgreementConstants.ResponseCode._404.getCode(), “网关协议调用失败!”).setNode(node()));
channel.writeAndFlush(response);
} catch (Exception e) {
// 4. 封装返回结果
DefaultFullHttpResponse response = new ResponseParser().parse(GatewayResultMessage.buildError(AgreementConstants.ResponseCode._502.getCode(), “网关协议调用失败!” + e.getMessage()).setNode(node()));
channel.writeAndFlush(response);
}
}

private String node(){
return gatewaySessionFactory.getConfiguration().getHostName() + “:” + gatewaySessionFactory.getConfiguration().getPort();
}

这里把在调用网关后返回一个 node 节点信息,把ip和端口反馈回去。类似这样的功能可以通过配置管理,避免正式使用的时候也反馈这样的信息。

四、功能使用

注意;启动Nginx、Zookeeper、Redis,之后分别启动 api-gateway-center、api-gateway-test-provider 和几个需要的 api-gateway-engine

这里我们先在启动 api-gateway-test 的时候,在配置中开启和关闭验证是否上报接口信息。

api-gateway-sdk:
enabled: true
address: http://192.168.1.102:8901 # 注册中心;从这里获取接口信息以及完成注册网关操作
systemId: api-gateway-test-provider
systemName: 网关sdk测试工程
systemRegistry: zookeeper://192.168.1.102:2181

调用:http://192.168.1.102:8090/10001/wg/activity/sayHi?str=10001

通过调用的日志可以看到此时已经 node 的节点信息打印了,如果你多测试几遍就会看到对应的节点信息变化。


https://allendericdalexander.github.io/2026/07/24/archtect/gateway/
作者
AtLuoFu
发布于
2026年7月24日
许可协议