gateway动态路由_结合Redis和MySQL实现的Gateway动态路由

Gateway动态路由

What?

前段时间买了个服务器,没怎么用,就跑了个在线获取IdeaCode的程序。使用率不怎么高,这次准备在跑一个Gateway网关,以后就把我所有的程序都接入到网关里。但是以前网关里的路由都是硬编码的形式写到配置文件里的,这就意味着我每发布一个程序都要重新打包部署一下网关。

程序猿的存在就是解决一些需要频繁操作的事件,所以要想办法解决硬编码路由的问题,所以我写了本篇Gateway动态路由。

思路

Gateway的路由配置有两种方式,一种是通过配置文件配置,一种是通过代码配置。我准备做一个类似于管理系统的系统来管理路由配置,所以要使用代码的方式配置路由。

在项目启动的时候从数据库读取配置并且存到Redis中。Gateway在初始化的时候从Redis中获取配置。

开发前的准备

  1. 一台电脑(废话,没有电脑怎么开发)
  2. Nacos注册中心(路由转发需要用到)
  3. MySQL数据库(持久化的储存路由配置)
  4. Redis(路由配置缓存)
  5. JDK1.8(我是基于JDK1.8做的开发)
  6. Maven(现在可是Maven的天下,总不能一个一个的添加依赖吧)

预览

Github:https://github.com/Ys2025/gateway-demo

279cac04139f14d2e0896688b2866e67.png

39e0168b8eba5ac84bbab5b4281d0f88.png

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;
39c1b14c805b69624c608e416f4b4d41.png


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暂时没用到,也没写。有空再补充吧。

- END -
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值