利用模版方法与策略模式 实现自定义 SpringCloud Gateway Filter 自动拼装

879 阅读6分钟

先聊聊 Spring Cloud Gateway

Spring Cloud Gateway 的三个火枪手

路由 Route

网关的基本构建块,根据断言进行路由匹配. 根据路由的数据结构,可以看出路由包含: 路由 id, 目标 URI, 断言集合,过滤器集合以及路由排序.

@Validated
public class RouteDefinition {

   @NotEmpty
   private String id = UUID.randomUUID().toString();

   @NotEmpty
   @Valid
   private List<PredicateDefinition> predicates = new ArrayList<>();

   @Valid
   private List<FilterDefinition> filters = new ArrayList<>();

   @NotNull
   private URI uri;

   private int order = 0;

断言 Predicate

网关断言基于 Java 8 函数式断言接口实现, 网关断言接口的输入类型是 ServerWebExchange, 这样可以匹配HTTP请求中的所有内容,例如标头或参数。

public interface GatewayPredicate extends Predicate<ServerWebExchange> 

过滤器 Filter

网关根据过滤器工厂,提供了功能丰富的内置过滤器,可以让我们在发送下游请求之前或之后修改请求和响应.

public interface GatewayFilter extends ShortcutConfigurable {

   /**
    * Name key.
    */
   String NAME_KEY = "name";

   /**
    * Value key.
    */
   String VALUE_KEY = "value";

   /**
    * Process the Web request and (optionally) delegate to the next {@code WebFilter}
    * through the given {@link GatewayFilterChain}.
    * @param exchange the current server exchange
    * @param chain provides a way to delegate to the next filter
    * @return {@code Mono<Void>} to indicate when request processing is complete
    */
   Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

}

Spring Cloud Gateway 运行过程

提及网关就少不了,官方文档上放出的运行过程图.

从这个过程中我们能看出,客户端发出请求, Gateway Handler Mapping 进行路由匹配的判断, 一旦匹配成功,请求就被分发到 Gateway Web Handler 进行处理. Gateway Web Handler 将请求经过一系列 filter chain.

而如何利用模版和策略模式进行过滤器拼装和动态注入,便是今天的主题.

自定义 Spring Cloud Gateway Filter

过滤器配置方式

官方文档上一个 sample, 在 id 为 add_request_header_route 这个路由对象中, 配置一个 增加请求头参数的过滤器.

spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-red, blue

自定义一个利用配置文件加载的过滤器

过滤器数据结构

/**
 * @author Spencer Gibb
 */
@Validated
public class FilterDefinition {

   @NotNull
   private String name;

   private Map<String, String> args = new LinkedHashMap<>();

抽象过滤器工厂实现一个黑名单过滤器

通过继承 Spring Cloud Gateway 提供的 AbstractGatewayFilterFactory 进行自定以过滤器.

/**
 * <功能描述> 黑名单过滤器
 *
 */
@Component
public class BlacklistGatewayFilterFactory extends AbstractGatewayFilterFactory<BlacklistGatewayFilterFactory.Config> {

    private static final Logger LOGGER = LoggerFactory.getLogger(BlacklistGatewayFilterFactory.class);

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String url = exchange.getRequest().getPath().toString();
            if (UrlMatchUtils.match(url, config.sources)) {
                LOGGER.info("==================进入黑名单网关过滤器================");
                exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                return exchange.getResponse().setComplete();
            } else {
                return chain.filter(exchange);
            }
        };
    }

    @Validated
    public static class Config {
        @NotEmpty
        private List<String> sources = new ArrayList<>();

        public List<String> getSources() {
            return sources;
        }

        public Config setSources(List<String> sources) {
            this.sources = sources;
            return this;
        }

        public Config setSources(String... sources) {
            this.sources = Arrays.asList(sources);
            return this;
        }
    }

}

配置自定义的黑名单过滤器

由于黑名单过滤器接收的参数为数组,所以不能用 shortcut Configuration 简写而是采用 Full Expanded Arguments 配置写法,将全部配置的数据结构展现在配置文件中.

