Skip to content

基于 Redis 配置网关路由

RouteDefinitionRepository

  • 网关路由接口

InMemoryRouteDefinitionRepository

RedisRouteDefinitionRepository

  • 网关路由基于 Redis 的实现
源码
java
/*
 * Copyright 2013-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.gateway.route;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveValueOperations;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Repository;

/**
 * @author Dennis Menge
 * @author lzhpo
 */
@Repository
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private static final Logger log = LoggerFactory.getLogger(RedisRouteDefinitionRepository.class);

    /**
     * Key prefix for RouteDefinition queries to redis.
     */
    private static final String ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY = "routedefinition_";

    private ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate;

    private ReactiveValueOperations<String, RouteDefinition> routeDefinitionReactiveValueOperations;

    public RedisRouteDefinitionRepository(ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate) {
        this.reactiveRedisTemplate = reactiveRedisTemplate;
        this.routeDefinitionReactiveValueOperations = reactiveRedisTemplate.opsForValue();
    }

    @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);
                    }
                });
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> routeDefinitionReactiveValueOperations
                .set(createKey(routeDefinition.getId()), routeDefinition).flatMap(success -> {
                    if (success) {
                        return Mono.empty();
                    }
                    return Mono.defer(() -> Mono.error(new RuntimeException(
                            String.format("Could not add route to redis repository: %s", routeDefinition))));
                }));
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> routeDefinitionReactiveValueOperations.delete(createKey(id)).flatMap(success -> {
            if (success) {
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(new NotFoundException(
                    String.format("Could not remove route from redis repository with id: %s", routeId))));
        }));
    }

    private String createKey(String routeId) {
        return ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY + routeId;
    }

}

配置

properties
# https://github.com/spring-cloud/spring-cloud-gateway/blob/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java
spring.cloud.gateway.redis-route-definition-repository.enabled=true
yaml
# https://github.com/spring-cloud/spring-cloud-gateway/blob/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java
spring:
  cloud:
    gateway:
      redis-route-definition-repository:
        enabled: true
java
package cn.com.xuxiaowei.shield.gateway.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ReactiveRedisTemplate;

/**
 * 使用 Redis 储存 网关 route 路由配置
 *
 * @author xuxiaowei
 * @since 0.0.1
 * @see <a href="https://github.com/spring-cloud/spring-cloud-gateway/blob/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java#L232">inMemoryRouteDefinitionRepository</a>
 */
@Slf4j
@Configuration
public class RedisRouteDefinitionRepositoryConfig {

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

}

使用

Redis 网关路由数据前缀

网关路由数据类型

  • spring.cloud.gateway.routes 可知, 网关路由配置类是 RouteDefinition

手动创建 Redis 网关路由数据

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: baidu
          uri: https://www.baidu.com
          predicates:
            - Host=baidu.localdev.me:*
properties
spring.cloud.gateway.routes[0].id=baidu
spring.cloud.gateway.routes[0].uri=https://www.baidu.com
spring.cloud.gateway.routes[0].predicates[0]=Host=baidu.localdev.me:*
json
{
  "id": "baidu",
  "predicates": [
    {
      "name": "Host",
      "args": {
        "args1": "baidu.localdev.me:*"
      }
    }
  ],
  "filters": [],
  "uri": "https://www.baidu.com",
  "metadata": {
  },
  "order": 0
}

使用程序创建 Redis 网关路由数据

java
package cn.com.xuxiaowei.shield.gateway.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

/**
 * 使用 Redis 储存 网关 route 路由配置 测试类
 *
 * @author xuxiaowei
 * @since 0.0.1
 */
@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RedisRouteDefinitionRepositoryConfigTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    @SneakyThrows
    public void set() {
        String key = "routedefinition_:" + UUID.randomUUID().toString().substring(0, 8);

        RouteDefinition routeDefinition = new RouteDefinition();
        routeDefinition.setId("baidu");
        routeDefinition.setUri(new URI("https://www.baidu.com"));

        PredicateDefinition predicateDefinition = new PredicateDefinition();
        predicateDefinition.setName("Host");
        HashMap<String, String> args = new HashMap<>();
        args.put("args1", "baidu.localdev.me:*");
        predicateDefinition.setArgs(args);

        routeDefinition.setPredicates(List.of(predicateDefinition));

        ObjectMapper objectMapper = new ObjectMapper();
        String routeDefinitionStr = objectMapper.writeValueAsString(routeDefinition);

        // {"id":"baidu","predicates":[{"name":"Host","args":{"args1":"baidu.localdev.me:*"}}],"filters":[],"uri":"https://www.baidu.com","metadata":{},"order":0}
        log.info("routeDefinition: {}", routeDefinitionStr);

        // 根据自己的需求,设置有效期
        stringRedisTemplate.opsForValue().set(key, routeDefinitionStr);
    }

}

使用 事件订阅 刷新 Redis 网关路由配置

java
package cn.com.xuxiaowei.shield.gateway.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

/**
 * 过滤器
 *
 * @author xuxiaowei
 * @since 0.0.1
 */
@Slf4j
@Setter
@Component
public class ApplicationEventPublisherWebFilter implements WebFilter, Ordered {

    public static final int ORDERED = Ordered.HIGHEST_PRECEDENCE + 20000;

    private static final String PUBLISH_EVENT_PATH = "/publish-event/refresh-routes-event";

    private ApplicationEventPublisher applicationEventPublisher;

    private int order = ORDERED;

    @Autowired
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @Override
    public int getOrder() {
        return order;
    }

    @NonNull
    @Override
    @SneakyThrows
    @SuppressWarnings("all")
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String path = uri.getPath();

        if (PUBLISH_EVENT_PATH.equals(path)) {

            applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));

            ServerHttpResponse response = exchange.getResponse();

            // 响应状态码
            response.setStatusCode(HttpStatus.OK);

            // 不可使用 APPLICATION_JSON(部分浏览器会乱码)
            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);

            Map<String, Object> map = new HashMap<>();
            map.put("msg", "网关路由事件已订阅");

            ObjectMapper objectMapper = new ObjectMapper();

            byte[] bytes = objectMapper.writeValueAsBytes(map);

            DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
            return response.writeWith(Mono.just(dataBuffer));
        }

        return chain.filter(exchange);
    }

}

使用 端点 刷新 Redis 网关路由配置

  • 引入 端点 依赖
xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 启用网关 端点 配置
yaml
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    gateway:
      # Spring Boot 2 默认为 true
      # https://github.com/spring-cloud/spring-cloud-gateway/blame/3.1.x/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
      # https://github.com/spring-cloud/spring-cloud-gateway/blame/4.0.x/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
      # https://github.com/spring-cloud/spring-cloud-gateway/commit/25fb7475a766345928a86652f0d56b771018b483
      enabled: true
yaml
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    gateway:
      # Spring Boot 3 默认为 false
      # https://github.com/spring-cloud/spring-cloud-gateway/blame/3.1.x/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
      # https://github.com/spring-cloud/spring-cloud-gateway/blame/4.0.x/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
      # https://github.com/spring-cloud/spring-cloud-gateway/commit/25fb7475a766345928a86652f0d56b771018b483
      enabled: true
  • 触发 端点 刷新
    • POST 请求访问 /actuator/gateway/refresh