Gateway动态路由
What?
前段时间买了个服务器,没怎么用,就跑了个在线获取IdeaCode
的程序。使用率不怎么高,这次准备在跑一个Gateway
网关,以后就把我所有的程序都接入到网关里。但是以前网关里的路由都是硬编码的形式写到配置文件里的,这就意味着我每发布一个程序都要重新打包部署一下网关。
程序猿的存在就是解决一些需要频繁操作的事件,所以要想办法解决硬编码路由的问题,所以我写了本篇Gateway
动态路由。
思路
Gateway
的路由配置有两种方式,一种是通过配置文件配置,一种是通过代码配置。我准备做一个类似于管理系统的系统来管理路由配置,所以要使用代码的方式配置路由。
在项目启动的时候从数据库读取配置并且存到Redis
中。Gateway
在初始化的时候从Redis
中获取配置。
开发前的准备
- 一台电脑(废话,没有电脑怎么开发)
- Nacos注册中心(路由转发需要用到)
- MySQL数据库(持久化的储存路由配置)
- Redis(路由配置缓存)
- JDK1.8(我是基于JDK1.8做的开发)
- Maven(现在可是Maven的天下,总不能一个一个的添加依赖吧)
预览
Github:https://github.com/Ys2025/gateway-demo
Start
1、初始化数据库
CREATE TABLE `gateway_route` (
`id` bigint NOT NULL,
`route_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`uri` varchar(255) NOT NULL,
`predicates` varchar(255) NOT NULL,
`filters` varchar(255) DEFAULT NULL,
`ord` int DEFAULT '0',
`remarks` varchar(255) DEFAULT NULL,
`create_time` date DEFAULT NULL,
`update_time` date DEFAULT NULL,
`is_deleted` int DEFAULT '0',
`version` int DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
2、创建一个Gateway项目
启动类
package cn.yanghuisen.gateway;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@MapperScan("cn.yanghuisen.gateway.mapper")
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.11.RELEASEversion>
<relativePath/>
parent>
<groupId>cn.yanghuisengroupId>
<artifactId>gatewayartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gatewayname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud-alibaba.version>2.2.1.RELEASEspring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR9spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.yml
server:
port: 8080
spring:
application:
name: GATEWAY
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/gateway_route?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: 123456
redis:
# Redis地址
host: localhost
# Redis端口
port: 6379
# RedisDB库
database: 0
# 链接超市
timeout: 10000ms
lettuce:
pool:
# 最大连接数
max-active: 1024
# 最大阻塞等待时间
max-wait: 10000ms
# 最大空闲时间
max-idle: 200
# 最小空闲链接
min-idle: 5
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-value: 1 # 逻辑已经删除的值(默认为1)
logic-not-delete-value: 0 # 逻辑没有删除的值(默认为0)
3、配置Redis
package cn.yanghuisen.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author admin
* @version 1.0
* @date 2020/5/14 20:52
* @Description 自定义模板
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
// 创建模板
RedisTemplate redisTemplate = new RedisTemplate<>();// 设置String类型的Key的序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());// 设置String类型的value的序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置Hash类型的Key的序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());// 设置hash类型的value的序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置连接方式
redisTemplate.setConnectionFactory(lettuceConnectionFactory);return redisTemplate;
}
}
4、自定义路由储存库(核心1)
此类用于从
Redis
获取路由配置信息,以及监听的路由保存和删除
package cn.yanghuisen.gateway.config;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* @author Y
* @date 2020/11/28 0:02
* @desc 自定义路由储存库
*/
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
@Autowired
private RedisTemplate redisTemplate;private static final String GATEWAY_ROUTES = "gateway:routes";/**
* 获取路由信息,此处从Redis获取路由配置信息
* @return
*/@Overridepublic Flux getRouteDefinitions() {
List routeDefinitions = new ArrayList<>();// 获取Redis中配置的路由信息
List routes = redisTemplate.opsForHash().values(GATEWAY_ROUTES);// 遍历路由
routes.forEach(route->{// 把json反序列话为RouteDefinition类对象
RouteDefinition routeDefinition = JSON.parseObject(route.toString(), RouteDefinition.class);
routeDefinitions.add(routeDefinition);
});return Flux.fromIterable(routeDefinitions);
}/**
* 添加路由
* @param route
* @return
*/@Overridepublic Mono save(Mono route) {return route.flatMap(routeDefinition -> {// 把路由信息存到Redis中
redisTemplate.opsForHash().put(GATEWAY_ROUTES,routeDefinition.getId(), JSON.toJSONString(routeDefinition));return Mono.empty();
});
}/**
* 删除路由
* @param routeId
* @return
*/@Overridepublic Mono delete(Mono routeId) {return routeId.flatMap(id -> {// 判断redis中是否有该id的路由数据if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTES,id)){// 删除数据
redisTemplate.opsForHash().delete(GATEWAY_ROUTES,id);return Mono.empty();
}return Mono.defer(()-> Mono.error(new NotFoundException("Redis中没有该路由:"+id)));
});
}
}
5、SpringBoot启动配置(核心2)
此类用于在
SpringBoot
启动的时候从数据库读取路由配置信息,并且把信息储存到Redis
中。以及发布路由的增删改事件
package cn.yanghuisen.gateway.handler;
import cn.yanghuisen.gateway.dto.GatewayRouteDTO;
import cn.yanghuisen.gateway.entity.GatewayRoute;
import cn.yanghuisen.gateway.mapper.GatewayRouteMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Y
* @date 2020/11/28 0:38
* @desc
*/
@Slf4j
@Component
public class GatewayServiceHandler implements ApplicationEventPublisherAware, CommandLineRunner {
private static final String GATEWAY_ROUTES = "gateway:routes";
private ApplicationEventPublisher publisher;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Autowired
private GatewayRouteMapper gatewayRouteMapper;
@Autowired
private RedisTemplate redisTemplate;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;
}/**
* 项目启动的时候执行
* @param args
* @throws Exception
*/@Overridepublic void run(String... args) throws Exception {// 记载路由配置this.loadRouteConfig();
}public String loadRouteConfig(){
log.info("开始加载网关路由配置信息");// 删除Redis里的路由配置信息
redisTemplate.delete(GATEWAY_ROUTES);// 从数据库查询数据
List gatewayRoutes = gatewayRouteMapper.selectList(null);
gatewayRoutes.forEach(gatewayRoute -> {
RouteDefinition definition = handleData(gatewayRoute);// 保存路由
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
});// 发布事件,通知更新数据this.publisher.publishEvent(new RefreshRoutesEvent(this));return "success";
}/**
* 保存路由
* @param dto
*/public void saveRoute(GatewayRouteDTO dto){
GatewayRoute gatewayRoute = new GatewayRoute();
BeanUtils.copyProperties(dto,gatewayRoute);// GatewayRoute转为RouteDefinition
RouteDefinition definition = handleData(gatewayRoute);// 保存路由数据
routeDefinitionWriter.save(Mono.just(definition)).subscribe();// 发布事件,通知更新数据this.publisher.publishEvent(new RefreshRoutesEvent(this));
}/**
* 更新路由
* @param dto
*/public void updateRoute(GatewayRouteDTO dto){
GatewayRoute gatewayRoute = new GatewayRoute();
BeanUtils.copyProperties(dto,gatewayRoute);// GatewayRoute转为RouteDefinition
RouteDefinition definition = handleData(gatewayRoute);// 根据路由ID删除路由信息
routeDefinitionWriter.delete(Mono.just(dto.getOldRouteId())).subscribe();// 重新保存路由数据
routeDefinitionWriter.save(Mono.just(definition)).subscribe();// 发布事件,通知更新数据this.publisher.publishEvent(new RefreshRoutesEvent(this));
}/**
* 删除路由
* @param routeId
*/public void deleteRoute(String routeId){// 根据路由ID删除路由信息
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();// 发布事件,通知更新数据this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("删除完毕");
}/**
* GatewayRoute转RouteDefinition
* @param gatewayRoute
* @return
*/public RouteDefinition handleData(GatewayRoute gatewayRoute){
RouteDefinition routeDefinition = new RouteDefinition();
URI uri = null;// 判断Uri是不是http地址if (gatewayRoute.getUri().startsWith("http")){// http地址
uri = UriComponentsBuilder.fromHttpUrl(gatewayRoute.getUri()).build().toUri();
}else {// 微服务服务名
uri = UriComponentsBuilder.fromUriString("lb://"+gatewayRoute.getUri()).build().toUri();
}// 设置路由ID
routeDefinition.setId(gatewayRoute.getRouteId());// 设置uri
routeDefinition.setUri(uri);//谓语(路由转发条件)
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
Map predicateArgs = new HashMap<>();
predicateArgs.put("pattern",gatewayRoute.getPredicates());
predicate.setArgs(predicateArgs);
routeDefinition.setPredicates(Collections.singletonList(predicate));// 设置ordif (null!=gatewayRoute.getOrd()){
routeDefinition.setOrder(gatewayRoute.getOrd());
}return routeDefinition;
}
}
6、创建entity和DTO类
package cn.yanghuisen.gateway.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* @author Y
* @date 2020/11/28 1:16
* @desc 实体类
*/
@Data
public class GatewayRoute {
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String routeId;
private String uri;
private String predicates;
private String filters;
private Integer ord;
private String remarks;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date updateTime;
/**
* 逻辑删除
*/
@TableLogic
private Integer isDeleted;
/**
* 乐观锁
*/
@Version
private Integer version;
}
package cn.yanghuisen.gateway.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* @author Y
* @date 2020/11/28 1:16
* @desc DTO类
*/
@Data
public class GatewayRouteDTO {
private String id;
private String routeId;
private String oldRouteId;
private String uri;
private String predicates;
private String filters;
private Integer ord;
private String remarks;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date updateTime;
private Integer isDeleted;
private Integer version;
}
7、创建Service层接口
package cn.yanghuisen.gateway.service;
import cn.yanghuisen.gateway.dto.GatewayRouteDTO;
import java.util.List;
/**
* @author Y
* @date 2020/11/28 22:38
* @desc
*/
public interface IRouteService {
/**
* 添加路由
* @param dto
* @return
*/
Integer add(GatewayRouteDTO dto);
/**
* 更新路由
* @param dto
* @return
*/
Integer update(GatewayRouteDTO dto);
/**
* 删除路由
* @param routeId
* @return
*/
Integer delete(String routeId);
/**
* 获取列表
* @return
*/
List list();
}
package cn.yanghuisen.gateway.service.impl;
import cn.yanghuisen.gateway.dto.GatewayRouteDTO;
import cn.yanghuisen.gateway.entity.GatewayRoute;
import cn.yanghuisen.gateway.mapper.GatewayRouteMapper;
import cn.yanghuisen.gateway.service.IRouteService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author Y
* @date 2020/11/28 22:40
* @desc
*/
@Service
public class RouteServiceImpl implements IRouteService {
@Autowired
private GatewayRouteMapper routeMapper;
@Override
public Integer add(GatewayRouteDTO dto) {
GatewayRoute gatewayRoute = new GatewayRoute();
BeanUtils.copyProperties(dto,gatewayRoute);
return routeMapper.insert(gatewayRoute);
}
@Override
public Integer update(GatewayRouteDTO dto) {
GatewayRoute gatewayRoute = new GatewayRoute();
BeanUtils.copyProperties(dto,gatewayRoute);
return routeMapper.updateById(gatewayRoute);
}
@Override
public Integer delete(String routeId) {
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("route_id",routeId);return routeMapper.delete(queryWrapper);
}@Overridepublic List list() {
List result = new ArrayList<>();
List gatewayRoutes = routeMapper.selectList(null);
gatewayRoutes.forEach(gatewayRoute -> {
GatewayRouteDTO dto = new GatewayRouteDTO();
BeanUtils.copyProperties(gatewayRoute,dto);
dto.setOldRouteId(gatewayRoute.getRouteId());
result.add(dto);
});return result;
}
}
8、controller接口
package cn.yanghuisen.gateway.controller;
import cn.yanghuisen.gateway.dto.GatewayRouteDTO;
import cn.yanghuisen.gateway.handler.GatewayServiceHandler;
import cn.yanghuisen.gateway.service.IRouteService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author Y
* @date 2020/11/28 22:28
* @desc 路由API接口
*/
@RestController
@RequestMapping("/route")
@CrossOrigin
public class RouteController {
@Autowired
private GatewayServiceHandler gatewayServiceHandler;
@Autowired
private IRouteService routeService;
/**
* 刷新路由配置
* @return
*/
@GetMapping("/refresh")
public String refresh(){
return this.gatewayServiceHandler.loadRouteConfig();
}
/**
* 添加路由配置
* @param dto
* @return
*/
@PostMapping("/save")
public String add(@RequestBody GatewayRouteDTO dto){
if (StringUtils.isNotBlank(dto.getId())){
this.gatewayServiceHandler.updateRoute(dto);
this.routeService.update(dto);
}else {
this.gatewayServiceHandler.saveRoute(dto);
this.routeService.add(dto);
}
return "success";
}
@GetMapping("/delete")
public String delete(String routeId){
this.gatewayServiceHandler.deleteRoute(routeId);
this.routeService.delete(routeId);
return "success";
}
@GetMapping("/list")
public List list(){
return this.routeService.list();
}
}
9、前端代码
新增路由刷新路由 :data="tableData"
:row-class-name="tableRowClassName"> prop="id"
label="ID"
show-overflow-tooltip> prop="routeId"
label="routeId"
show-overflow-tooltip> prop="uri"
label="uri"
show-overflow-tooltip> prop="predicates"
label="predicates"
show-overflow-tooltip> prop="filters"
label="filters"
show-overflow-tooltip> prop="ord"
label="order"
> prop="remarks"
label="remarks"
> prop="createTime"
label="createTime"
> prop="updateTime"
label="updateTime"
> label="操作"
fixed="right"
> size="mini"
@click="handleEdit(scope.row)">编辑 size="mini"
type="danger"
@click="handleDelete(scope.row)">删除
取 消确 定
时间原因,代码中没做必填校验。filters
暂时没用到,也没写。有空再补充吧。