# SpringCloudGateway

Gateway网关

# 1. XXXX

待补充

# 2. 动态路由

路由配置默认是从配置文件读取,这样展示和更新都不方便,可以调整为将路由配置保存到 MySQL 数据库或者 Redis 缓存处理

  • 提供一个统一刷新当前机器路由方法,每次调用去查询最新配置加载到当前机器
  • 改造读取路由代码为直接从 MySQL 或者 Redis 读取,当然一般是从 Redis 时时读取

第一种集群机器太多不方便,需要每台机器调用,可以使用消息订阅方案,调用接口触发一个消息,所有网关机器订阅消费更新,第二种每次请求时时读取这样也稍微影响性能,所以推荐是使用第一种,机器多可以使用消息订阅方案

# 2.1. Actuator

可以从 Actuator 直接处理本机内存的动态路由,一般不配置默认显示

# 默认为true
management.endpoint.gateway.enabled = true
# 显示gateway端点
management.endpoints.web.exposure.include = gateway

以下所有端点都挂在 /actuator/gateway 下面

ID HTTP Method Description
/globalfilters GET 展示所有的全局过滤器
/routepredicates GET 展示所有的路由断言工厂
/routefilters GET 展示所有的路由过滤器工厂
/routedefinitions GET 展示当前所有的路由配置
/routes GET 展示当前所有的路由列表
/routes/{id} GET 展示指定id的路由的信息
/routes/{id} POST 新增一个路由
/routes/{id} DELETE 删除一个路由
/refresh POST 刷新当前路由配置

删除路由和刷新配置都不需要参数,新增路由参数如下,其实就和路由列表结构一样

{
  "predicates": [
    {
      "name": "Path",
      "args": {
        "_genkey_0": "/gw/**"
      }
    }
  ],
  "filters": [
    {
      "name": "AddRequestHeader",
      "args": {
        "_genkey_0": "X-Request-Foo",
        "_genkey_1": "Bar"
      }
    },
    {
      "name": "StripPrefix",
      "args": {
        "_genkey_0": "1"
      }
    }
  ],
  "uri": "https://dolyw.com",
  "order": 0
}

# 2.2. 统一加载

一般就是实现 ApplicationEventPublisherAware 接口进行内存里路由统一刷新,使用 RouteDefinitionRepository 里的 RouteDefinitionWriter 的 save() 及 delete() 方法处理内存里路由的新增,修改

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.pcic.dto.GatewayRouteDto;
import com.pcic.service.GatewayRouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.Inet4Address;
import java.net.URI;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 网关路由处理
 * https://www.cnblogs.com/ylcc-zyq/p/13156526.html
 * https://blog.csdn.net/qiuxinfa123/article/details/120317471
 * https://blog.csdn.net/huangjinjin520/article/details/124976940
 *
 * @author wliduo[i@dolyw.com]
 * @date 2022/11/2 16:47
 */
@Slf4j
@Service("gatewayRouteHandle")
public class GatewayRouteHandle implements ApplicationEventPublisherAware, CommandLineRunner {

    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private List<GatewayFilterFactory> gatewayFilters;

    @Autowired
    private List<RoutePredicateFactory> routePredicates;

    @Autowired
    private RouteDefinitionRepository routeDefinitionRepository;

    @Autowired
    private Environment environment;

    @Autowired
    private GatewayRouteService gatewayRouteService;

    /**
     * 获取推送事件
     *
     * @param applicationEventPublisher
     */
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * 启动运行
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(String... args) throws Exception {
        try {
            this.loadRoute();
        } catch (Exception e) {
            log.error("动态路由加载异常:", e);
        }
    }

    /**
     * 加载数据库动态路由
     *
     * @param
     * @return void
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2022/11/2 16:52
     */
    public String loadRoute() throws Exception {
        log.info("---------- Loaded Route Start ----------");
        // 获取原有路由
        Flux<RouteDefinition> routeDefinitionFlux = routeDefinitionRepository.getRouteDefinitions();
        Mono<List<RouteDefinition>> routeDefinitionListMono =  routeDefinitionFlux.collectList();
        List<RouteDefinition> routeDefinitionList = routeDefinitionListMono.block();
        // 删除原有路由
        for (RouteDefinition routeDefinition : routeDefinitionList) {
            routeDefinitionRepository.delete(Mono.just(routeDefinition.getId())).subscribe();
        }
        // 查询数据库最新配置路由
        List<GatewayRouteDto> gatewayRouteList = gatewayRouteService.list();
        log.info("【加载路由列表】: {}", JSON.toJSONString(gatewayRouteList));
        for (GatewayRouteDto gatewayRouteDto : gatewayRouteList) {
            RouteDefinition routeDefinition = new RouteDefinition();
            routeDefinition.setId(gatewayRouteDto.getRouteId());
            routeDefinition.setUri(URI.create(gatewayRouteDto.getRouteUri()));
            // 多个规则转换List
            gatewayRouteDto = gatewayRouteService.transList(gatewayRouteDto);
            // 处理路由匹配规则
            if (CollUtil.isNotEmpty(gatewayRouteDto.getPredicateList())) {
                List<PredicateDefinition> predicateDefinitionList = gatewayRouteDto.getPredicateList().stream()
                        .map(predicateDefinition -> new PredicateDefinition(predicateDefinition.getName()))
                        .collect(Collectors.toList());
                routeDefinition.setPredicates(predicateDefinitionList);
            }
            // 处理路由过滤规则
            if (CollUtil.isNotEmpty(gatewayRouteDto.getFilterList())) {
                List<FilterDefinition> filterDefinitionList = gatewayRouteDto.getFilterList().stream()
                        .map(filterDefinition -> new FilterDefinition(filterDefinition.getName()))
                        .collect(Collectors.toList());
                routeDefinition.setFilters(filterDefinitionList);
            }
            // 路由规则验证
            String errorMessage = this.validateRouteDefinition(routeDefinition);
            if (StrUtil.isBlank(errorMessage)) {
                routeDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
            } else {
                // 验证不通过
                log.error("【路由{}配置异常】: {}", gatewayRouteDto.getRouteId(), errorMessage);
            }
        }
        // 刷新路由
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
        log.info("---------- Loaded Route End ----------");
        // 返回当前处理机器IP
        /*String ip = Inet4Address.getLocalHost().getHostAddress();
        // 端口号
        String port = environment.getProperty("server.port");
        // 项目名称
        String webApp = environment.getProperty("server.servlet.context-path");
        if (StrUtil.isBlank(webApp)) {
            webApp = StrUtil.EMPTY;
        }
        return ip + ":" + port + webApp;*/
        return Inet4Address.getLocalHost().getHostAddress();
    }