spring:
  cloud:
    gateway:
      routes:
        - id: blacklist_filter
      uri: https://example.org
      filters:
        - name: Blacklist
        args:
          sources:
            - /demo1/**
            - /demo2/**        

这样便完成了一个自定义网关过滤器的加载.

将路由对象进行 Mysql 持久化

之前写过一篇 利用 Mysql 实现 Spring Cloud Gateway 动态路由, 建立两张表来持久化, RouteDefinition 这个对象.

Mysql Schema

gateway_route_t

gateway_route_param_t

我们将 刚刚定义的 Blacklist 黑名单过滤器配置的参数存入表中

gateway_route_t

gateway_route_param_t

进行黑名单过滤器组装

拼装过滤器之模版方法

依据上面的设计,我们进行过滤器组装需要两个步骤.

  • 从数据库中根据 PARAM_NAME 进行查询.
  • 不同过滤器存在不同的组装方式.

考虑到动态进行过滤器组装的注入,我们在过滤器组装模版中增加第三个方法

  • 从上下文中删除已拼装好的过滤器查询ke y

所以我们的模版 抽象类:

/**
 * <功能描述> 路由 filter 组装策略模版方法
 *
 */
public abstract class WrapTemplate implements IFilterWrap {

    /**
     * 抽象查询方法
     *
     * @param context
     * @return
     */
    protected abstract List<RouteParamDomain> query(WrapContext context);

    /**
     * 抽象拼装方法
     *
     * @param routeParamDomainList
     * @return
     */
    protected abstract Map<String, List<FilterDefinition>> wrap(List<RouteParamDomain> routeParamDomainList);

    /**
     * 抽象移除已拼装过上下文的方法
     *
     * @param context
     */
    protected abstract void removeContext(WrapContext context);

    /**
     * 依据上线文进行过滤器集合拼装的模版方法
     * @param wrapContext
     * @return
     */
    public Map<String, List<FilterDefinition>> wrapFilterDefinition(WrapContext wrapContext) {
        if (CollectionUtils.isEmpty(wrapContext.getParamName())) {
            return new HashMap<>();
        }
        //获取 route param domain list
        List<RouteParamDomain> routeParamDomainList = query(wrapContext);
        //组装成map
        Map<String, List<FilterDefinition>> filterWrapMap = wrap(routeParamDomainList);
        //删除已经组装过的上下文
        removeContext(wrapContext);
        return filterWrapMap;
    }
}

实现黑名单过滤器之策略模式

由于定义好了模版方法,那么不同自定义过滤器的拼装就是不同的实现策略.所以黑名单过滤器拼装采用策略模式.

黑名单过滤器策略模式拼装实现

/**
 * <功能描述> 路由 filter 组装 黑名单 filter 策略
 *
 * @author 20024322
 * @date 2020/12/24 13:20
 */
@Service
public class BlaclFilterStrategyWrap extends WrapTemplate {
    /**
     * 黑名单过滤器工厂名称
     */
    private static final String BLACK_FILTER_FACTORY_NAME = "Blacklist";

    /**
     * API 过滤器工厂参数 key 前缀
     */
    private static final String API_FILTER_ARGS_KEY_PREFIX = "sources";


    private final IRouteParamConfigService routeParamConfigService;

    /**
     * Api Filter Strategy 参数数组
     */
    private final List<String> paramNameList = new ArrayList<>();

    public ApiFilterStrategyWrap(IRouteParamConfigService routeParamConfigService) {
        this.routeParamConfigService = routeParamConfigService;
        //初始化当前上下文的参数数组
        paramNameList.add(BLACK_FILTER_FACTORY_NAME);
    }

    @Override
    public List<RouteParamDomain> query(WrapContext wrapContext) {
        //上下文
        if (!wrapContext.getParamName().contains(BLACK_FILTER_FACTORY_NAME)) {
            return new ArrayList<>();
        }

        RouteQuerySchema querySchema = new RouteQuerySchema();
        querySchema.setParamNameList(paramNameList);
        querySchema.setValid(Constants.ROUTE_VALID);
        List<RouteParamDomain> routeParamDomainList = routeParamConfigService.getParamDomainByQuerySchema(querySchema);
        if (CollectionUtils.isEmpty(routeParamDomainList)) {
            return new ArrayList<>();
        }
        return routeParamDomainList;
    }

