欢迎光临
我们一直在努力

Gateway - 动态路由管理平台:通过 Web UI 实时配置路由

在这里插入图片描述

👋 大家好,欢迎来到我的技术博客! 💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Gateway这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


文章目录

  • Gateway – 动态路由管理平台:通过 Web UI 实时配置路由 🔄🌐
    • 一、引言 🎯
      • 1.1 微服务架构中的路由挑战 🌐
      • 1.2 动态路由的价值 🧠
      • 1.3 本文目标 🧪
    • 二、环境准备 🧱
      • 2.1 技术栈概览 🧱
      • 2.2 项目结构 🧱
      • 2.3 Maven 依赖配置 🧱
      • 2.4 数据库准备 🧱
        • 2.4.1 初始化数据库
        • 2.4.2 创建路由表
      • 2.5 应用配置 📝
    • 三、核心概念与设计思路 🧠
      • 3.1 动态路由的工作原理 🔄
      • 3.2 数据模型设计 🧠
        • 3.2.1 RouteDefinition 实体类 ✅
        • 3.2.2 数据访问接口 ✅
      • 3.3 路由转换工具类 ✅
      • 3.4 动态路由配置类 ✅
      • 3.5 路由业务逻辑服务 ✅
      • 3.6 路由控制器 ✅
      • 3.7 Mermaid 图解动态路由工作流程 🔄
    • 四、Web UI 开发 ✅
      • 4.1 前端页面设计 ✅
      • 4.2 启动与访问 ✅
    • 五、高级功能与优化 ✅
      • 5.1 路由状态实时同步 ✅
        • 5.1.1 简单的 SSE 方案 ✅
        • 5.1.2 前端监听 SSE ✅
      • 5.2 路由导入导出 ✅
        • 5.2.1 导出功能 ✅
        • 5.2.2 导入功能 ✅
      • 5.3 路由版本控制 ✅
        • 5.3.1 添加版本信息 ✅
        • 5.3.2 增加历史记录表 ✅
        • 5.3.3 在 Service 层记录变更 ✅
      • 5.4 权限控制 ✅
        • 5.4.1 添加 Spring Security 依赖 ✅
        • 5.4.2 配置安全策略 ✅
        • 5.4.3 配置用户 ✅
    • 六、性能优化与最佳实践 ✅
      • 6.1 数据库优化 ✅
      • 6.2 Gateway 性能优化 ✅
      • 6.3 安全性 ✅
      • 6.4 可靠性 ✅
    • 七、常见问题与解决方案 🚨
      • 7.1 路由更新后 Gateway 未及时生效 ✅
        • 7.1.1 问题现象 ✅
        • 7.1.2 常见原因与解决办法 ✅
      • 7.2 JSON 格式错误导致解析失败 ✅
        • 7.2.1 问题现象 ✅
        • 7.2.2 常见原因与解决办法 ✅
      • 7.3 前端 UI 响应缓慢 ✅
        • 7.3.1 问题现象 ✅
        • 7.3.2 常见原因与解决办法 ✅
    • 八、与现有系统的集成 ✅
      • 8.1 与服务注册中心集成 ✅
        • 8.1.1 自动发现服务 ✅
        • 8.1.2 动态构建路由 ✅
      • 8.2 与配置中心集成 ✅
      • 8.3 与监控系统集成 ✅
    • 九、总结与展望 📝🚀
      • 9.1 关键要点回顾 ✅
      • 9.2 未来发展趋势 ✅
    • 附录:相关资源链接 📚🔗

Gateway – 动态路由管理平台:通过 Web UI 实时配置路由 🔄🌐

在现代微服务架构中,服务的动态伸缩和灵活部署变得越来越重要。传统的静态路由配置方式(如硬编码在 application.yml 文件中)在面对频繁的服务变更时显得笨拙且难以维护。一个理想的解决方案是构建一个动态路由管理平台,允许运维人员或开发者通过一个Web UI实时地添加、修改、删除和启用/禁用路由规则,而无需重启服务或手动编辑配置文件。

本文将深入探讨如何构建这样一个动态路由管理平台,重点介绍如何在 Spring Cloud Gateway 上实现基于 Web UI 的动态路由配置。我们将从基础概念出发,逐步讲解如何设计和实现一个完整的动态路由管理功能,包括前端交互、后端 API、数据存储以及与 Gateway 的集成。通过本篇文章,你将掌握如何创建一个高效、易用的动态路由管理系统。

一、引言 🎯

1.1 微服务架构中的路由挑战 🌐

随着微服务架构的普及,应用被拆分成多个独立的服务单元,这些服务通过网络进行通信。一个典型的请求可能需要经过多个服务才能完成。为了实现服务间的通信,需要一个强大的API 网关来处理请求路由、认证、限流、熔断等功能。

然而,传统基于静态配置的路由方式存在以下问题:

  • 部署繁琐:每次修改路由都需要更新配置文件并重启服务。
  • 响应迟缓:在紧急情况下(如故障切换、临时维护),无法快速调整路由策略。
  • 缺乏可视化:路由规则分散在不同文件中,难以统一管理和查看。
  • 权限控制复杂:难以精确控制谁可以修改哪些路由规则。

1.2 动态路由的价值 🧠

动态路由管理平台旨在解决上述痛点,它提供了一套实时、可视化的路由配置机制,使运维团队能够:

  • 即时生效:修改后的路由规则可以立即在 Gateway 中生效,无需重启。
  • 集中管理:通过统一的 Web UI 界面,可以方便地查看、编辑所有路由规则。
  • 灵活控制:支持根据请求参数、头部信息、路径等条件进行复杂的路由匹配。
  • 权限隔离:可以为不同用户或角色分配不同的路由管理权限。

1.3 本文目标 🧪

