前置项目请参考《Spring Cloud Alibaba入门十二:Spring Cloud Gateway的使用》,这里来说说如何实现Gateway的动态网关路由,我们不采用配置中心JSON串的形式,因为不太方便管理,因此我们采用数据库的形式来进行处理
1. 创建Gateway网关路由管理表
数据库这里采用的是MySql8,建表sql如下:
/*
Navicat Premium Data Transfer
Source Server : 本机
Source Server Type : MySQL
Source Server Version : 80021
Source Host : 127.0.0.1:3306
Source Schema : nacos
Target Server Type : MySQL
Target Server Version : 80021
File Encoding : 65001
Date: 04/11/2021 16:27:13
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for config_gateway
-- ----------------------------
DROP TABLE IF EXISTS `config_gateway`;
CREATE TABLE `config_gateway` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`route_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由名称',
`route_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由ID',
`route_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由映射地址',
`route_predicate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由匹配方式',
`route_pattern` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由匹配规则',
`route_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由类型,0 http协议地址 1 注册中心地址',
`state` int NOT NULL DEFAULT 1 COMMENT '路由状态: 0不可用 1禁用',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注信息',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '动态网关配置表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2. pom.xml引入MySql连接和MyBatis依赖
<!-- 3.引入MySQL连接支持 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 4.开启mybatis支持,一定要使用starter,不然无法自动配置和注入 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
3. application.yml配置文件中添加数据库连接信息
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/nacos?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
username: root
password: 666666
然后屏蔽配置文件中设置的网关路由信息
4. 编写后台相关接口
4.1 编写实体类
public class ConfigGateway {
private Integer id;
private String routeName;
private String routeId;
private String routeUri;
private String routePredicate;
private String routePattern;
private String routeType;
private Integer state;
private String remark;
// get、set方法省略,或者你可以使用Lombok自动生成
}
4.2 编写DAO
import java.util.List;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import com.qfx.entity.ConfigGateway;
public interface ConfigGatewayDao {
("select id, route_name routeName, route_id routeId, route_uri routeUri, route_predicate routePredicate,"
+ " route_pattern routePattern, route_type routeType, state, remark from config_gateway where state = 1")
List<ConfigGateway> list();
("delete from config_gateway where id = #{id}")
int deleteByPk(Integer id);
("update config_gateway set route_name = #{record.routeName}, route_id = #{record.routeId}, route_uri = #{record.routeUri},"
+ " route_predicate = #{record.routePredicate}, route_pattern = #{record.routePattern}, route_type = #{record.routeType},"
+ " state= #{record.state}, remark = #{record.remark} where id = #{record.id}")
int updateByPk(ConfigGateway record);
}
4.3 编写service
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import com.qfx.dao.ConfigGatewayDao;
import com.qfx.entity.ConfigGateway;
import reactor.core.publisher.Mono;
/**
* <h5>描述:Gateway动态网关路由业务</h5>
*
*/
public class ConfigGatewayService implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
private RouteDefinitionWriter routeDefinitionWriter;
private ConfigGatewayDao configGatewayDao;
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* <h5>描述:刷新所有路由</h5>
*
* @return
*/
public String loadAllLoadRoute() {
List<ConfigGateway> configGatewayList = configGatewayDao.list();
String loadResult = "初始化路由信息失败:未获取到到路由信息!";
for (ConfigGateway gb : configGatewayList) {
loadResult = loadRoute(gb);
}
return loadResult;
}
/**
* <h5>描述:删除路由</h5>
*
* @param id 路由ID
* @return
*/
public String deleteRoute(String id) {
String delResult;
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
// 通知更改
notifyChanged();
delResult = "路由信息[" + id + "]删除成功!";
} catch (Exception e) {
if (e.getMessage().contains("RouteDefinition not found")) {
delResult = "路由信息[" + id + "]不存在,无需删除!";
} else {
e.printStackTrace();
delResult = "路由信息[" + id + "]删除失败:" + e.getMessage();
}
}
return delResult;
}
// =================== private method ==================
/**
* <h5>描述:刷新路由</h5>
*
* @param configGateway
* @return
*/
private String loadRoute(ConfigGateway configGateway) {
RouteDefinition definition = new RouteDefinition();
Map<String, String> predicateParams = new HashMap<>(8);
PredicateDefinition predicate = new PredicateDefinition();
FilterDefinition filterDefinition = new FilterDefinition();
Map<String, String> filterParams = new HashMap<>(8);
String loadResult;
try {
URI uri = null;
if ("1".equals(configGateway.getRouteType())) {
// 如果配置路由type为0的话 则从注册中心获取服务地址
uri = UriComponentsBuilder.fromUriString("lb://" + configGateway.getRouteUri() + "/").build().toUri();
} else {
uri = UriComponentsBuilder.fromHttpUrl(configGateway.getRouteUri()).build().toUri();
}
// 定义的路由唯一的id
definition.setId(configGateway.getRouteId());
// 定义匹配规则
predicate.setName(configGateway.getRoutePredicate());
//路由转发地址
predicateParams.put("pattern", configGateway.getRoutePattern());
predicate.setArgs(predicateParams);
// 名称是固定的, 路径去前缀
filterDefinition.setName("StripPrefix");
filterParams.put("_genkey_0", "1");
filterDefinition.setArgs(filterParams);
definition.setPredicates(Arrays.asList(predicate));
definition.setFilters(Arrays.asList(filterDefinition));
definition.setUri(uri);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
// 通知更改
notifyChanged();
loadResult = "初始化路由信息成功!";
} catch (Exception e) {
e.printStackTrace();
loadResult = "初始化路由信息发生异常:" + e.getMessage();
}
return loadResult;
}
/**
* <h5>描述:通知所有注册到此应用程序的匹配侦听器应用程序(通知更改)</h5>
*
*/
private void notifyChanged() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
4.4 编写Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.qfx.service.ConfigGatewayService;
("configGateway")
public class ConfigGatewayController {
private ConfigGatewayService configGatewayService;
/**
* 同步网关路由配置信息
*
* @return
*/
("sync")
public String syncGatewayConfig() {
return configGatewayService.loadAllLoadRoute();
}
/**
* 删除指定路由ID的网关路由配置信息
*
* @return
*/
("delete/{id}")
public String deleteGatewayConfig( String id) {
return configGatewayService.deleteRoute(id);
}
}
4.5 编辑启动类
指定Mybatis指定Mapper需要扫描的包
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
("com.qfx.*.dao") // 指定Mapper需要扫描的包,这样就不用每个Mapper上都添加@Mapper注解了
public class QfxSpringcloudGatewayDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QfxSpringcloudGatewayDemoApplication.class, args);
}
}
4.6 数据库添加测试数据
INSERT INTO `nacos`.`config_gateway` VALUES (1, '产品路由', 'qfx-product', 'product-service', 'Path', '/product/**', '1', 1, NULL);
INSERT INTO `nacos`.`config_gateway` VALUES (2, '产品路由', 'qfx-product', 'product-service', 'Method', 'get', '1', 1, NULL);
INSERT INTO `nacos`.`config_gateway` VALUES (3, '产品路由2', 'qfx-productExt', 'product-service', 'Path', '/productExt/**', '1', 0, NULL);
注意:相同路由ID匹配多个规则,只需要添加多条相同数据,并分别指定route_predicate和route_pattern即可
5. 启动测试
注意需先启动Nacos,如果要测试路由是否生效,也需要将对应的微服务启动
5.1 动态网关路由同步
http://127.0.0.1:8090/configGateway/sync
5.2 调用接口
http://127.0.0.1:8090/product/book/add?bookName=三体
5.3 删除指定ID的动态网关路由
http://127.0.0.1:8090/configGateway/delete/qfx-product
路径中最后一个参数"qfx-product"就是4.6中的route_id(动态网关路由ID)
5.4 说明:
调用5.2之前必须先执行5.1,否则会报404
执行5.3之后,5.2同样会报404,因为已经把对应的网关路由给删除了,必须重新执行5.1之后5.2才可以正常访问