业务流程
现在我们来回顾下我们之前几节的内容,我们前面所做的所有内容都围绕这一个主题进行:Dubbo的泛化调用。我们前面几节的内容都是让我们的网关可以更好的进行Dubbo的泛化调用,不断地优化设计,使整个调用可扩展,尽量解耦合,尽量安全。但是我们的网关功能不可能只有这一点,上面的1-8节都更像网关的一个核心通信组件,那么有一个非常重要的点就是这个通信组件究竟是和谁通信呢。本节的内容不在于代码的开发,而更多的在于网关整体的架构设计。请看下图(图片来源于xfg)
如图:
我们之前做的内容都属于
api-gateway-core
的内容,这是最核心的通信层(包装Netty),但是正如我们前面的代码开发的那般,我们对于这块的使用需要进行配置Configuration
,而这个配置在初始化的过程中需要加载映射HttpStatement
,这映射中包含着当我们用CGLIB动态创建代理对象时,一旦外部http请求到达网关,这个动态代理对象就会执行内部空的方法,这个方法执行时,会被拦截,从而走本地引用缓存genericService
执行的远程泛化调用,这里就设计到我们的映射HttpStatement
,它里面包含着每一个访问网关的请求对应者远程的RPC那个服务,我们的网关不可能一直如测试类这样进行手动的创建HttpStatement
,然后给Configuration
,所以接下来就涉及到网关注册中心,让其提供这种映射。
现在我们来仔细梳理这张图中的设计结构,我们可以通过
api-gateway-admin
手动录入相关RPC接口的映射,然后放入数据库DB中,然后由注册中心api-gateway-center
从中读取,或者我们设计api-gateway-sdk
组件,将其嵌入应用中,当服务引入这个组件并且动时,只要检测到那些接口使用了组件特定的注解,组件便将其注册到api-gateway-center
中去。用以上的两种方式我们的网关的注册中心将会包含对应服务的接口映射,而网关便可从中取得相关映射,进行服务的远程调用。在图中的结构中,我们为什么还会设计
api-gateway-assist
和api-gateway-engine
这两个中间模块呢,而不是直接让api-gateway-core
与api-gateway-center
相连接呢?在之前的对api-gateway-core
的设计中,我们参考的是Mybatis的设计,我们会将这泛化调用当成一种数据资源,无论是从单一职责原则或者是数据资源的考虑,它都不适合再添加注册网关和拉取接口的功能,同时既然是数据资源mybatis之类的东西,在调用时,我们往往会做一层包装api-gateway-assist
,而其中的api-gateway-engine
这个模块,就会更像spring starter一样,在启动时加载各种各样的component容器,同时spring starter也是要与mybatis解耦合的,无论spring starter或者mybatis发生怎样的版本更新,它都不会影响到对方的使用。最后本章节我们先来开发的就是网关注册中心
api-gateway-center
的内容。为什么先开发这一快呢?因为先开发这一块既可以为后续的api-gateway-sdk
做准备,也可以在开发完成后,为api-gateway-engine
部分定义接口的标准提供获取样例,使整个流程无论是开发剩余的哪一个模块,都可以让流程连贯起来,如果先开发其它模块,其业务整体的连贯性反而没有那么强。
所以接下来很多节的内容都会是注册中心的开发,本节我们先来对注册中心做一个初始化的,把整个注册中心的框架搭建起来,后续填补对应的的功能代码。整个注册中心的工程采用简单的DDD工程模型结构开发,对外提供 HTTP 接口。之所以提供 HTTP 接口是因为可以引入更少的组件,也能满足需求。
整体的过程目录如下
工程初始化
初始化DDD工程架构,并做一个已有映射信息的查询服务
环境搭建
由于注册中心可以从数据库DB中获取映射数据,因此在这里我们要引入数据库MySQL,而在之前api-gateway-core
的测试中,我们也使用了zookeeper组件,这里我们都使用docker-compose进行创建
docker-compose-environment.yml
version: '1.0'
services:
zookeeper:
image: zookeeper:3.4.13
container_name: zookeeper
restart: always
hostname: zoo1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zookeeper:2888:3888
networks:
- api-gateway
mysql:
image: mysql:8.0.32
container_name: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
ports:
- "13306:3306"
volumes:
- ./mysql/my.cnf:/etc/mysql/conf.d/mysql.cnf:ro
- ./mysql/sql:/docker-entrypoint-initdb.d
networks:
- api-gateway
networks:
api-gateway:
driver: bridge
同时对于数据库MySQL在doc目录下配置有my.cnf
[client]
port = 3306
default-character-set = utf8mb4
[mysqld]
user = mysql
port = 3306
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
default-storage-engine = InnoDB
default-authentication-plugin = mysql_native_password
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4'
log-bin = mysql-bin
binlog-format = row
server-id = 1
binlog-do-db = big_market_01
binlog-do-db = big_market_02
slow_query_log
#long_query_time = 3
slow-query-log-file = /var/log/mysql/mysql.slow.log
log-error = /var/log/mysql/mysql.error.log
default-time-zone = '+8:00'
[mysql]
default-character-set = utf8mb4
以及在sql目录下的有关数据库表的创建文件api-gateway.sql
/*
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: 22/10/2022 15:47:44
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 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;
-- ----------------------------
-- Records of http_statement
-- ----------------------------
BEGIN;
INSERT INTO `http_statement` VALUES (1, 'api-gateway-test', 'cn.bugstack.gateway.rpc.IActivityBooth', 'sayHi', 'java.lang.String', '/wg/activity/sayHi', 'GET', 0, '2022-10-22 15:30:00', '2022-10-22 15:30:00');
INSERT INTO `http_statement` VALUES (2, 'api-gateway-test', 'cn.bugstack.gateway.rpc.IActivityBooth', 'insert', 'cn.bugstack.gateway.rpc.dto.XReq', '/wg/activity/insert', 'POST', 1, '2022-10-22 15:30:00', '2022-10-22 15:30:00');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
配置写好后启动docker-compose-environment.yml
,利用docker创建对应的容器即可
DDD工程搭建
首先按照DDD工程的架构,它会有许多模块,但是我们这个网关注册中心本身不属于复杂的业务类型项目,所以也就不需要像以往做的业务项目那样创建多个模块,但是对于DDD架构的思想我们可以用对应的包来代替以往业务中的模块
按照DDD的思想,我们所有与数据库相关的操作都会放在infrastructure包下,所有的领域设计都会放在domain包下,对外的接口设计都会放于interfaces包下,而对应领域服务的标准定义,我们都会放于application包下。
关于DDD设计的内容详情可以参考文章DDD专题案例
对应工程的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zshunbao</groupId>
<artifactId>api-gateway-ceneter-01</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--
# mysql 5.x driver-class-name: com.mysql.jdbc.Driver mysql-connector-java 5.1.34
# mysql 8.x driver-class-name: com.mysql.cj.jdbc.Driver mysql-connector-java 8.0.22-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</dependency>
</dependencies>
<build>
<finalName>api-gateway-center-01</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/**</include>
</includes>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
启动类ApiGatewayApplication.java
package com.zshunbao.gateway.center;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @program: api-gateway-center
* @ClassName ApiGatewayApplication
* @description: 网关注册中心启动服务
* @author: zs宝
* @create: 2025-08-16 15:53
* @Version 1.0
**/
@SpringBootApplication
@Configurable
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
接下来我们来填充一些基本的内容
infrastructure包
在业务流程中我们提到过,注册中心主要就是要提供对应的映射信息,而数据库中保存的也就是这个,即在api-gateway-core
中的HttpStatement
。因此对于po包下我们就要创建
HttpStatement
(不能说与api-gateway-core
下一模一样,只能说是几乎完全一致)
package com.zshunbao.gateway.center.infrastructure.po;
import java.util.Date;
/**
* @program: api-gateway-center
* @ClassName HttpStatement
* @description: 接口映射
* @author: zs宝
* @create: 2025-08-16 16:08
* @Version 1.0
**/
public class HttpStatement {
/** 自增ID */
private Integer id;
/** 应用名称; */
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 String httpCommandType;
/** 是否鉴权;true = 1是、false = 0否 */
private Integer auth;
/** 创建时间 */
private Date createTime;
/** 更新时间 */
private Date updateTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getApplication() {
return application;
}
public void setApplication(String application) {
this.application = application;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getHttpCommandType() {
return httpCommandType;
}
public void setHttpCommandType(String httpCommandType) {
this.httpCommandType = httpCommandType;
}
public Integer getAuth() {
return auth;
}
public void setAuth(Integer auth) {
this.auth = auth;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
dao包下有对应的mybatis的dao接口
package com.zshunbao.gateway.center.infrastructure.dao;
import com.zshunbao.gateway.center.infrastructure.po.HttpStatement;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @program: api-gateway-center
* @ClassName IHttpStatementDao
* @description:
* @author: zs宝
* @create: 2025-08-16 16:07
* @Version 1.0
**/
@Mapper
public interface IHttpStatementDao {
List<HttpStatement> queryHttpStatementList();
}
而repository包下其实是关于领域层的repository的接口实现,这里有一个查询,我们在完成domain层的内容后该有
package com.zshunbao.gateway.center.infrastructure.repository;
import com.zshunbao.gateway.center.domain.model.ApiData;
import com.zshunbao.gateway.center.domain.repository.IApiRepository;
import com.zshunbao.gateway.center.infrastructure.dao.IHttpStatementDao;
import com.zshunbao.gateway.center.infrastructure.po.HttpStatement;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @program: api-gateway-center
* @ClassName ApiRepository
* @description: 仓储实现
* @author: zs宝
* @create: 2025-08-16 16:09
* @Version 1.0
**/
@Component
public class ApiRepository implements IApiRepository {
@Resource
private IHttpStatementDao httpStatementDao;
@Override
public List<ApiData> queryHttpStatementList() {
List<HttpStatement> httpStatements = httpStatementDao.queryHttpStatementList();
List<ApiData> apiDataList = new ArrayList<>(httpStatements.size());
for (HttpStatement httpStatement : httpStatements) {
ApiData apiData = new ApiData();
apiData.setApplication(httpStatement.getApplication());
apiData.setInterfaceName(httpStatement.getInterfaceName());
apiData.setMethodName(httpStatement.getMethodName());
apiData.setParameterType(httpStatement.getParameterType());
apiData.setUri(httpStatement.getUri());
apiData.setHttpCommandType(httpStatement.getHttpCommandType());
apiData.setAuth(httpStatement.getAuth());
apiDataList.add(apiData);
}
return apiDataList;
}
}
domain包
由于先做了一个有关映射信息的查询服务
因此model包下有
package com.zshunbao.gateway.center.domain.model;
/**
* @program: api-gateway-center
* @ClassName ApiData
* @description: API 数据 其实就是api-gateway-core里面的httpStatement
* @author: zs宝
* @create: 2025-08-16 16:01
* @Version 1.0
**/
public class ApiData {
/** 应用名称; */
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 String httpCommandType;
/** 是否鉴权;true = 1是、false = 0否 */
private Integer auth;
public String getApplication() {
return application;
}
public void setApplication(String application) {
this.application = application;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getHttpCommandType() {
return httpCommandType;
}
public void setHttpCommandType(String httpCommandType) {
this.httpCommandType = httpCommandType;
}
public Integer getAuth() {
return auth;
}
public void setAuth(Integer auth) {
this.auth = auth;
}
}
repository包下有
package com.zshunbao.gateway.center.domain.repository;
import com.zshunbao.gateway.center.domain.model.ApiData;
import java.util.List;
/**
* @program: api-gateway-center
* @ClassName IApiRepository
* @description: API 仓储
* @author: zs宝
* @create: 2025-08-16 16:02
* @Version 1.0
**/
public interface IApiRepository {
List<ApiData> queryHttpStatementList();
}
service包下
package com.zshunbao.gateway.center.domain.service;
import com.zshunbao.gateway.center.application.IApiService;
import com.zshunbao.gateway.center.domain.model.ApiData;
import com.zshunbao.gateway.center.domain.repository.IApiRepository;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* @program: api-gateway-center
* @ClassName ApiServiceImpl
* @description:
* @author: zs宝
* @create: 2025-08-16 16:06
* @Version 1.0
**/
@Service
public class ApiServiceImpl implements IApiService {
@Resource
private IApiRepository apiRepository;
@Override
public List<ApiData> queryHttpStatementList() {
return apiRepository.queryHttpStatementList();
}
}
这个服务对应与application层下的接口
package com.zshunbao.gateway.center.application;
import java.util.List;
import com.zshunbao.gateway.center.domain.model.ApiData;
/**
* @program: api-gateway-center
* @ClassName IApiService
* @description: API 服务
* @author: zs宝
* @create: 2025-08-16 16:05
* @Version 1.0
**/
public interface IApiService {
List<ApiData> queryHttpStatementList();
}
resource配置
工程的resource目录下主要有三个配置
首先是有关mybatis的两个配置
src/main/resources/config/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 暂时未使用 -->
<typeAliases>
</typeAliases>
</configuration>
src/main/resources/mapper/Http_Statement.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zshunbao.gateway.center.infrastructure.dao.IHttpStatementDao">
<resultMap id="HttpStatementMap" type="com.zshunbao.gateway.center.infrastructure.po.HttpStatement">
<id column="id" property="id"/>
<id column="application" property="application"/>
<id column="interface_name" property="interfaceName"/>
<id column="method_name" property="methodName"/>
<id column="parameter_type" property="parameterType"/>
<id column="uri" property="uri"/>
<id column="http_command_type" property="httpCommandType"/>
<id column="auth" property="auth"/>
<id column="create_time" property="createTime"/>
<id column="update_time" property="updateTime"/>
</resultMap>
<select id="queryHttpStatementList" resultMap="HttpStatementMap">
SELECT application, interface_name, method_name, parameter_type, uri, http_command_type, auth, create_time, update_time
FROM http_statement
</select>
</mapper>
最后是有关整个工程的配置
src/main/resources/application.yml
server:
port: 80
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:13306/api_gateway?useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:/mapper/*.xml
config-location: classpath:/config/mybatis-config.xml
测试
package com.zshunbao.gateway.center.test;
import com.alibaba.fastjson.JSON;
import com.zshunbao.gateway.center.application.IApiService;
import com.zshunbao.gateway.center.domain.model.ApiData;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.List;
/**
* @program: api-gateway-center
* @ClassName ApiTest
* @description: 单元测试
* @author: zs宝
* @create: 2025-08-16 16:14
* @Version 1.0
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Resource
private IApiService apiService;
@Test
public void test() {
List<ApiData> apiDataList = apiService.queryHttpStatementList();
logger.info("测试结果:{}", JSON.toJSONString(apiDataList));
}
}
测试结果
由于提供了对外接口,这里我们再启动整个项目,访问测试一下
访问