news 2026/4/18 21:42:24

Spring Gateway动态路由

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Gateway动态路由

问题引入

前面,我们已经介绍了Spring Gateway的核心概念与流程。

之前我们的路由配置都是写在配置文件中,但是,问题来了:

  1. 配置文件中那么多配置,记不住怎么办,能通过代码直接调用接口添加吗
  2. 实际生产环境中,我们路由往往是要变的,不能每次都去该配置文件或者代码重新发版吧,有没有能动态更新路由的方法

RouteLocator编程式创建路由

来回忆一下RoutePredicateHandlerMapping使用的RouteLocator是CachingRouteLocator,而CachingRouteLocator会收集Spring容器中所有的RouteLocator。

所以我们直接可以通过@Bean创建RouteLocator的方式来创建路由就可以。

@Bean public RouteLocator customRouteLocator2(RouteLocatorBuilder builder) { // 没有id会自动通过uuid生成一个 return builder.routes() .route(p -> p .path("/goods/**") .uri("http://localhost:8002")) .build(); }

RouteLocatorBuilder构建器想用好并不简单,主要原因是它设计的路由和过滤器本身比较复杂。

其实它的功能还是非常强大,很多配置都能通过简单方法添加,注意下面3个类的顺序就完全没有问题:

  1. PredicateSpec:先用order,然后path相关方法
  2. GatewayFilterSpec:然后接filters链式调用
  3. UriSpec:最后uri

我们可以通过代码来实现复杂的路由:

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { ZonedDateTime datetime = ZonedDateTime.of(2026, 1, 1, 12, 0, 0, 0, ZoneId.systemDefault()); return builder.routes() .route("crl-route-id",p -> p .order(10) .path("/order/**") .and().method("GET") .filters(gfs -> gfs.addRequestHeader("GW-Blue-Ok","true") .addRequestParameter("param","v")) .uri("http://localhost:8001")) .route(p -> p .path("/goods/**") .and() .asyncPredicate(exchange -> { String method = exchange.getRequest().getMethod().name(); if ("GET".equals(method)) { return Mono.just(true); } else if ("POST".equals(method)) { boolean after = ZonedDateTime.now().isAfter(datetime); return Mono.just(after); } return Mono.just(false); }) .uri("http://localhost:8002")) .build(); }

actuator查看路由

我们添加到路由到底有没有成功,怎么查看呢?

我们当然可以直接访问对应的接口来确认,但是我们怎么知道是路由问题,还是其他问题呢?

所以,更加直接到办法是通过actuator来查看路由情况。

使用actuator需要添加对应的依赖:

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

并且配置开启:

management: endpoints: web: exposure: include: gateway endpoint: gateway: enabled: true

这里配置了暴露gateway的,可以配置为”*”

http://localhost:8000/actuator/gateway/routes

从文件创建路由

很简单,我们实现RouteDefinitionRepository就可以了。

前面我们说的InMemoryRouteDefinitionRepository就实现了RouteDefinitionRepository

RouteDefinitionRepository继承了RouteDefinitionLocator

所以所有RouteDefinitionRepository也会被CompositeRouteDefinitionLocator拿到,然后给RouteDefinitionRouteLocator,最终给了CachingRouteLocator,而CachingRouteLocator最后到RoutePredicateHandlerMapping实际使用的RouteLocator。

注意:容器中有RouteDefinitionRepository,InMemoryRouteDefinitionRepository就不会创建了。

建议保留一个RouteDefinitionRepository就可以,多个很容易就冲突了,一堆依赖注入需要兼容处理。

import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.File; import java.util.ArrayList; import java.util.List; @Slf4j @Component public class FileRouteDefinitionRepository implements RouteDefinitionRepository { private List<RouteDefinition> routeDefinitionList = new ArrayList<>(); @Value("${gateway.route.config.file}") private String configFilePathStr; @PostConstruct public void init() { load(); } /** * 加载 */ private void load() { try { ObjectMapper objectMapper = new ObjectMapper(); File file = new File(configFilePathStr); routeDefinitionList = objectMapper.readValue(file, new TypeReference<>() { }); log.info("路由配置已加载,加载条数:{}", routeDefinitionList.size()); } catch (Exception e) { log.error("从文件加载路由配置异常;{}", e.getMessage(), e); } } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return Mono.defer(() -> Mono.error(new UnsupportedOperationException("Unsupported operation"))); } @Override public Mono<Void> delete(Mono<String> routeId) { return Mono.defer(() -> Mono.error(new UnsupportedOperationException("Unsupported operation"))); } @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(routeDefinitionList); } }

配置项:

gateway: route: config: file: D:/tmp/route.json

route.json的内容:

[ { "id": "fileRouteId", "predicates": [ { "name": "Method", "args": { "_genkey_0": "GET", "_genkey_1": "POST" } } ], "filters": [ { "name": "StripPrefix", "args": { "_genkey_0": "1" } } ], "uri": "http://localhost:8055", "metadata": {}, "order": 0 } ]

