跳到主要内容
Spring Cloud Gateway 动态路由管理平台:Web UI 实时配置 | 极客日志
Java java
Spring Cloud Gateway 动态路由管理平台:Web UI 实时配置 介绍基于 Spring Cloud Gateway 构建的动态路由管理平台,通过 Web UI 实现路由规则的实时增删改查。核心方案包括使用 MySQL 存储路由定义,JPA 进行数据持久化,结合 RouteDefinitionWriter 动态刷新网关路由。涵盖环境搭建、实体设计、API 开发、前端交互及高级功能如版本控制、权限管理和性能优化,解决微服务架构中静态路由配置维护困难的问题。
Stephaine Walsh 发布于 2026/2/4 更新于 2026/5/27 24 浏览Spring Cloud 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 : 减少样板代码。前端 (可选,本例使用简单 HTML+JavaScript):
HTML/CSS/JavaScript : 构建简单的 Web UI。
Axios : 发起 HTTP 请求。
工具 :
Maven : 项目构建工具。
IDE (如 IntelliJ IDEA) : 开发环境。
2.2 项目结构 dynamic-gateway-management/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── dynamicgateway/
│ │ │ ├── DynamicGatewayApplication.java
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── model/
│ │ │ ├── repository/
│ │ │ ├── service/
│ │ │ └── util/
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── data.sql
│ │ └── static /
│ │ └── index.html
└── pom.xml
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 >
</parent >
<properties >
<java.version > 11</java.version >
<spring-cloud.version > 2021.0.3</spring-cloud.version >
</properties >
<dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-web</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-starter-gateway</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-data-jpa</artifactId >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<scope > runtime</scope >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
<optional > true</optional >
</dependency >
<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 ,
filters TEXT,
order_num INT DEFAULT 0 ,
status TINYINT DEFAULT 1 ,
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 应用配置 server:
port: 8080
spring:
application:
name: dynamic-gateway-management
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
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
cloud:
gateway:
discovery:
locator:
enabled: false
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 实体类
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;
@Column(name = "route_id", nullable = false, unique = true)
private String routeId;
@Column(name = "route_name")
private String routeName;
@Column(name = "uri", nullable = false)
private String uri;
@Column(name = "predicates", columnDefinition = "TEXT", nullable = false)
private String predicates;
@Column(name = "filters", columnDefinition = "TEXT")
private String filters;
@Column(name = "order_num", nullable = false, columnDefinition = "INT DEFAULT 0")
private Integer orderNum = 0 ;
@Column(name = "status", nullable = false, columnDefinition = "TINYINT DEFAULT 1")
private Integer status = 1 ;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
3.2.2 数据访问接口
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
public interface RouteRepository extends JpaRepository <RouteDefinition, Long> {
RouteDefinition findByRouteId (String routeId) ;
List<RouteDefinition> findByStatus (Integer status) ;
List<RouteDefinition> findAllByStatus (Integer status) ;
}
3.3 路由转换工具类
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;
}
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());
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);
}
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;
}
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());
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);
}
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;
}
}
3.4 动态路由配置类
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 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();
}
public void loadAllRoutes () {
List<RouteDefinition> enabledRoutes = routeRepository.findByStatus(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());
}
}
refreshRoutes(gatewayRoutes);
}
private void refreshRoutes (List<org.springframework.cloud.gateway.route.RouteDefinition> gatewayRoutes) {
publisher.publishEvent(new RefreshRoutesEvent (this ));
}
public void addRoute (org.springframework.cloud.gateway.route.RouteDefinition routeDefinition) {
routeDefinitionWriter.save(Flux.just(routeDefinition)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent (this ));
}
public Flux<org.springframework.cloud.gateway.route.RouteDefinition> getAllRoutes() {
return routeDefinitionLocator.getRouteDefinitions();
}
}
3.5 路由业务逻辑服务
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;
public List<RouteDefinition> getAllRoutes () {
return routeRepository.findAll();
}
public List<RouteDefinition> getEnabledRoutes () {
return routeRepository.findByStatus(1 );
}
public Optional<RouteDefinition> getRouteById (Long id) {
return routeRepository.findById(id);
}
public Optional<RouteDefinition> getRouteByRouteId (String routeId) {
return Optional.ofNullable(routeRepository.findByRouteId(routeId));
}
public RouteDefinition createRoute (RouteDefinition routeDefinition) {
if (routeRepository.findByRouteId(routeDefinition.getRouteId()) != null ) {
throw new IllegalArgumentException ("Route ID already exists: " + routeDefinition.getRouteId());
}
return routeRepository.save(routeDefinition);
}
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);
}
public void deleteRoute (Long id) {
routeRepository.deleteById(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);
}
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 路由控制器
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;
@GetMapping
public ResponseEntity<List<RouteDefinition>> getAllRoutes () {
List<RouteDefinition> routes = routeService.getAllRoutes();
return ResponseEntity.ok(routes);
}
@GetMapping("/enabled")
public ResponseEntity<List<RouteDefinition>> getEnabledRoutes () {
List<RouteDefinition> routes = routeService.getEnabledRoutes();
return ResponseEntity.ok(routes);
}
@GetMapping("/{id}")
public ResponseEntity<RouteDefinition> getRouteById (@PathVariable Long id) {
Optional<RouteDefinition> route = routeService.getRouteById(id);
return route.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
@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();
}
}
@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();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteRoute (@PathVariable Long id) {
routeService.deleteRoute(id);
return ResponseEntity.noContent().build();
}
@PatchMapping("/{id}/enable")
public ResponseEntity<Void> enableRoute (@PathVariable Long id) {
routeService.enableRoute(id);
return ResponseEntity.noContent().build();
}
@PatchMapping("/{id}/disable")
public ResponseEntity<Void> disableRoute (@PathVariable Long id) {
routeService.disableRoute(id);
return ResponseEntity.noContent().build();
}
}
3.7 动态路由工作流程说明 用户请求发送到 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 前端页面设计
<!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/[email protected] /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 ; }
.table th { border-top : none; }
</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 >
<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 >
<div class ="mb-3" >
<label for ="predicates" class ="form-label" > 谓词 (JSON 格式)*</label >
<textarea class ="form-control" id ="predicates" rows ="3" required > </textarea >
</div >
<div class ="mb-3" >
<label for ="filters" class ="form-label" > 过滤器 (JSON 格式)</label >
<textarea class ="form-control" id ="filters" rows ="3" > </textarea >
</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 ( ) {
try {
const response = await axios.get (API_BASE_URL );
renderRoutes (response.data );
} catch (error) {
console .error ('Error loading routes:' , error);
}
}
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. . ( );
routesTable. . ( );
routes. ( {
row = . ( );
row. = ;
tbody. (row);
});
}
( ) {
. ( ). = ;
. ( ). = ;
. ( ). ();
. ( ). . ( );
}
( ) {
. ( ). . ( );
}
( ) {
{
response = axios. ( );
route = response. ;
. ( ). = ;
. ( ). = route. ;
. ( ). = route. ;
. ( ). = route. || ;
. ( ). = route. ;
. ( ). = route. ;
. ( ). = route. || ;
. ( ). = route. ;
. ( ). = route. ;
. ( ). . ( );
} (error) {
. ( , error);
}
}
( ) {
(! ( )) ;
{
axios. ( );
();
} (error) {
. ( , error);
}
}
( ) {
(! ( )) ;
{
axios. ( );
();
} (error) {
. ( , error);
}
}
( ) {
(! ( )) ;
{
axios. ( );
();
} (error) {
. ( , error);
}
}
. ( ). ( , ( ) {
e. ();
formData = ( );
routeData = {
: formData. ( ) ? (formData. ( )) : ,
: formData. ( ),
: formData. ( ),
: formData. ( ),
: formData. ( ),
: formData. ( ),
: (formData. ( )) || ,
: (formData. ( ))
};
{
response;
(routeData. ) {
response = axios. ( , routeData);
} {
response = axios. ( , routeData);
}
();
();
} (error) {
. ( , error);
}
});
</script >
</body >
</html >
4.2 启动与访问
启动数据库 :确保 MySQL 服务已启动,并且 dynamic_gateway 数据库已创建。
启动应用 :使用 Maven 或 IDE 启动 DynamicGatewayApplication。
访问 UI :在浏览器中打开 http://localhost:8080。
五、高级功能与优化
5.1 路由状态实时同步 为了让用户能实时看到路由状态的变化,可以考虑使用 WebSocket 或 Server-Sent Events (SSE) 来推送状态变更。
5.1.1 SSE 方案
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;
@RestController
public class SseController {
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 const sseSource = new EventSource ('http://localhost:8080/sse/updates' );
sseSource.addEventListener ('route-update' , function (event ) {
console .log ('Received route update:' , event.data );
loadRoutes ();
});
window .addEventListener ('beforeunload' , function ( ) {
sseSource.close ();
});
5.2 路由导入导出
5.2.1 导出功能 @GetMapping("/export")
public ResponseEntity<byte []> exportRoutes() {
List<RouteDefinition> routes = routeService.getAllRoutes();
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 导入功能 @PostMapping("/import")
public ResponseEntity<Void> importRoutes (@RequestBody List<RouteDefinition> routes) {
try {
for (RouteDefinition route : routes) {
if (routeRepository.findByRouteId(route.getRouteId()) == null ) {
routeRepository.save(route);
}
}
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
5.3 路由版本控制 对于重要的路由变更,可以引入版本控制机制,记录每一次变更的历史。
5.3.1 添加版本信息 在 RouteDefinition 实体类中添加版本字段:
@Column(name = "version", nullable = false, columnDefinition = "INT DEFAULT 1")
private Integer version = 1 ;
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 ,
old_data TEXT,
new_data TEXT,
changed_by VARCHAR (255 ),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
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 配置安全策略 @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()
.and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder ();
}
}
六、性能优化与最佳实践
6.1 数据库优化
索引优化 :为 route_id, status, created_at 等常用查询字段添加索引。
分页查询 :当路由数量庞大时,使用分页查询来避免一次性加载过多数据。
缓存策略 :对于不频繁变动的路由信息,可以考虑在内存中缓存,减少数据库查询次数。
6.2 Gateway 性能优化
路由缓存 :Gateway 本身会缓存路由规则,确保在大量请求下也能高效处理。
合理的并发控制 :避免在高并发情况下频繁地刷新路由,可以加入队列或锁机制。
6.3 安全性
输入校验 :对用户输入的 JSON 字符串进行严格的校验,防止恶意输入。
访问控制 :确保只有授权用户才能访问和修改路由。
6.4 可靠性
事务管理 :在更新路由时,确保数据库操作的原子性。
错误处理 :妥善处理各种异常情况,如数据库连接失败、JSON 解析错误等。
七、常见问题与解决方案
7.1 路由更新后 Gateway 未及时生效
原因 :RefreshRoutesEvent 事件可能未正确触发,或者 Gateway 的 RouteDefinitionLocator 未能及时感知变化。
解决办法 :检查 GatewayConfig 中的 loadAllRoutes() 方法,确保其正确调用了 publisher.publishEvent(new RefreshRoutesEvent(this))。
7.2 JSON 格式错误导致解析失败
原因 :前端未对 JSON 输入进行语法校验,或后端未严格校验。
解决办法 :在前端添加 JSON 格式校验逻辑,在后端增加 try-catch 块并提供详细错误信息。
7.3 前端 UI 响应缓慢
原因 :大量 DOM 操作或数据库查询慢。
解决办法 :使用虚拟滚动技术,添加数据库索引,实现分页查询。
八、与现有系统的集成
8.1 与服务注册中心集成 如果使用了 Eureka、Consul 或 Nacos 等服务注册中心,可以动态地根据服务实例的变化来更新路由规则。
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 实现实时配置,极大地提升了微服务架构中路由管理的灵活性和效率。希望这篇文章能够为你在实际项目中的应用提供有价值的参考和指导。
classList
add
'd-none'
classList
remove
'd-none'
forEach
route =>
const
document
createElement
'tr'
innerHTML
`
<td>${route.id} </td>
<td>${route.routeId} </td>
<td>${route.routeName || '-' } </td>
<td>${route.uri} </td>
<td>${route.status === 1 ? '<span>启用</span>' : '<span>禁用</span>' } </td>
<td>
<button onclick="editRoute(${route.id} )">编辑</button>
${route.status === 1 ? `<button onclick="disableRoute(${route.id} )">禁用</button>` : `<button onclick="enableRoute(${route.id} )">启用</button>` }
<button onclick="deleteRoute(${route.id} )">删除</button>
</td>
`
appendChild
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
await
get
`${API_BASE_URL} /${id} `
const
data
document
getElementById
'formTitle'
textContent
'编辑路由'
document
getElementById
'editId'
value
id
document
getElementById
'routeId'
value
routeId
document
getElementById
'routeName'
value
routeName
''
document
getElementById
'uri'
value
uri
document
getElementById
'predicates'
value
predicates
document
getElementById
'filters'
value
filters
''
document
getElementById
'orderNum'
value
orderNum
document
getElementById
'status'
value
status
document
getElementById
'addEditFormContainer'
classList
remove
'd-none'
catch
console
error
'Error fetching route for edit:'
async
function
enableRoute
id
if
confirm
'确定要启用此路由吗?'
return
try
await
patch
`${API_BASE_URL} /${id} /enable`
loadRoutes
catch
console
error
'Error enabling route:'
async
function
disableRoute
id
if
confirm
'确定要禁用此路由吗?'
return
try
await
patch
`${API_BASE_URL} /${id} /disable`
loadRoutes
catch
console
error
'Error disabling route:'
async
function
deleteRoute
id
if
confirm
'确定要删除此路由吗?此操作不可撤销。'
return
try
await
delete
`${API_BASE_URL} /${id} `
loadRoutes
catch
console
error
'Error deleting route:'
document
getElementById
'routeForm'
addEventListener
'submit'
async
function
e
preventDefault
const
new
FormData
this
const
id
get
'editId'
parseInt
get
'editId'
undefined
routeId
get
'routeId'
routeName
get
'routeName'
uri
get
'uri'
predicates
get
'predicates'
filters
get
'filters'
orderNum
parseInt
get
'orderNum'
0
status
parseInt
get
'status'
try
let
if
id
await
put
`${API_BASE_URL} /${routeData.id} `
else
await
post
`${API_BASE_URL} `
hideForm
loadRoutes
catch
console
error
'Error saving route:'
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online