Spring Gateway动态路由
问题引入
前面,我们已经介绍了Spring Gateway的核心概念与流程。
之前我们的路由配置都是写在配置文件中,但是,问题来了:
- 配置文件中那么多配置,记不住怎么办,能通过代码直接调用接口添加吗
- 实际生产环境中,我们路由往往是要变的,不能每次都去该配置文件或者代码重新发版吧,有没有能动态更新路由的方法
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个类的顺序就完全没有问题:
- PredicateSpec:先用order,然后path相关方法
- GatewayFilterSpec:然后接filters链式调用
- 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
}
]

从数据库创建路由
从文件加载使用比较少,我们更多的时候是从数据库加载。
还是实现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
}

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

更棒的是,我们修改了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事件,并且有一些延迟,删除是马上就会删除。

注意: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();
}
}

[
{
"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添加路有点方式非常灵活,核心在于:
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
不管是借助已经存在的RouteDefinitionLocator,还是我们自定义的,只需要保证getRouteDefinitions返回我们需要的路由就可以。
更多推荐




所有评论(0)