Spring Gateway从文件加载路由信息

从数据库创建路由

从文件加载使用比较少,我们更多的时候是从数据库加载。

还是实现RouteDefinitionRepository接口就可以。

这里我们就不自己实现了,我们来看一下Spring Gateway自带从Redis数据库加载路由的实现。

核心实现如下:

@Override public Flux<RouteDefinition> getRouteDefinitions() { return reactiveRedisTemplate.scan(ScanOptions.scanOptions().match(createKey("*")).build()) .flatMap(key -> reactiveRedisTemplate.opsForValue().get(key)) .onErrorContinue((throwable, routeDefinition) -> { if (log.isErrorEnabled()) { log.error("get routes from redis error cause : {}", throwable.toString(), throwable); } }); }

会扫描redis中所有以routedefinition_为前缀的key都值,然后转换为RouteDefinition。

默认使用的转换器是JSON。

所以,我们随便创建一个以routedefinition_开头的可以,例如routedefinition_redisRouteId1

内容如下:

{ "id": "redisRouteId1", "predicates": [ { "name": "Method", "args": { "_genkey_0": "GET", "_genkey_1": "POST" } } ], "filters": [ { "name": "StripPrefix", "args": { "_genkey_0": "1" } } ], "uri": "http://localhost:8055", "metadata": {}, "order": 0 }

Spring Gateway Redis路由数据结构

然后我们就可以在actuator看到:

Spring Gateway从Redis加载路由信息

更棒的是,我们修改了redis的数据,自动就能获取到对应的路由信息,不需要重启。

使用之前要先创建RedisRouteDefinitionRepository:

@Configuration public class RouteConfig { @Bean public RedisRouteDefinitionRepository RedisRouteDefinitionRepository(ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate){ return new RedisRouteDefinitionRepository(reactiveRedisTemplate); } }

需要添加redis依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>

配置redis数据库信息:

spring: redis: host: localhost port: 6379

通过接口动态修改路由

除了通过Redis,我们还可以通过api接口来动态添加,这个借助InMemoryRouteDefinitionRepository来实现。

我们知道当Spring容器中没有其他的RouteDefinitionRepository就会创建一个InMemoryRouteDefinitionRepository。

所以,我们可以直接使用InMemoryRouteDefinitionRepository即可。

import jakarta.annotation.Resource; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import java.util.List; @RestController @RequestMapping("/route") public class DynamicRouteController { @Resource private InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository; @Resource private ApplicationEventPublisher applicationEventPublisher; @RequestMapping(value = "/add/{id}", method = RequestMethod.GET) public ResponseEntity<String> addRoute(@PathVariable("id") String id) { RouteDefinition routeDefinition = new RouteDefinition(id + "=http://localhost:8055"); List<FilterDefinition> filterDefinitions = List.of(new FilterDefinition("StripPrefix=1")); routeDefinition.setFilters(filterDefinitions); List<PredicateDefinition> predicateDefinitions = List.of(new PredicateDefinition("Method=GET,POST")); routeDefinition.setPredicates(predicateDefinitions); add(routeDefinition); return ResponseEntity.ok("添加成功"); } @RequestMapping(value = "/delete/{id}", method = RequestMethod.GET) public ResponseEntity<String> deleteRoute(@PathVariable("id") String id) { delete(id); return ResponseEntity.ok("删除成功"); } private void add(RouteDefinition routeDefinition) { inMemoryRouteDefinitionRepository.save(Mono.just(routeDefinition)) .then(Mono.fromRunnable(() -> applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this)) )).subscribe(); } private void delete(String id){ inMemoryRouteDefinitionRepository.delete(Mono.just(id)).subscribe(); } }

然后,我们就可以通过类似下面的接口来动态添加删除路由了:

http://localhost:8000/route/add/hhhh http://localhost:8000/route/delete/hhhh

这种方式麻烦一点的在于要属性RouteDefinition、FilterDefinition、PredicateDefinition的构造。

另外,添加路由需要ApplicationEventPublisher发布RefreshRoutesEvent事件,并且有一些延迟,删除是马上就会删除。

Spring Gateway通过接口添加路由信息

注意:InMemoryRouteDefinitionRepository只能删除它自己维护的路由,不能删除配置文件中的,因为配置文件是属于PropertiesRouteDefinitionLocator。

通过类Nacos配置中心动态修改路由

我们还是借助InMemoryRouteDefinitionRepository来实现。

通过cloude的@NacosConfigListener来监听配置中心的数据变化,变化了就刷新路由。