本文旨在:

  • 介绍动态路由管理平台的核心概念与价值。
  • 详细阐述如何在 Spring Cloud Gateway 中实现动态路由管理。
  • 提供完整的 Java 代码示例,包括后端 API、数据库模型和数据访问层。
  • 展示如何构建一个简单的 Web UI 来管理路由。
  • 分享最佳实践和常见问题的解决方案。
  • 二、环境准备 🧱

    2.1 技术栈概览 🧱

    本项目主要涉及以下技术栈:

    • 后端:
      • Spring Boot: 快速构建应用的基础框架。
      • Spring Cloud Gateway: 提供路由功能的网关。
      • Spring Data JPA: 数据持久化层。
      • MySQL: 用于存储路由规则。
      • Lombok: 减少样板代码。
      • Swagger (可选): 用于 API 文档。
    • 前端(可选,本例使用简单 HTML+JavaScript):
      • HTML/CSS/JavaScript: 构建简单的 Web UI。
      • Axios: 发起 HTTP 请求。
      • Bootstrap (可选): 简化 UI 样式。
    • 工具:
      • Maven: 项目构建工具。
      • IDE (如 IntelliJ IDEA): 开发环境。
      • Postman (可选): 测试 API。
      • MySQL Workbench (可选): 数据库管理工具。

    2.2 项目结构 🧱

    dynamic-gateway-management/
    ├── src/
    │ ├── main/
    │ │ ├── java/
    │ │ │ └── com/
    │ │ │ └── example/
    │ │ │ └── dynamicgateway/
    │ │ │ ├── DynamicGatewayApplication.java (主类)
    │ │ │ ├── config/
    │ │ │ │ └── GatewayConfig.java (Gateway 动态路由配置)
    │ │ │ ├── controller/
    │ │ │ │ └── RouteController.java (路由管理 API)
    │ │ │ ├── model/
    │ │ │ │ └── RouteDefinition.java (路由实体)
    │ │ │ ├── repository/
    │ │ │ │ └── RouteRepository.java (数据访问层)
    │ │ │ ├── service/
    │ │ │ │ └── RouteService.java (路由业务逻辑)
    │ │ │ └── util/
    │ │ │ └── RouteUtil.java (路由转换工具)
    │ │ └── resources/
    │ │ ├── application.yml (配置文件)
    │ │ ├── data.sql (初始化数据)
    │ │ └── static/
    │ │ └── index.html (前端页面)
    │ └── test/
    └── pom.xml (Maven 配置)

    2.3 Maven 依赖配置 🧱

    <?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.example</groupId>
    <artifactId>dynamic-gateway-management</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>dynamic-gateway-management</name>
    <description>Dynamic Routing Management for Spring Cloud Gateway</description>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
    <relativePath/> <!– lookup parent from repository –>
    </parent>

    <properties>
    <java.version>11</java.version>
    <spring-cloud.version>2021.0.3</spring-cloud.version>
    </properties>

    <dependencies>
    <!– Spring Boot Web Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!– Spring Cloud Gateway –>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!– Spring Data JPA –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!– MySQL Driver –>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!– Lombok –>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>

    <!– Swagger (可选) –>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    </dependency>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
    </dependency>

    <!– Test Dependencies –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>

    </project>

    2.4 数据库准备 🧱

    2.4.1 初始化数据库

    创建一个名为 dynamic_gateway 的数据库:

    CREATE DATABASE IF NOT EXISTS dynamic_gateway CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    USE dynamic_gateway;

    2.4.2 创建路由表

    — 创建路由定义表
    CREATE TABLE IF NOT EXISTS route_definition (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    route_id VARCHAR(255) NOT NULL UNIQUE,
    route_name VARCHAR(255),
    uri VARCHAR(255) NOT NULL,
    predicates TEXT NOT NULL, — JSON 字符串,存储谓词列表
    filters TEXT, — JSON 字符串,存储过滤器列表
    order_num INT DEFAULT 0,
    status TINYINT DEFAULT 1, — 1: 启用, 0: 禁用
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );

    — 插入初始数据 (可选)
    INSERT INTO route_definition (route_id, route_name, uri, predicates, filters, order_num, status)
    VALUES ('user-service-route', 'User Service Route', 'lb://user-service', '[{\\"name\\":\\"Path\\",\\"args\\":{\\"pattern\\":\\"/api/user/**\\"}}]', '[{\\"name\\":\\"Retry\\",\\"args\\":{\\"retries\\":3,\\"statuses\\":\\"BAD_GATEWAY\\"}}]', 1, 1);

    2.5 应用配置 📝

    # application.yml

    server:
    port: 8080

    spring:
    application:
    name: dynamicgatewaymanagement

    datasource:
    url: jdbc:mysql://localhost:3306/dynamic_gateway?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root # 替换为你的数据库用户名
    password: your_password # 替换为你的数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver

    jpa:
    hibernate:
    ddl-auto: none # 设置为 none,因为我们手动管理表结构
    show-sql: true
    properties:
    hibernate:
    dialect: org.hibernate.dialect.MySQL8Dialect
    format_sql: true

    cloud:
    gateway:
    # 禁用默认的路由加载 (因为我们要通过动态方式加载)
    # 通常不需要显式设置,但明确一下有助于理解
    # routes: [] # 空路由列表,防止默认加载
    # 动态路由相关的配置
    discovery:
    locator:
    enabled: false # 如果使用服务发现,这里设置为 true

    management:
    endpoints:
    web:
    exposure:
    include: health,info,route,filters,metrics # 暴露路由相关端点

    三、核心概念与设计思路 🧠

    3.1 动态路由的工作原理 🔄

    在 Spring Cloud Gateway 中,路由规则是由 RouteDefinition 对象组成的。RouteDefinition 包含以下关键字段:

    • id: 路由的唯一标识符。
    • uri: 目标服务的 URI(如 lb://user-service)。
    • predicates: 路由匹配规则列表(如路径匹配、请求头匹配等)。
    • filters: 路由过滤器列表(如重试、限流、添加请求头等)。
    • order: 路由优先级,数值越小优先级越高。

    要实现动态路由,我们需要:

  • 存储路由定义:将路由规则保存在数据库中。
  • 读取路由定义:在应用启动或需要时,从数据库中读取所有有效的路由规则。
  • 构建 RouteDefinition:将数据库中的路由数据转换为 Spring Cloud Gateway 可识别的 RouteDefinition 对象。
  • 更新 Gateway 路由:通过 RouteDefinitionLocator 或者 RouteRefreshListener 等机制,通知 Gateway 更新其内部路由表。
  • 提供 Web UI:允许用户通过图形界面管理路由规则。
  • 3.2 数据模型设计 🧠

    3.2.1 RouteDefinition 实体类 ✅

    // src/main/java/com/example/dynamicgateway/model/RouteDefinition.java

    package com.example.dynamicgateway.model;

    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.AllArgsConstructor;
    import javax.persistence.*;
    import java.time.LocalDateTime;

    /**
    * 路由定义实体类
    */

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @Table(name = "route_definition")
    public class RouteDefinition {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
    * 路由 ID,必须唯一
    */

    @Column(name = "route_id", nullable = false, unique = true)
    private String routeId;

    /**
    * 路由名称(可选)
    */

    @Column(name = "route_name")
    private String routeName;

    /**
    * 目标 URI
    */

    @Column(name = "uri", nullable = false)
    private String uri;

    /**
    * 路由谓词 (JSON 格式字符串)
    */

    @Column(name = "predicates", columnDefinition = "TEXT", nullable = false)
    private String predicates; // 例如: [{"name":"Path","args":{"pattern":"/api/user/**"}}]

    /**
    * 路由过滤器 (JSON 格式字符串)
    */

    @Column(name = "filters", columnDefinition = "TEXT")
    private String filters; // 例如: [{"name":"Retry","args":{"retries":3,"statuses":"BAD_GATEWAY"}}]

    /**
    * 路由顺序 (优先级)
    */

    @Column(name = "order_num", nullable = false, columnDefinition = "INT DEFAULT 0")
    private Integer orderNum = 0;

    /**
    * 路由状态 (1: 启用, 0: 禁用)
    */

    @Column(name = "status", nullable = false, columnDefinition = "TINYINT DEFAULT 1")
    private Integer status = 1; // 1: 启用, 0: 禁用

    /**
    * 创建时间
    */

    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    /**
    * 更新时间
    */

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // 构造函数、getter、setter 等由 Lombok 自动生成
    }

    3.2.2 数据访问接口 ✅

    // src/main/java/com/example/dynamicgateway/repository/RouteRepository.java

    package com.example.dynamicgateway.repository;

    import com.example.dynamicgateway.model.RouteDefinition;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;

    import java.util.List;

    /**
    * 路由定义 Repository
    */

    @Repository
    public interface RouteRepository extends JpaRepository<RouteDefinition, Long> {

    /**
    * 根据路由 ID 查询路由
    */

    RouteDefinition findByRouteId(String routeId);

    /**
    * 查询所有启用的路由
    */

    List<RouteDefinition> findByStatus(Integer status);

    /**
    * 根据状态查询路由
    */

    List<RouteDefinition> findAllByStatus(Integer status);
    }

    3.3 路由转换工具类 ✅

    为了方便地将数据库中的 JSON 字符串转换为 Spring Cloud Gateway 所需的 RouteDefinition 对象,我们需要一个工具类。

    // src/main/java/com/example/dynamicgateway/util/RouteUtil.java

    package com.example.dynamicgateway.util;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
    import org.springframework.cloud.gateway.route.RouteDefinition;
    import org.springframework.cloud.gateway.filter.FilterDefinition;
    import org.springframework.stereotype.Component;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * 路由工具类
    */

    @Component
    public class RouteUtil {

    private final ObjectMapper objectMapper;

    public RouteUtil(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
    }

    /**
    * 将数据库中的 RouteDefinition 转换为 Spring Cloud Gateway 的 RouteDefinition
    *
    * @param dbRouteDefinition 数据库中的路由定义
    * @return Spring Cloud Gateway 的 RouteDefinition 对象
    */

    public RouteDefinition convertToGatewayRoute(com.example.dynamicgateway.model.RouteDefinition dbRouteDefinition) {
    if (dbRouteDefinition == null) {
    return null;
    }

    RouteDefinition gatewayRouteDefinition = new RouteDefinition();
    gatewayRouteDefinition.setId(dbRouteDefinition.getRouteId());
    gatewayRouteDefinition.setUri(dbRouteDefinition.getUri());
    gatewayRouteDefinition.setOrder(dbRouteDefinition.getOrderNum());

    // 解析 predicates
    try {
    if (dbRouteDefinition.getPredicates() != null && !dbRouteDefinition.getPredicates().isEmpty()) {
    TypeFactory typeFactory = objectMapper.getTypeFactory();
    List<PredicateDefinition> predicateDefinitions = objectMapper.readValue(
    dbRouteDefinition.getPredicates(), typeFactory.constructCollectionType(List.class, PredicateDefinition.class));
    gatewayRouteDefinition.setPredicates(predicateDefinitions);
    }
    } catch (Exception e) {
    // 处理解析错误
    throw new RuntimeException("Failed to parse predicates for route ID: " + dbRouteDefinition.getRouteId(), e);
    }

    // 解析 filters
    try {
    if (dbRouteDefinition.getFilters() != null && !dbRouteDefinition.getFilters().isEmpty()) {
    TypeFactory typeFactory = objectMapper.getTypeFactory();
    List<FilterDefinition> filterDefinitions = objectMapper.readValue(
    dbRouteDefinition.getFilters(), typeFactory.constructCollectionType(List.class, FilterDefinition.class));
    gatewayRouteDefinition.setFilters(filterDefinitions);
    }
    } catch (Exception e) {
    // 处理解析错误
    throw new RuntimeException("Failed to parse filters for route ID: " + dbRouteDefinition.getRouteId(), e);
    }

    return gatewayRouteDefinition;
    }

    /**
    * 将 Spring Cloud Gateway 的 RouteDefinition 转换为数据库中的 RouteDefinition
    *
    * @param gatewayRouteDefinition Spring Cloud Gateway 的路由定义
    * @return 数据库中的路由定义
    */

    public com.example.dynamicgateway.model.RouteDefinition convertToDbRoute(RouteDefinition gatewayRouteDefinition) {
    if (gatewayRouteDefinition == null) {
    return null;
    }

    com.example.dynamicgateway.model.RouteDefinition dbRouteDefinition =
    new com.example.dynamicgateway.model.RouteDefinition();

    dbRouteDefinition.setRouteId(gatewayRouteDefinition.getId());
    dbRouteDefinition.setUri(gatewayRouteDefinition.getUri().toString());
    dbRouteDefinition.setOrderNum(gatewayRouteDefinition.getOrder());

    // 序列化 predicates
    try {
    if (gatewayRouteDefinition.getPredicates() != null && !gatewayRouteDefinition.getPredicates().isEmpty()) {
    dbRouteDefinition.setPredicates(objectMapper.writeValueAsString(gatewayRouteDefinition.getPredicates()));
    }
    } catch (Exception e) {
    throw new RuntimeException("Failed to serialize predicates for route ID: " + gatewayRouteDefinition.getId(), e);
    }

    // 序列化 filters
    try {
    if (gatewayRouteDefinition.getFilters() != null && !gatewayRouteDefinition.getFilters().isEmpty()) {
    dbRouteDefinition.setFilters(objectMapper.writeValueAsString(gatewayRouteDefinition.getFilters()));
    }
    } catch (Exception e) {
    throw new RuntimeException("Failed to serialize filters for route ID: " + gatewayRouteDefinition.getId(), e);
    }

    // 默认启用状态
    dbRouteDefinition.setStatus(1);

    return dbRouteDefinition;
    }
    }

    🧠 说明:

    • convertToGatewayRoute 方法将数据库中的 RouteDefinition 转换为 Spring Cloud Gateway 的 RouteDefinition 对象,这是构建动态路由的关键步骤。
    • convertToDbRoute 方法则相反,用于将 Gateway 的 RouteDefinition 转换为数据库模型,主要用于新增或更新时的反序列化。

    3.4 动态路由配置类 ✅

    这是实现动态路由的核心部分。我们需要创建一个类来定期从数据库加载路由规则,并将其应用到 Gateway 中。

    // src/main/java/com/example/dynamicgateway/config/GatewayConfig.java

    package com.example.dynamicgateway.config;

    import com.example.dynamicgateway.model.RouteDefinition;
    import com.example.dynamicgateway.repository.RouteRepository;
    import com.example.dynamicgateway.util.RouteUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
    import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
    import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.ApplicationEventPublisherAware;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
    import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
    import reactor.core.publisher.Flux;

    import javax.annotation.PostConstruct;
    import java.util.ArrayList;
    import java.util.List;

    /**
    * 动态路由配置类
    */

    @Configuration
    public class GatewayConfig implements ApplicationEventPublisherAware {

    @Autowired
    private RouteRepository routeRepository;

    @Autowired
    private RouteUtil routeUtil;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    @Autowired
    private RouteDefinitionLocator routeDefinitionLocator;

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
    this.publisher = publisher;
    }

    /**
    * 初始化时加载所有启用的路由规则
    */

    @PostConstruct
    public void initRoutes() {
    loadAllRoutes();
    }

    /**
    * 加载所有启用的路由规则并更新 Gateway
    */

    public void loadAllRoutes() {
    // 从数据库查询所有启用的路由
    List<RouteDefinition> enabledRoutes = routeRepository.findByStatus(1); // 1 表示启用

    List<org.springframework.cloud.gateway.route.RouteDefinition> gatewayRoutes = new ArrayList<>();
    for (RouteDefinition dbRoute : enabledRoutes) {
    try {
    org.springframework.cloud.gateway.route.RouteDefinition gatewayRoute =
    routeUtil.convertToGatewayRoute(dbRoute);
    if (gatewayRoute != null) {
    gatewayRoutes.add(gatewayRoute);
    }
    } catch (Exception e) {
    // 记录日志,跳过有问题的路由
    System.err.println("Error converting route ID: " + dbRoute.getRouteId() + ", Error: " + e.getMessage());
    }
    }

    // 清除现有的路由
    // 注意:这是一个非常基础的实现,实际项目中可能需要更精细的处理
    // 例如,只更新变化的路由,而不是清空所有
    // 这里为了演示简单,先清空所有再重新加载
    // 但在实际应用中,应该使用更高效的更新方式
    // 此处省略了完全清除的代码,因为直接使用 routeDefinitionWriter.update() 更合适
    // 为了简化,我们可以先移除所有,然后添加新路由
    // 但这会导致短暂的路由中断,应谨慎使用

    // 更好的方式是:使用 RouteDefinitionWriter 更新或添加
    // 由于 Gateway 的 RouteDefinitionLocator 是只读的,我们需要通过 RouteDefinitionWriter 来动态添加/更新
    // 但通常的做法是使用自定义的 RouteDefinitionLocator 实现
    // 这里我们采用一种更直接的思路:在需要时,通过 Publisher 发布事件刷新路由

    // 简化版:直接发布刷新事件 (注意:这可能会引发重复加载等问题)
    // publisher.publishEvent(new RefreshRoutesEvent(this));

    // 更精确的处理方式:逐个更新或添加
    // 为了演示,我们先简单地重新加载所有路由
    refreshRoutes(gatewayRoutes);
    }

    /**
    * 刷新路由
    * @param gatewayRoutes 新的路由列表
    */

    private void refreshRoutes(List<org.springframework.cloud.gateway.route.RouteDefinition> gatewayRoutes) {
    // 这里只是示例,实际实现可能更复杂
    // 通常需要先删除旧路由,再添加新路由
    // 或者通过自定义的 RouteDefinitionLocator 实现
    // 本例中,我们通过发布 RefreshRoutesEvent 来触发刷新
    publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    /**
    * 通过 RouteDefinitionWriter 动态添加路由
    * @param routeDefinition 路由定义
    */

    public void addRoute(org.springframework.cloud.gateway.route.RouteDefinition routeDefinition) {
    routeDefinitionWriter.save(Flux.just(routeDefinition)).subscribe();
    // 发布刷新事件,通知 Gateway 更新路由
    publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    /**
    * 通过 RouteDefinitionWriter 动态更新路由
    * @param routeDefinition 路由定义
    */

    public void updateRoute(org.springframework.cloud.gateway.route.RouteDefinition routeDefinition) {
    // 注意:Gateway 的 RouteDefinitionWriter 通常需要通过 routeId 删除再添加
    // 这里简化处理,实际项目中需要先删除再添加
    // 为了演示,直接刷新
    publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    /**
    * 通过 RouteDefinitionWriter 动态删除路由
    * @param routeId 路由 ID
    */

    public void deleteRoute(String routeId) {
    // 通过 RouteDefinitionWriter 删除路由
    // 注意:需要实现具体的删除逻辑,这里只是示意
    // publisher.publishEvent(new RefreshRoutesEvent(this)); // 刷新路由
    }

    /**
    * 获取所有路由定义
    * @return 路由定义列表
    */

    public Flux<org.springframework.cloud.gateway.route.RouteDefinition> getAllRoutes() {
    return routeDefinitionLocator.getRouteDefinitions();
    }
    }

    🧠 说明:

    • @PostConstruct 注解的 initRoutes 方法在应用启动时调用,加载所有启用的路由。
    • loadAllRoutes 方法是核心逻辑,它从数据库加载所有启用的路由,并将其转换为 Gateway 可识别的对象。
    • refreshRoutes 方法用于刷新路由表。在实际应用中,可能需要更精确地判断哪些路由发生了变化,只更新变化的部分。
    • addRoute, updateRoute, deleteRoute 方法展示了如何动态操作路由,但实际实现可能需要更复杂的逻辑来确保一致性。
    • getAllRoutes 方法用于获取当前 Gateway 中的所有路由定义(通过 RouteDefinitionLocator)。

    3.5 路由业务逻辑服务 ✅

    为了更好地封装业务逻辑,我们创建一个服务类来处理路由相关的业务。

    // src/main/java/com/example/dynamicgateway/service/RouteService.java

    package com.example.dynamicgateway.service;

    import com.example.dynamicgateway.model.RouteDefinition;
    import com.example.dynamicgateway.repository.RouteRepository;
    import com.example.dynamicgateway.util.RouteUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;
    import java.util.Optional;

    /**
    * 路由服务类
    */

    @Service
    @Transactional
    public class RouteService {

    @Autowired
    private RouteRepository routeRepository;

    @Autowired
    private RouteUtil routeUtil;

    /**
    * 获取所有路由
    * @return 路由列表
    */

    public List<RouteDefinition> getAllRoutes() {
    return routeRepository.findAll();
    }

    /**
    * 获取启用的路由
    * @return 启用的路由列表
    */

    public List<RouteDefinition> getEnabledRoutes() {
    return routeRepository.findByStatus(1);
    }

    /**
    * 根据 ID 获取路由
    * @param id 路由 ID
    * @return 路由定义
    */

    public Optional<RouteDefinition> getRouteById(Long id) {
    return routeRepository.findById(id);
    }

    /**
    * 根据 routeId 获取路由
    * @param routeId 路由 ID
    * @return 路由定义
    */

    public Optional<RouteDefinition> getRouteByRouteId(String routeId) {
    return Optional.ofNullable(routeRepository.findByRouteId(routeId));
    }

    /**
    * 创建新路由
    * @param routeDefinition 路由定义
    * @return 创建后的路由定义
    */

    public RouteDefinition createRoute(RouteDefinition routeDefinition) {
    // 检查 routeId 是否已存在
    if (routeRepository.findByRouteId(routeDefinition.getRouteId()) != null) {
    throw new IllegalArgumentException("Route ID already exists: " + routeDefinition.getRouteId());
    }
    return routeRepository.save(routeDefinition);
    }

    /**
    * 更新路由
    * @param id 路由 ID
    * @param routeDefinition 更新后的路由定义
    * @return 更新后的路由定义
    */

    public RouteDefinition updateRoute(Long id, RouteDefinition routeDefinition) {
    RouteDefinition existingRoute = routeRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("Route not found with ID: " + id));

    // 更新字段
    existingRoute.setRouteName(routeDefinition.getRouteName());
    existingRoute.setUri(routeDefinition.getUri());
    existingRoute.setPredicates(routeDefinition.getPredicates());
    existingRoute.setFilters(routeDefinition.getFilters());
    existingRoute.setOrderNum(routeDefinition.getOrderNum());
    existingRoute.setStatus(routeDefinition.getStatus());

    return routeRepository.save(existingRoute);
    }

    /**
    * 删除路由
    * @param id 路由 ID
    */

    public void deleteRoute(Long id) {
    routeRepository.deleteById(id);
    }

    /**
    * 启用路由
    * @param id 路由 ID
    */

    public void enableRoute(Long id) {
    RouteDefinition route = routeRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("Route not found with ID: " + id));
    route.setStatus(1);
    routeRepository.save(route);
    }

    /**
    * 禁用路由
    * @param id 路由 ID
    */

    public void disableRoute(Long id) {
    RouteDefinition route = routeRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("Route not found with ID: " + id));
    route.setStatus(0);
    routeRepository.save(route);
    }
    }

    3.6 路由控制器 ✅

    控制器负责处理 Web 请求,与前端交互。

    // src/main/java/com/example/dynamicgateway/controller/RouteController.java

    package com.example.dynamicgateway.controller;

    import com.example.dynamicgateway.model.RouteDefinition;
    import com.example.dynamicgateway.service.RouteService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;
    import java.util.Optional;

    /**
    * 路由控制器
    */

    @RestController
    @RequestMapping("/api/routes")
    public class RouteController {

    @Autowired
    private RouteService routeService;

    /**
    * 获取所有路由
    * @return 路由列表
    */

    @GetMapping
    public ResponseEntity<List<RouteDefinition>> getAllRoutes() {
    List<RouteDefinition> routes = routeService.getAllRoutes();
    return ResponseEntity.ok(routes);
    }

    /**
    * 获取启用的路由
    * @return 启用的路由列表
    */

    @GetMapping("/enabled")
    public ResponseEntity<List<RouteDefinition>> getEnabledRoutes() {
    List<RouteDefinition> routes = routeService.getEnabledRoutes();
    return ResponseEntity.ok(routes);
    }

    /**
    * 根据 ID 获取路由
    * @param id 路由 ID
    * @return 路由定义
    */

    @GetMapping("/{id}")
    public ResponseEntity<RouteDefinition> getRouteById(@PathVariable Long id) {
    Optional<RouteDefinition> route = routeService.getRouteById(id);
    return route.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
    }

    /**
    * 创建路由
    * @param routeDefinition 路由定义
    * @return 创建成功的路由定义
    */

    @PostMapping
    public ResponseEntity<RouteDefinition> createRoute(@RequestBody RouteDefinition routeDefinition) {
    try {
    RouteDefinition createdRoute = routeService.createRoute(routeDefinition);
    return ResponseEntity.status(HttpStatus.CREATED).body(createdRoute);
    } catch (IllegalArgumentException e) {
    return ResponseEntity.badRequest().build(); // 返回 400 Bad Request
    }
    }

    /**
    * 更新路由
    * @param id 路由 ID
    * @param routeDefinition 更新后的路由定义
    * @return 更新后的路由定义
    */

    @PutMapping("/{id}")
    public ResponseEntity<RouteDefinition> updateRoute(@PathVariable Long id, @RequestBody RouteDefinition routeDefinition) {
    try {
    RouteDefinition updatedRoute = routeService.updateRoute(id, routeDefinition);
    return ResponseEntity.ok(updatedRoute);
    } catch (IllegalArgumentException e) {
    return ResponseEntity.notFound().build(); // 返回 404 Not Found
    }
    }

    /**
    * 删除路由
    * @param id 路由 ID
    * @return 删除状态
    */

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteRoute(@PathVariable Long id) {
    routeService.deleteRoute(id);
    return ResponseEntity.noContent().build();
    }

    /**
    * 启用路由
    * @param id 路由 ID
    * @return 启用状态
    */

    @PatchMapping("/{id}/enable")
    public ResponseEntity<Void> enableRoute(@PathVariable Long id) {
    routeService.enableRoute(id);
    return ResponseEntity.noContent().build();
    }

    /**
    * 禁用路由
    * @param id 路由 ID
    * @return 禁用状态
    */

    @PatchMapping("/{id}/disable")
    public ResponseEntity<Void> disableRoute(@PathVariable Long id) {
    routeService.disableRoute(id);
    return ResponseEntity.noContent().build();
    }
    }

    3.7 Mermaid 图解动态路由工作流程 🔄

    渲染错误: Mermaid 渲染失败: Parse error on line 6: … E –> F[Database (MySQL)] F –> E ———————–^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

    🧠 说明:

    • 用户请求 发送到 Gateway。
    • Gateway 接收到请求后,通过 RouteController 处理。
    • RouteController 调用 RouteService。
    • RouteService 与 RouteRepository 交互。
    • RouteRepository 与 Database (MySQL) 进行数据交互。
    • RouteService 处理完业务逻辑后,返回结果给 RouteController。
    • RouteController 将结果返回给 Gateway。
    • Gateway 根据 Dynamic Routes (Memory/Cache) 中的路由规则,将请求转发给 Target Service。
    • UI (Web Page) 提供用户界面,允许用户输入 User Input 来管理路由。
    • User Input 通过 UI 发送给 RouteController,从而更新数据库中的路由规则。

    四、Web UI 开发 ✅

    4.1 前端页面设计 ✅

    我们创建一个简单的 HTML 页面来作为前端 UI。

    <!– src/main/resources/static/index.html –>

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态路由管理平台</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <style>
    body {
    padding-top: 20px;
    background-color: #f8f9fa;
    }
    .card {
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    margin-bottom: 1rem;
    }
    .btn-primary {
    background-color: #0d6efd;
    border-color: #0d6efd;
    }
    .btn-success {
    background-color: #198754;
    border-color: #198754;
    }
    .btn-danger {
    background-color: #dc3545;
    border-color: #dc3545;
    }
    .btn-warning {
    background-color: #ffc107;
    border-color: #ffc107;
    }
    .table th {
    border-top: none;
    }
    .form-control:focus {
    border-color: #86b7fe;
    box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
    }
    </style>
    </head>
    <body>
    <div class="container">
    <h1 class="mb-4">动态路由管理平台 🔄🌐</h1>

    <!– 操作按钮区域 –>
    <div class="row mb-4">
    <div class="col-md-12 text-end">
    <button class="btn btn-primary me-2" onclick="showAddForm()">添加路由</button>
    <button class="btn btn-secondary" onclick="loadRoutes()">刷新列表</button>
    </div>
    </div>

    <!– 路由列表 –>
    <div class="card">
    <div class="card-header">
    <h5 class="mb-0">路由列表</h5>
    </div>
    <div class="card-body">
    <div id="loadingSpinner" class="text-center d-none">
    <div class="spinner-border text-primary" role="status">
    <span class="visually-hidden">Loading…</span>
    </div>
    <p class="mt-2">加载中…</p>
    </div>
    <table id="routesTable" class="table table-striped table-hover d-none">
    <thead>
    <tr>
    <th>ID</th>
    <th>路由 ID</th>
    <th>路由名称</th>
    <th>URI</th>
    <th>状态</th>
    <th>操作</th>
    </tr>
    </thead>
    <tbody id="routesTableBody">
    </tbody>
    </table>
    <div id="noRoutesMessage" class="text-center d-none">
    <p class="text-muted">暂无路由数据</p>
    </div>
    </div>
    </div>

    <!– 添加/编辑路由表单 –>
    <div id="addEditFormContainer" class="card d-none">
    <div class="card-header">
    <h5 class="mb-0" id="formTitle">添加路由</h5>
    </div>
    <div class="card-body">
    <form id="routeForm">
    <input type="hidden" id="editId">
    <div class="mb-3">
    <label for="routeId" class="form-label">路由 ID *</label>
    <input type="text" class="form-control" id="routeId" required>
    <div class="form-text">唯一标识符,不可重复。</div>
    </div>
    <div class="mb-3">
    <label for="routeName" class="form-label">路由名称</label>
    <input type="text" class="form-control" id="routeName">
    </div>
    <div class="mb-3">
    <label for="uri" class="form-label">URI *</label>
    <input type="text" class="form-control" id="uri" required>
    <div class="form-text">例如:lb://user-service</div>
    </div>
    <div class="mb-3">
    <label for="predicates" class="form-label">谓词 (JSON格式)*</label>
    <textarea class="form-control" id="predicates" rows="3" required></textarea>
    <div class="form-text">例如:[{"name":"Path","args":{"pattern":"/api/user/**"}}]</div>
    </div>
    <div class="mb-3">
    <label for="filters" class="form-label">过滤器 (JSON格式)</label>
    <textarea class="form-control" id="filters" rows="3"></textarea>
    <div class="form-text">例如:[{"name":"Retry","args":{"retries":3,"statuses":"BAD_GATEWAY"}}]</div>
    </div>
    <div class="mb-3">
    <label for="orderNum" class="form-label">排序 (数字越小优先级越高)</label>
    <input type="number" class="form-control" id="orderNum" value="0">
    </div>
    <div class="mb-3">
    <label for="status" class="form-label">状态</label>
    <select class="form-select" id="status">
    <option value="1">启用</option>
    <option value="0">禁用</option>
    </select>
    </div>
    <button type="submit" class="btn btn-success">提交</button>
    <button type="button" class="btn btn-secondary" onclick="hideForm()">取消</button>
    </form>
    </div>
    </div>
    </div>

    <script>
    const API_BASE_URL = 'http://localhost:8080/api/routes'; // 根据实际情况修改

    document.addEventListener('DOMContentLoaded', function () {
    loadRoutes();
    });

    // 加载路由列表
    async function loadRoutes() {
    showLoading(true);
    try {
    const response = await axios.get(API_BASE_URL);
    const routes = response.data;
    renderRoutes(routes);
    } catch (error) {
    console.error('Error loading routes:', error);
    alert('加载路由失败');
    } finally {
    showLoading(false);
    }
    }

    // 渲染路由列表
    function renderRoutes(routes) {
    const tbody = document.getElementById('routesTableBody');
    const noRoutesMessage = document.getElementById('noRoutesMessage');
    const routesTable = document.getElementById('routesTable');

    tbody.innerHTML = '';

    if (routes.length === 0) {
    noRoutesMessage.classList.remove('d-none');
    routesTable.classList.add('d-none');
    return;
    }

    noRoutesMessage.classList.add('d-none');
    routesTable.classList.remove('d-none');

    routes.forEach(route => {
    const row = document.createElement('tr');
    row.innerHTML = `
    <td>
    ${route.id}</td>
    <td>
    ${route.routeId}</td>
    <td>
    ${route.routeName || '-'}</td>
    <td>
    ${route.uri}</td>
    <td>
    ${route.status === 1 ? '<span class="badge bg-success">启用</span>' : '<span class="badge bg-secondary">禁用</span>'}</td>
    <td>
    <button class="btn btn-sm btn-warning me-1" onclick="editRoute(
    ${route.id})">编辑</button>
    ${route.status === 1 ?
    `<button class="btn btn-sm btn-danger me-1" onclick="disableRoute(${route.id})">禁用</button>` :
    `<button class="btn btn-sm btn-success me-1" onclick="enableRoute(${route.id})">启用</button>`
    }

    <button class="btn btn-sm btn-danger" onclick="deleteRoute(
    ${route.id})">删除</button>
    </td>
    `
    ;
    tbody.appendChild(row);
    });
    }

    // 显示加载动画
    function showLoading(show) {
    const spinner = document.getElementById('loadingSpinner');
    const table = document.getElementById('routesTable');
    const message = document.getElementById('noRoutesMessage');

    if (show) {
    spinner.classList.remove('d-none');
    table.classList.add('d-none');
    message.classList.add('d-none');
    } else {
    spinner.classList.add('d-none');
    }
    }

    // 显示添加表单
    function showAddForm() {
    document.getElementById('formTitle').textContent = '添加路由';
    document.getElementById('editId').value = '';
    document.getElementById('routeForm').reset();
    document.getElementById('addEditFormContainer').classList.remove('d-none');
    }

    // 隐藏表单
    function hideForm() {
    document.getElementById('addEditFormContainer').classList.add('d-none');
    }

    // 编辑路由
    async function editRoute(id) {
    try {
    const response = await axios.get(`${API_BASE_URL}/${id}`);
    const route = response.data;
    document.getElementById('formTitle').textContent = '编辑路由';
    document.getElementById('editId').value = route.id;
    document.getElementById('routeId').value = route.routeId;
    document.getElementById('routeName').value = route.routeName || '';
    document.getElementById('uri').value = route.uri;
    document.getElementById('predicates').value = route.predicates;
    document.getElementById('filters').value = route.filters || '';
    document.getElementById('orderNum').value = route.orderNum;
    document.getElementById('status').value = route.status;
    document.getElementById('addEditFormContainer').classList.remove('d-none');
    } catch (error) {
    console.error('Error fetching route for edit:', error);
    alert('获取路由信息失败');
    }
    }

    // 启用路由
    async function enableRoute(id) {
    if (!confirm('确定要启用此路由吗?')) return;
    try {
    await axios.patch(`${API_BASE_URL}/${id}/enable`);
    loadRoutes(); // 重新加载列表
    alert('路由已启用');
    } catch (error) {
    console.error('Error enabling route:', error);
    alert('启用路由失败');
    }
    }

    // 禁用路由
    async function disableRoute(id) {
    if (!confirm('确定要禁用此路由吗?')) return;
    try {
    await axios.patch(`${API_BASE_URL}/${id}/disable`);
    loadRoutes(); // 重新加载列表
    alert('路由已禁用');
    } catch (error) {
    console.error('Error disabling route:', error);
    alert('禁用路由失败');
    }
    }

    // 删除路由
    async function deleteRoute(id) {
    if (!confirm('确定要删除此路由吗?此操作不可撤销。')) return;
    try {
    await axios.delete(`${API_BASE_URL}/${id}`);
    loadRoutes(); // 重新加载列表
    alert('路由已删除');
    } catch (error) {
    console.error('Error deleting route:', error);
    alert('删除路由失败');
    }
    }

    // 处理表单提交
    document.getElementById('routeForm').addEventListener('submit', async function (e) {
    e.preventDefault();

    const formData = new FormData(this);
    const routeData = {
    id: formData.get('editId') ? parseInt(formData.get('editId')) : undefined,
    routeId: formData.get('routeId'),
    routeName: formData.get('routeName'),
    uri: formData.get('uri'),
    predicates: formData.get('predicates'),
    filters: formData.get('filters'),
    orderNum: parseInt(formData.get('orderNum')) || 0,
    status: parseInt(formData.get('status'))
    };

    try {
    let response;
    if (routeData.id) {
    // 更新路由
    response = await axios.put(`${API_BASE_URL}/${routeData.id}`, routeData);
    alert('路由已更新');
    } else {
    // 创建路由
    response = await axios.post(`${API_BASE_URL}`, routeData);
    alert('路由已创建');
    }
    hideForm();
    loadRoutes(); // 重新加载列表
    } catch (error) {
    console.error('Error saving route:', error);
    alert('保存路由失败');
    }
    });
    </script>
    </body>
    </html>

    4.2 启动与访问 ✅

  • 启动数据库:确保 MySQL 服务已启动,并且 dynamic_gateway 数据库已创建。
  • 启动应用:使用 Maven 或 IDE 启动 DynamicGatewayApplication。
  • 访问 UI:在浏览器中打开 http://localhost:8080。你应该能看到一个简单的 Web UI,可以用来管理路由。
  • 五、高级功能与优化 ✅

    5.1 路由状态实时同步 ✅

    为了让用户能实时看到路由状态的变化(如启用/禁用),可以考虑使用 WebSocket 或 Server-Sent Events (SSE) 来推送状态变更。

    5.1.1 简单的 SSE 方案 ✅

    在后端添加一个 SSE 端点,用于推送路由状态更新。

    // src/main/java/com/example/dynamicgateway/controller/SseController.java

    package com.example.dynamicgateway.controller;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

    import java.io.IOException;
    import java.util.concurrent.CopyOnWriteArraySet;

    /**
    * Server-Sent Events Controller
    */

    @RestController
    public class SseController {

    // 存储所有 SSE 连接
    private static final CopyOnWriteArraySet<SseEmitter> emitters = new CopyOnWriteArraySet<>();

    @GetMapping("/sse/updates")
    public SseEmitter handleSseUpdates() {
    SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 保持连接长时间有效
    emitters.add(emitter);

    emitter.onTimeout(() -> emitters.remove(emitter));
    emitter.onCompletion(() -> emitters.remove(emitter));

    return emitter;
    }

    /**
    * 发送路由更新事件
    */

    public static void sendRouteUpdate(String event) {
    for (SseEmitter emitter : emitters) {
    try {
    emitter.send(SseEmitter.event().name("route-update").data(event));
    } catch (IOException e) {
    emitters.remove(emitter);
    }
    }
    }
    }

    5.1.2 前端监听 SSE ✅

    在前端 JavaScript 中添加监听逻辑:

    // 在页面加载完成后,连接 SSE
    const sseSource = new EventSource('http://localhost:8080/sse/updates');

    sseSource.addEventListener('route-update', function(event) {
    console.log('Received route update:', event.data);
    // 根据 event.data 内容,更新前端 UI
    loadRoutes(); // 重新加载路由列表
    });

    // 注意:需要在页面关闭或离开时关闭连接
    window.addEventListener('beforeunload', function() {
    sseSource.close();
    });

    5.2 路由导入导出 ✅

    为了方便批量操作,可以添加路由的导入和导出功能。

    5.2.1 导出功能 ✅

    // 在 RouteController 中添加导出方法
    @GetMapping("/export")
    public ResponseEntity<byte[]> exportRoutes() {
    List<RouteDefinition> routes = routeService.getAllRoutes();
    // 将 routes 转换为 CSV 或 JSON 格式
    // 这里简化为返回 JSON
    try {
    ObjectMapper mapper = new ObjectMapper();
    String json = mapper.writeValueAsString(routes);
    byte[] content = json.getBytes();
    return ResponseEntity.ok()
    .header("Content-Disposition", "attachment; filename=\\"routes.json\\"")
    .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
    .body(content);
    } catch (Exception e) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
    }

    5.2.2 导入功能 ✅

    // 在 RouteController 中添加导入方法
    @PostMapping("/import")
    public ResponseEntity<Void> importRoutes(@RequestBody List<RouteDefinition> routes) {
    try {
    for (RouteDefinition route : routes) {
    // 需要处理重复路由 ID 的情况
    if (routeRepository.findByRouteId(route.getRouteId()) == null) {
    routeRepository.save(route);
    } else {
    // 可以选择更新或跳过
    // 这里简单跳过
    }
    }
    // 通知 Gateway 刷新路由
    // gatewayConfig.loadAllRoutes(); // 假设你有对 GatewayConfig 的引用
    return ResponseEntity.ok().build();
    } catch (Exception e) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
    }

    5.3 路由版本控制 ✅

    对于重要的路由变更,可以引入版本控制机制,记录每一次变更的历史。

    5.3.1 添加版本信息 ✅

    修改 RouteDefinition 实体类,添加版本字段:

    // 在 RouteDefinition.java 中添加
    @Column(name = "version", nullable = false, columnDefinition = "INT DEFAULT 1")
    private Integer version = 1;

    // 以及对应的 getter 和 setter

    5.3.2 增加历史记录表 ✅

    — 创建路由历史记录表
    CREATE TABLE IF NOT EXISTS route_history (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    route_id VARCHAR(255) NOT NULL,
    action VARCHAR(50) NOT NULL, — CREATE, UPDATE, DELETE
    old_data TEXT, — JSON 字符串,存储变更前的数据
    new_data TEXT, — JSON 字符串,存储变更后的数据
    changed_by VARCHAR(255),
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    5.3.3 在 Service 层记录变更 ✅

    // 在 RouteService.java 中修改相关方法
    public RouteDefinition updateRoute(Long id, RouteDefinition routeDefinition) {
    RouteDefinition existingRoute = routeRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("Route not found with ID: " + id));

    // 记录变更前的数据
    ObjectMapper objectMapper = new ObjectMapper();
    String oldDataJson = "";
    try {
    oldDataJson = objectMapper.writeValueAsString(existingRoute);
    } catch (Exception e) {
    // 记录日志
    }

    // 更新字段
    existingRoute.setRouteName(routeDefinition.getRouteName());
    existingRoute.setUri(routeDefinition.getUri());
    existingRoute.setPredicates(routeDefinition.getPredicates());
    existingRoute.setFilters(routeDefinition.getFilters());
    existingRoute.setOrderNum(routeDefinition.getOrderNum());
    existingRoute.setStatus(routeDefinition.getStatus());
    existingRoute.setVersion(existingRoute.getVersion() + 1); // 版本号递增

    RouteDefinition updatedRoute = routeRepository.save(existingRoute);

    // 记录变更后的数据
    String newDataJson = "";
    try {
    newDataJson = objectMapper.writeValueAsString(updatedRoute);
    } catch (Exception e) {
    // 记录日志
    }

    // 保存历史记录 (需要注入 RouteHistoryRepository)
    // routeHistoryRepository.save(new RouteHistory(existingRoute.getRouteId(), "UPDATE", oldDataJson, newDataJson, "admin"));

    return updatedRoute;
    }

    5.4 权限控制 ✅

    在生产环境中,需要对路由管理进行权限控制。可以使用 Spring Security 来实现。

    5.4.1 添加 Spring Security 依赖 ✅

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    5.4.2 配置安全策略 ✅

    // src/main/java/com/example/dynamicgateway/config/SecurityConfig.java

    package com.example.dynamicgateway.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/api/routes/**").authenticated() // 需要认证
    .antMatchers("/static/**", "/").permitAll() // 允许访问静态资源和首页
    .and()
    .httpBasic() // 使用 Basic Auth
    .and()
    .csrf().disable(); // 禁用 CSRF (对于 API)
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    }

    5.4.3 配置用户 ✅

    # application.yml
    spring:
    security:
    user:
    name: admin
    password: $2a$10$examplePasswordHash # 使用 BCrypt 加密后的密码

    六、性能优化与最佳实践 ✅

    6.1 数据库优化 ✅

    • 索引优化:为 route_id, status, created_at 等常用查询字段添加索引。
    • 分页查询:当路由数量庞大时,使用分页查询来避免一次性加载过多数据。
    • 缓存策略:对于不频繁变动的路由信息,可以考虑在内存中缓存,减少数据库查询次数。

    6.2 Gateway 性能优化 ✅

    • 路由缓存:Gateway 本身会缓存路由规则,确保在大量请求下也能高效处理。
    • 合理的并发控制:避免在高并发情况下频繁地刷新路由,可以加入队列或锁机制。
    • 监控与告警:集成监控系统,实时监控路由更新频率和 Gateway 的性能指标。

    6.3 安全性 ✅

    • 输入校验:对用户输入的 JSON 字符串进行严格的校验,防止恶意输入。
    • 访问控制:确保只有授权用户才能访问和修改路由。
    • 审计日志:记录所有路由变更操作,便于事后追溯。

    6.4 可靠性 ✅

    • 事务管理:在更新路由时,确保数据库操作的原子性。
    • 错误处理:妥善处理各种异常情况,如数据库连接失败、JSON 解析错误等。
    • 健康检查:提供健康检查端点,监控应用和数据库的状态。

    七、常见问题与解决方案 🚨

    7.1 路由更新后 Gateway 未及时生效 ✅

    7.1.1 问题现象 ✅

    修改路由后,通过 Web UI 查看路由列表已更新,但实际请求 Gateway 时并未按新规则转发。

    7.1.2 常见原因与解决办法 ✅
    • 刷新机制问题:
      • 原因:RefreshRoutesEvent 事件可能未正确触发,或者 Gateway 的 RouteDefinitionLocator 未能及时感知变化。
      • 解决办法:
        • 检查 GatewayConfig 中的 loadAllRoutes() 方法,确保其正确调用了 publisher.publishEvent(new RefreshRoutesEvent(this))。
        • 确保 RouteDefinitionWriter 和 RouteDefinitionLocator 正确配置。
        • 可以尝试在 loadAllRoutes 中直接调用 routeDefinitionWriter.delete(…) 然后 save(…) 来强制更新,但这可能影响性能。
    • 数据库连接问题:
      • 原因:数据库连接池问题或事务未提交。
      • 解决办法:
        • 检查数据库连接配置。
        • 确保 @Transactional 注解正确应用。
        • 在服务方法中添加日志,确认数据确实被写入数据库。

    7.2 JSON 格式错误导致解析失败 ✅

    7.2.1 问题现象 ✅

    在添加或更新路由时,填写了错误的 JSON 格式(如缺少逗号、括号不匹配),导致解析失败,路由无法保存。

    7.2.2 常见原因与解决办法 ✅
    • 前端校验不足:
      • 原因:前端未对 JSON 输入进行语法校验。
      • 解决办法:
        • 在前端添加 JSON 格式校验逻辑,使用 JSON.parse() 尝试解析。
        • 提供在线 JSON 校验工具的链接或提示。
    • 后端校验不严:
      • 原因:后端未对 JSON 字符串进行严格校验。
      • 解决办法:
        • 在 RouteUtil 中增加 try-catch 块,并抛出自定义异常。
        • 提供详细的错误信息反馈给前端。

    7.3 前端 UI 响应缓慢 ✅

    7.3.1 问题现象 ✅

    在路由列表很多时,加载页面或执行操作(如删除、启用)响应缓慢。

    7.3.2 常见原因与解决办法 ✅
    • 前端渲染性能:
      • 原因:大量 DOM 操作或复杂计算。
      • 解决办法:
        • 使用虚拟滚动(Virtual Scrolling)技术,只渲染可见区域内的数据。
        • 优化 JavaScript 逻辑,减少不必要的重绘和回流。
    • 后端性能:
      • 原因:数据库查询慢或网络延迟。
      • 解决办法:
        • 添加数据库索引。
        • 实现分页查询。
        • 使用缓存(如 Redis)缓存热点数据。

    八、与现有系统的集成 ✅

    8.1 与服务注册中心集成 ✅

    如果使用了 Eureka、Consul 或 Nacos 等服务注册中心,可以动态地根据服务实例的变化来更新路由规则。

    8.1.1 自动发现服务 ✅

    // 在 RouteService 或 GatewayConfig 中
    @Autowired
    private DiscoveryClient discoveryClient;

    public List<String> getServiceInstances(String serviceName) {
    return discoveryClient.getInstances(serviceName).stream()
    .map(instance -> instance.getUri().toString())
    .collect(Collectors.toList());
    }

    8.1.2 动态构建路由 ✅

    // 根据服务名动态生成路由
    String serviceUri = "lb://" + serviceName;
    // 构建 predicates 和 filters

    8.2 与配置中心集成 ✅

    可以将路由规则存储在 Spring Cloud Config Server 或类似的配置中心中,实现配置的集中管理和动态刷新。

    8.3 与监控系统集成 ✅

    将路由管理平台与 Prometheus、Grafana 等监控系统集成,可以监控路由变更、访问量、成功率等指标。

    九、总结与展望 📝🚀

    9.1 关键要点回顾 ✅

  • 动态路由的核心:通过数据库存储路由规则,并在 Gateway 中动态加载这些规则。
  • Web UI 的作用:提供友好的用户界面,方便管理和操作路由规则。
  • 技术实现:利用 Spring Boot、Spring Cloud Gateway、JPA 和 MySQL 构建后端,使用 HTML/CSS/JS 构建前端。
  • 数据模型:设计合理的实体类和 Repository 来操作路由数据。
  • 路由转换:通过工具类将数据库模型转换为 Gateway 所需的 RouteDefinition 对象。
  • 安全性:考虑权限控制、输入校验和审计日志。
  • 9.2 未来发展趋势 ✅

    • 更智能的路由管理:结合机器学习算法,自动推荐最优路由策略。
    • 云原生集成:与 Kubernetes、Istio 等云原生平台深度集成,实现更复杂的流量治理。
    • 可视化编排:提供图形化界面,让用户可以拖拽方式构建复杂的路由规则。
    • 多租户支持:在企业级应用中,支持不同租户之间的路由隔离。

    通过本文的详细介绍,我们成功构建了一个基于 Spring Cloud Gateway 的动态路由管理平台。这个平台不仅提供了基本的路由 CRUD 操作,还展示了如何通过 Web UI 实现实时配置,极大地提升了微服务架构中路由管理的灵活性和效率。希望这篇文章能够为你在实际项目中的应用提供有价值的参考和指导。


    附录:相关资源链接 📚🔗

    • Spring Cloud Gateway 官方文档:https://cloud.spring.io/spring-cloud-gateway/reference/html/
    • Spring Data JPA 官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
    • MySQL 官方文档:https://dev.mysql.com/doc/
    • Bootstrap 官方文档:https://getbootstrap.com/docs/5.1/getting-started/introduction/
    • Axios 官方文档:https://axios-http.com/docs/intro

    本文档全面介绍了如何构建一个动态路由管理平台,从基础概念、技术选型、核心代码实现到高级功能和最佳实践,旨在帮助开发者快速上手并构建一个高效、易用的动态路由管理系统。通过这套方案,你可以轻松应对微服务架构中路由规则频繁变更的需求,为系统的稳定运行和快速迭代提供坚实保障。持续的优化和扩展是保证系统长期生命力的关键,让我们一起打造更加智能化、自动化的微服务治理平台!


    🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

    赞(0)
    未经允许不得转载:171主机测评 » Gateway - 动态路由管理平台:通过 Web UI 实时配置路由
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址