Spring Cloud Gateway 动态路由管理平台:Web UI 实时配置
本文介绍基于 Spring Cloud Gateway 构建的动态路由管理平台,通过 Web UI 实现路由规则的实时增删改查。核心方案包括使用 MySQL 存储路由定义,JPA 进行数据持久化,结合 RouteDefinitionWriter 动态刷新网关路由。涵盖环境搭建、实体设计、API 开发、前端交互及高级功能如版本控制、权限管理和性能优化,解决微服务架构中静态路由配置维护困难的问题。

本文介绍基于 Spring Cloud Gateway 构建的动态路由管理平台,通过 Web UI 实现路由规则的实时增删改查。核心方案包括使用 MySQL 存储路由定义,JPA 进行数据持久化,结合 RouteDefinitionWriter 动态刷新网关路由。涵盖环境搭建、实体设计、API 开发、前端交互及高级功能如版本控制、权限管理和性能优化,解决微服务架构中静态路由配置维护困难的问题。


在现代微服务架构中,服务的动态伸缩和灵活部署变得越来越重要。传统的静态路由配置方式(如硬编码在 application.yml 文件中)在面对频繁的服务变更时显得笨拙且难以维护。一个理想的解决方案是构建一个动态路由管理平台,允许运维人员或开发者通过一个Web UI实时地添加、修改、删除和启用/禁用路由规则,而无需重启服务或手动编辑配置文件。
本文将深入探讨如何构建这样一个动态路由管理平台,重点介绍如何在 Spring Cloud Gateway 上实现基于 Web UI 的动态路由配置。我们将从基础概念出发,逐步讲解如何设计和实现一个完整的动态路由管理功能,包括前端交互、后端 API、数据存储以及与 Gateway 的集成。
随着微服务架构的普及,应用被拆分成多个独立的服务单元,这些服务通过网络进行通信。一个典型的请求可能需要经过多个服务才能完成。为了实现服务间的通信,需要一个强大的API 网关来处理请求路由、认证、限流、熔断等功能。
然而,传统基于静态配置的路由方式存在以下问题:
动态路由管理平台旨在解决上述痛点,它提供了一套实时、可视化的路由配置机制,使运维团队能够:
本文旨在:
本项目主要涉及以下技术栈:
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
<?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>
创建一个名为 dynamic_gateway 的数据库:
CREATE DATABASE IF NOT EXISTS dynamic_gateway CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE dynamic_gateway;
-- 创建路由定义表
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);
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
在 Spring Cloud Gateway 中,路由规则是由 RouteDefinition 对象组成的。RouteDefinition 包含以下关键字段:
lb://user-service)。要实现动态路由,我们需要:
RouteDefinition 对象。RouteDefinitionLocator 或者 RouteRefreshListener 等机制,通知 Gateway 更新其内部路由表。// 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;
@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;
}
// 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
public interface RouteRepository extends JpaRepository<RouteDefinition, Long> {
RouteDefinition findByRouteId(String routeId);
List<RouteDefinition> findByStatus(Integer status);
List<RouteDefinition> findAllByStatus(Integer status);
}
// 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;
}
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;
}
}
// 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 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();
}
}
// 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;
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);
}
}
// 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;
@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();
}
}
用户请求发送到 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,从而更新数据库中的路由规则。
<!-- 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/[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>
dynamic_gateway 数据库已创建。DynamicGatewayApplication。http://localhost:8080。为了让用户能实时看到路由状态的变化,可以考虑使用 WebSocket 或 Server-Sent Events (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;
@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);
}
}
}
}
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();
});
为了方便批量操作,可以添加路由的导入和导出功能。
@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();
}
}
@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();
}
}
对于重要的路由变更,可以引入版本控制机制,记录每一次变更的历史。
在 RouteDefinition 实体类中添加版本字段:
@Column(name = "version", nullable = false, columnDefinition = "INT DEFAULT 1")
private Integer version = 1;
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
);
在生产环境中,需要对路由管理进行权限控制。可以使用 Spring Security 来实现。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@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();
}
}
route_id, status, created_at 等常用查询字段添加索引。RefreshRoutesEvent 事件可能未正确触发,或者 Gateway 的 RouteDefinitionLocator 未能及时感知变化。GatewayConfig 中的 loadAllRoutes() 方法,确保其正确调用了 publisher.publishEvent(new RefreshRoutesEvent(this))。try-catch 块并提供详细错误信息。如果使用了 Eureka、Consul 或 Nacos 等服务注册中心,可以动态地根据服务实例的变化来更新路由规则。
可以将路由规则存储在 Spring Cloud Config Server 或类似的配置中心中,实现配置的集中管理和动态刷新。
将路由管理平台与 Prometheus、Grafana 等监控系统集成,可以监控路由变更、访问量、成功率等指标。
RouteDefinition 对象。通过本文的详细介绍,我们成功构建了一个基于 Spring Cloud Gateway 的动态路由管理平台。这个平台不仅提供了基本的路由 CRUD 操作,还展示了如何通过 Web UI 实现实时配置,极大地提升了微服务架构中路由管理的灵活性和效率。希望这篇文章能够为你在实际项目中的应用提供有价值的参考和指导。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online