//import com.alibaba.nacos.api.config.annotation.NacosConfigListener; import com.alibaba.cloud.nacos.annotation.NacosConfigListener; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; @Component @Slf4j public class NacosRouteConfig { @Resource private InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository; @Resource private ApplicationEventPublisher applicationEventPublisher; @NacosConfigListener(dataId = "route", group = "DEFAULT_GROUP") public void onChange(String newConfig) { try { ObjectMapper objectMapper = new ObjectMapper(); List<RouteDefinition> routeDefinitionList = objectMapper.readValue(newConfig, new TypeReference<>() { }); log.info("路由配置已加载,加载条数:{}", routeDefinitionList.size()); updateRoutesAtomically(routeDefinitionList); applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this)); } catch (Exception e) { log.error("配置中心更新路由配置异常;{}", e.getMessage(), e); } } private void updateRoutesAtomically(List<RouteDefinition> newRoutes) { synchronized (this) { try { clearOldRoutes(); saveNewRoutes(newRoutes); } catch (Exception e) { log.error("原子更新路由失败,尝试回滚", e); throw new RuntimeException("路由更新失败", e); } } } private void clearOldRoutes() { inMemoryRouteDefinitionRepository.getRouteDefinitions() .flatMap(rd -> inMemoryRouteDefinitionRepository.delete(Mono.just(rd.getId()))) .then() .block(); } private void saveNewRoutes(List<RouteDefinition> routes) { Flux.fromIterable(routes) .flatMap(route -> inMemoryRouteDefinitionRepository.save(Mono.just(route))) .then() .block(); } }

Spring Gateway Nacos路由信息

[ { "id": "nacosRouteId", "predicates": [ { "name": "Method", "args": { "_genkey_0": "GET", "_genkey_1": "POST" } } ], "filters": [ { "name": "StripPrefix", "args": { "_genkey_0": "1" } } ], "uri": "http://localhost:8055", "metadata": {}, "order": 0 } ]

我们可以通过actuator查看已经添加成功:

Spring Gateway从Nacos配置中心添加路由

小结

Spring Gateway添加路有点方式非常灵活,核心在于:

public interface RouteDefinitionLocator { Flux<RouteDefinition> getRouteDefinitions(); }

不管是借助已经存在的RouteDefinitionLocator,还是我们自定义的,只需要保证getRouteDefinitions返回我们需要的路由就可以。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/19 19:04:56

语音合成灰度渐进式增强:逐步增加功能复杂度

语音合成灰度渐进式增强&#xff1a;逐步增加功能复杂度 在内容创作、智能交互和无障碍服务日益普及的今天&#xff0c;用户早已不满足于“能听清”的机械朗读。他们期待的是更自然、更具个性、甚至能传递情绪的语音表达——就像真人一样说话。然而&#xff0c;要让机器真正“说…

作者头像 李华
网站建设 2026/4/17 19:08:30

WebGIS开发智慧校园笔记 | 2.WebGIS开发平台介绍

往期内容&#xff1a; WebGIS开发智慧校园&#xff08;1&#xff09;GIS开发的基本概念 GIS基础平台 1&#xff09; 什么是基础平台 基础平台&#xff0c;也称系统平台&#xff0c;提供全面的开发支持&#xff0c;为专业应用方向提供基础设施. 类似于手机里的Android IOS系统…

作者头像 李华
网站建设 2026/4/18 6:13:50

为什么你的PHP应用扛不住百万流量?分库分表+读写分离才是终极解法

第一章&#xff1a;为什么你的PHP应用扛不住百万流量当用户量从千级跃升至百万级别时&#xff0c;许多基于PHP构建的应用系统开始出现响应延迟、服务崩溃甚至数据库宕机等问题。根本原因往往并非PHP语言本身性能不足&#xff0c;而是架构设计与资源调度未能适配高并发场景。同步…

作者头像 李华
网站建设 2026/4/17 12:23:58

语音合成灰度技术创新激励:奖励优秀改进提案

语音合成灰度技术创新激励&#xff1a;奖励优秀改进提案 在智能语音内容爆发式增长的今天&#xff0c;用户早已不满足于“能说话”的机械朗读。从虚拟主播的情感演绎&#xff0c;到有声书中的角色音色定制&#xff1b;从教育场景下的标准发音播报&#xff0c;到客服系统中个性化…

作者头像 李华
网站建设 2026/4/18 1:21:36

2026年哪个降AI率工具的效果最好?10个主流去AI工具测评

2026年&#xff0c;各高校明确要求毕业论文必须通过AIGC检测&#xff0c;AI率高于30%甚至20%将无法参加答辩。知网作为国内主流AIGC查重系统&#xff0c;使用知网查论文AI率的学校和师生特别多。 2025年12月28日知网完成AIGC检测算法升级&#xff0c;知网个人AIGC检测服务系统…

作者头像 李华
网站建设 2026/4/17 20:21:17

哪个工具能降知网AI率?实测比话把AI率从39降到0!

2026年&#xff0c;各高校明确要求毕业论文必须通过AIGC检测&#xff0c;AI率高于30%甚至20%将无法参加答辩。知网作为国内主流AIGC查重系统&#xff0c;使用知网查论文AI率的学校和师生特别多。 2025年12月28日知网完成AIGC检测算法升级&#xff0c;知网个人AIGC检测服务系统…

作者头像 李华