    @Override
    protected Map<String, List<FilterDefinition>> wrap(List<RouteParamDomain> routeParamDomainList) {
        Map<String, List<FilterDefinition>> routeFilterDefinitionMap = new HashMap<>();
        Map<String, List<RouteParamDomain>> routeIdParamMap = routeParamDomainList.stream().collect(Collectors.groupingBy(RouteParamDomain::getRouteId));
        routeIdParamMap.forEach((k, v) -> {
            if (CollectionUtils.isEmpty(v)) {
                return;
            }

            Map<String, List<RouteParamDomain>> paramNameMap = v.stream()
                    .collect(Collectors.groupingBy(RouteParamDomain::getParamName));

            List<FilterDefinition> filterDefinitionList = new ArrayList<>();
            paramNameMap.forEach((n, l) -> {
                FilterDefinition filterDefinition = new FilterDefinition();
                filterDefinition.setName(n);
                filterDefinition.setArgs(wrapParamArgs(l));
                filterDefinitionList.add(filterDefinition);
            });

            routeFilterDefinitionMap.put(k, filterDefinitionList);
        });

        return routeFilterDefinitionMap;
    }

    @Override
    protected void removeContext(WrapContext context) {
        //清除已装填类型
        context.getParamName().removeAll(paramNameList);
    }

    protected Map<String, String> wrapParamArgs(List<RouteParamDomain> routeParamDomainList) {
        Map<String, String> args = new HashMap<>();
        for (int i = 0; i < routeParamDomainList.size(); i++) {
            RouteParamDomain routeParamDomain = routeParamDomainList.get(i);
            args.put(API_FILTER_ARGS_KEY_PREFIX + "." + i, routeParamDomain.getParamValue());
        }
        return args;
    }
}

默认过滤器的拼装策略

除了特殊自定义过滤器的拼装实现,我们还要提供默认的过滤器拼装实现,由于不需要自定义组装策略的上下文所以默认过滤器的执行顺序在所有拼装策略最后进行.

/**
 * <功能描述> 路由 filter 默认组装策略
 * 执行顺序最后
 *
 */
@Service
@Order
public class DefaultFilterStrategyWrap extends WrapTemplate {

    private final IRouteParamConfigService routeParamConfigService;

    public DefaultFilterStrategyWrap(IRouteParamConfigService routeParamConfigService) {
        this.routeParamConfigService = routeParamConfigService;
    }

    @Override
    protected List<RouteParamDomain> query(WrapContext context) {
        //获取最后剩下 paramNameList 参数
        RouteQuerySchema querySchema = new RouteQuerySchema();
        querySchema.setParamNameList(new ArrayList<>(context.getParamName()));
        querySchema.setValid(Constants.ROUTE_VALID);
        List<RouteParamDomain> routeParamDomainList = routeParamConfigService.getParamDomainByQuerySchema(querySchema);
        if (CollectionUtils.isEmpty(routeParamDomainList)) {
            return new ArrayList<>();
        }
        return routeParamDomainList;
    }

    @Override
    protected Map<String, List<FilterDefinition>> wrap(List<RouteParamDomain> routeParamDomainList) {
        if (CollectionUtils.isEmpty(routeParamDomainList)) {
            return new HashMap<>();
        }
        //组装返回值在
        Map<String, List<FilterDefinition>> defaultFilterDefinitionMap = new HashMap<>();
        Map<String, List<RouteParamDomain>> routeIdParamMap = routeParamDomainList.stream().collect(Collectors.groupingBy(RouteParamDomain::getRouteId));
        routeIdParamMap.forEach((k, v) -> {
            if (CollectionUtils.isEmpty(v)) {
                return;
            }

            List<FilterDefinition> filterDefinitionList = new ArrayList<>();
            Map<String, List<RouteParamDomain>> filterParamNameMap = v.stream().collect(Collectors.groupingBy(RouteParamDomain::getParamName));
            filterParamNameMap.forEach((f, l) -> filterDefinitionList.add(wrapFilter(l)));
            defaultFilterDefinitionMap.put(k, filterDefinitionList);
        });
        return defaultFilterDefinitionMap;
    }