    /**
     * 路由规则验证
     * 
     * @param routeDefinition
     * @return java.lang.String
     * @throws 
     * @author wliduo[i@dolyw.com]
     * @date 2022/11/9 14:00
     */
    public String validateRouteDefinition(RouteDefinition routeDefinition) {
        Set<String> predicatesValidateSet = routeDefinition.getPredicates().stream().filter((predicateDefinition) -> {
            return !this.routePredicates.stream().anyMatch((routePredicate) -> {
                return predicateDefinition.getName().equals(routePredicate.name());
            });
        }).map(PredicateDefinition::getName).collect(Collectors.toSet());
        Set<String> filterValidateSet = routeDefinition.getFilters().stream().filter((filterDefinition) -> {
            return !this.gatewayFilters.stream().anyMatch((gatewayFilterFactory) -> {
                return filterDefinition.getName().equals(gatewayFilterFactory.name());
            });
        }).map(FilterDefinition::getName).collect(Collectors.toSet());
        // 判断验证结果不为空
        StringBuffer errorMessage = new StringBuffer();
        if (CollUtil.isNotEmpty(predicatesValidateSet)) {
            errorMessage.append(String.format("[无效的路由匹配规则: %s]", predicatesValidateSet));
        }
        if (CollUtil.isNotEmpty(filterValidateSet)) {
            errorMessage.append(String.format("[无效的路由过滤规则: %s]", filterValidateSet));
        }
        return errorMessage.toString();
    }
}

# 2.3. 时时读取

一般就是改造 RouteDefinitionLocator 接口的 getRouteDefinitions() 方法

# 3. 自定义

自定义断言 Predicate 和过滤器 Filter

# 3.1. Predicate

待补充

# 3.2. Filter

自定义过滤器工厂,类似 SetPathGatewayFilterFactory,配置方式 ParamPathFilter={Path},单个参数的,实现多个参数可以查看其他默认提供的 KeyValue 的过滤器工厂源码

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.GatewayToStringStyler;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriTemplate;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * 自定义过滤器工厂,类似 SetPathGatewayFilterFactory
 * 配置 ParamPathFilter={Path}
 * https://www.jianshu.com/p/5d394afab425
 *
 * @author wliduo[i@dolyw.com]
 * @date 2023/1/5 17:57
 */
@Slf4j
@Component
public class ParamPathFilter extends AbstractGatewayFilterFactory<ParamPathFilter.Config> {

    public static final String PARAM_KEY = "param";

    public ParamPathFilter() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(PARAM_KEY);
    }

    @Override
    public GatewayFilter apply(Config config) {
        // 创建Uri模板用于替换动态Uri参数{uri}
        UriTemplate uriTemplate = new UriTemplate(config.param);
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取当前请求路径
                ServerHttpRequest req = exchange.getRequest();
                ServerWebExchangeUtils.addOriginalRequestUrl(exchange, req.getURI());
                // 获取动态Uri参数
                Map<String, String> uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange);
                // 当前请求路径动态Uri参数替换得到最终转发地址
                URI uri = uriTemplate.expand(uriVariables);
                String newPath = uri.getRawPath();
                // 请求转发
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, uri);
                ServerHttpRequest request = req.mutate().path(newPath).build();
                return chain.filter(exchange.mutate().request(request).build());
            }

            @Override
            public String toString() {
                return GatewayToStringStyler.filterToStringCreator(ParamPathFilter.this).append(PARAM_KEY, config.getParam()).toString();
            }
        };
    }

    public static class Config {
        private String param;

        public Config() {}

        public String getParam() {
            return param;
        }

        public void setParam(String param) {
            this.param = param;
        }
    }

}

# P. 问题

记录问题

# P.1. 参数过大

错误内容,默认请求参数最大不能超过 256KB

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
	at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:101)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ HTTP POST "/" [ExceptionHandlingWebHandler]

添加配置

# 参数缓存区大小
spring.codec.max-in-memory-size = 16MB

spring:
  codec:
    max-in-memory-size: 16MB

调整初始化 ServerRequest 代码

@Autowired
private ServerCodecConfigurer serverCodecConfigurer;

// ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
// 使用默认参数缓冲区只能256K,切换为自定义大小 spring.codec.max-in-memory-size = 16MB
ServerRequest serverRequest = ServerRequest.create(exchange, serverCodecConfigurer.getReaders());

参考

上次更新时间: 2023-12-15 03:14:55