问题引入

前面,我们已经介绍了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
    }
]

在这里插入图片描述

从数据库创建路由

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

还是实现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返回我们需要的路由就可以。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