    @Override
    protected void removeContext(WrapContext context) {
        // Do nothing because the Wrapper's order is lowest
    }

    private FilterDefinition wrapFilter(List<RouteParamDomain> filterList) {
        if (CollectionUtils.isEmpty(filterList)) {
            return new FilterDefinition();
        }

        FilterDefinition filterDefinition = new FilterDefinition();
        filterDefinition.setName(filterList.get(0).getParamName());
        Map<String, String> args = filterList.stream()
                .collect(Collectors.toMap(RouteParamDomain::getParamKey, RouteParamDomain::getParamValue, (v1, v2) -> v2));
        filterDefinition.setArgs(args);
        return filterDefinition;
    }
}

利用 Spring bean 管理的方式,进行过滤器组装策略的动态注入

定义组装过滤器策略的客户端接口

public interface IWrapFilterClient {

    /**
     * 根据策略获取所有过滤器
     *
     * @param filterContext
     * @return
     */
    Map<String, List<FilterDefinition>> wrapFilterDefinition(WrapContext filterContext);
}

定义组装过滤器策略的客户端接口实现

  • 注入拼装策略 List 获取所有的拼装策略
/**
 * <功能描述> 组装路由数据客户端
 *
 * @author 20024322
 * @date 2020/12/24 13:20
 */
@Service
public class WrapFilterClientImpl implements IWrapFilterClient {
    /**
     * 注入所有的过滤器拼装策略
     */
    private final List<IFilterWrap> iFilterWraps;

    public WrapFilterClientImpl(List<IFilterWrap> iFilterWraps) {
        this.iFilterWraps = iFilterWraps;
    }

    @Override
    public Map<String, List<FilterDefinition>> wrapFilterDefinition(WrapContext filterContext) {
        Map<String, List<FilterDefinition>> resultMap = new HashMap<>();
        for (IFilterWrap filterWrap : iFilterWraps) {
            Map<String, List<FilterDefinition>> predicateDefinitionMap = filterWrap.wrapFilterDefinition(filterContext);
            predicateDefinitionMap.forEach((k, v) -> {
                if (resultMap.containsKey(k)) {
                    resultMap.get(k).addAll(v);
                } else {
                    resultMap.put(k, v);
                }
            });
        }
        return resultMap;
    }
}

将拼装好的过滤交由网关管理

利用 RouteDefinitionWriter 实现路由的生效

public interface RouteDefinitionWriter {

   Mono<Void> save(Mono<RouteDefinition> route);

   Mono<Void> delete(Mono<String> routeId);

}

利用事件监听保存本地路由缓存策略实现

/**
 * <功能描述> 保存本地路由缓存策略实现
 *
 */
@Service
public class SaveLocalRouteCacheImpl implements IRefreshRouteEventStrategy {
    private static final Logger LOGGER = LoggerFactory.getLogger(SaveLocalRouteCacheImpl.class);

    private final IGatewayRouteService gatewayRouteService;

    public SaveLocalRouteCacheImpl(IGatewayRouteService gatewayRouteService) {
        this.gatewayRouteService = gatewayRouteService;
    }


    @Override
    public void executeEvent(RouteEventContext context) {
        if (null == context || CollectionUtils.isEmpty(context.getRouteEventContentList())) {
            return;
        }

        //判断类型
        List<RouteEventContent> saveRouteEvent = context.getRouteEventContentList().stream()
                .filter(eve -> eve.getRefreshType().equals(RouteEventType.SAVE_LOCAL_ROUTE)).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(saveRouteEvent)) {
            return;
        }

        List<RouteDefinition> routeDefinitionList = new ArrayList<>();
        //处理保存路由事件
        saveRouteEvent.forEach(route -> {
            List<RouteDefinition> singleRouteDefinitionList = (List<RouteDefinition>) route.getArgs().get(Constants.ROUTE_DEFINITION_LIST_KEY);
            routeDefinitionList.addAll(singleRouteDefinitionList);
        });

        LOGGER.info("执行刷新本地缓存策略:{}", routeDefinitionList);
        routeDefinitionList.forEach(gatewayRouteService::addInMemoryRouteRefresh);
    }

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

以上就完成了 自定义过滤器的动态拼装与生效, 欢迎留言交流!