使用网关过滤器,根据业务规则实现微服务动态路由

文章目录

业务场景

  • 我们服务使用Spring Cloud微服务架构,使用Spring Cloud Gateway 作为网关,使用 Spring Cloud OpenFeign 作为服务间通信方式
  • 作为网关,主要作用是鉴权与路由转发。大多数应用场景,网关主要是针对前端的请求,前端调用接口,网关鉴权和转发。对于微服务间的调用,一般都不经过网关,直接根据注册服务列表路由到对应服务
  • 对于一般的微服务路由转发,一般按照默认设置,根据服务名转发到对应服务即可
  • 对于一些有转发规则的请求,也可以根据规则(如url前缀、某个参数的值、请求header里某个属性的)匹配转发到对应服务
  • 简单的直接在配置文件里编写,例如某个值到某个服务是已知的,这些值是确定的和少量的,可以根据不通值到对应的服务,也可以自定义过滤器实现
  • 现在有一个需求,需要根据参数的值路由到对应的服务,但是参数的值不是枚举值,数量不确定,可能有很多,值也不确定
  • 但是这个值对应哪个服务(ip 端口)是已知的,需要实现根据这个业务规则动态路由到对应服务

拦截器实现

  • Spring Cloud Gateway的默认配置写法,将服务名转化为小写,根据名字匹配
yml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          predicates:
            - name: Path
              args:
                pattern: "'/services/'+serviceId.toLowerCase()+'/**'"
          filters:
            - name: RewritePath
              args:
                regexp: "'/services/' + serviceId.toLowerCase() + '/(?<remaining>.*)'"
                replacement: "'/${remaining}'"
  • 默认写法,只根据服务名字匹配进行路由转发,同服务名的多个微服务,轮询转发
  • 一开始试着重写RouteToRequestUrlFilter,发现没有作用,根本走不到这个类
  • 后面在@Component注解里指定了名称value = "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter",完全覆盖,生效了
  • 具体代码如下,核心是路由转发重新指定exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl)
java 复制代码
package com.newatc.com.authorization;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.newatc.com.SpringContextHolderUtil;
import com.newatc.com.authorization.util.HttpClientUtil;
import com.newatc.com.authorization.vo.SignalVO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * 重写Spring的RouteToRequestUrlFilter,自定义unit服务路由
 *
 * @author yanyulin
 * @date 2023-12-6 19:44:52
 */
@Component(value = "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter")
@Lazy
public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {

    public static final int ROUTE_TO_URL_FILTER_ORDER = 10000;
    private static final Log log = LogFactory.getLog(RouteToRequestUrlFilter.class);
    private static final String SCHEME_REGEX = "[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*";
    static final Pattern schemePattern = Pattern.compile(SCHEME_REGEX);
    private static final String UNIT_API_PREFIX = "unit";
    private static final String UNIT_ADAPTER = "UNIT_ADAPTER";
    public static final String GET_UNIT_URL = "/services/core/api/signalcontrol/getSignalList";
    public static final String GET_UNIT_HOSTS = "/services/core/api/signalcontrol/getUnitHostList";
    private static final RedisTemplate<String, String> redisTemplate = SpringContextHolderUtil.getBean(StringRedisTemplate.class);

    @Value("${server.port}")
    private Integer SERVER_PORT;

    public RouteToRequestUrlFilter() {}

    static boolean hasAnotherScheme(URI uri) {
        return schemePattern.matcher(uri.getSchemeSpecificPart()).matches() && uri.getHost() == null && uri.getRawPath() == null;
    }

    public int getOrder() {
        return ROUTE_TO_URL_FILTER_ORDER;
    }

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        if (route == null) {
            return chain.filter(exchange);
        } else {
            log.trace("RouteToRequestUrlFilter start");
            URI uri = exchange.getRequest().getURI();
            boolean encoded = ServerWebExchangeUtils.containsEncodedParts(uri);
            URI routeUri = route.getUri();
            if (hasAnotherScheme(routeUri)) {
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
                routeUri = URI.create(routeUri.getSchemeSpecificPart());
            }
            log.debug("routeUri: " + JSON.toJSONString(routeUri));

            if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
                throw new IllegalStateException("Invalid host: " + routeUri);
            } else {
                // 原先的Uri
                URI mergedUrl = UriComponentsBuilder
                    .fromUri(uri)
                    .scheme(routeUri.getScheme())
                    .host(routeUri.getHost())
                    .port(routeUri.getPort())
                    .build(encoded)
                    .toUri();

                // 如果是需要自定义指定路由的服务,根据业务重写
                if (UNIT_API_PREFIX.equalsIgnoreCase(routeUri.getHost())) {
                    try {
                        log.info("mergedUrl前: " + JSON.toJSONString(mergedUrl));
                        String[] pathArr = uri.getPath().split("/");
                        String unitId = pathArr[pathArr.length - 1];
                        String host = "";
                        if (StringUtils.hasText(unitId)) {
                            host = getUnitHost(unitId);
                        }

                        if (StringUtils.hasText(host)) {
                            String newurl = host + mergedUrl.getPath();
                            if (StringUtils.hasText(exchange.getRequest().getURI().getQuery())) {
                                newurl = newurl + "?" + exchange.getRequest().getURI().getQuery();
                            }
                            URI newURI = new URI(newurl);
                            mergedUrl =
                                UriComponentsBuilder
                                    .fromUri(uri)
                                    .scheme(newURI.getScheme())
                                    .host(newURI.getHost())
                                    .port(newURI.getPort())
                                    .build(encoded)
                                    .toUri();
                            log.debug("mergedUrl后: " + JSON.toJSONString(mergedUrl));
                        }
                    } catch (Exception e) {
                        log.error("uri error", e);
                    }
                }
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);
                return chain.filter(exchange);
            }
        }
    }

    /**
     * 根据信号机编号获取对应的服务适配器
     *
     * @param unitId
     * @return
     */
    private String getUnitHost(String unitId) {
        Map<String, String> unitIdHostMap = new HashMap<>();
        List<SignalVO> signalList;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(UNIT_ADAPTER))) {
            signalList = JSONArray.parseArray(redisTemplate.opsForValue().get(UNIT_ADAPTER), SignalVO.class);
        } else {
            String url = "http://localhost:" + SERVER_PORT + GET_UNIT_URL;
            String info = HttpClientUtil.doGet(url, null);
            signalList = JSONArray.parseArray(info, SignalVO.class);
            if (null != signalList && !signalList.isEmpty()) {
                redisTemplate.opsForValue().set(UNIT_ADAPTER, JSON.toJSONString(signalList), 1, TimeUnit.MINUTES);
            }
        }
        if (null != signalList && !signalList.isEmpty()) {
            signalList.forEach(e -> unitIdHostMap.put(e.getUnitId(), "http://" + e.getServerIp() + ":" + e.getServerPort()));
        }

        return unitIdHostMap.get(unitId);
    }
}
  • 核心就是重新指定uri,再设置进请求里
java 复制代码
String newurl = host + mergedUrl.getPath();
if (StringUtils.hasText(exchange.getRequest().getURI().getQuery())) {
    newurl = newurl + "?" + exchange.getRequest().getURI().getQuery();
}
URI newURI = new URI(newurl);
mergedUrl =
    UriComponentsBuilder
        .fromUri(uri)
        .scheme(newURI.getScheme())
        .host(newURI.getHost())
        .port(newURI.getPort())
        .build(encoded)
        .toUri();
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);

Spring Cloud Gateway介绍

Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,用于构建基于Spring Boot的API网关服务。Spring Cloud Gateway基于Reactive编程模型,使用WebFlux框架实现,可以快速、可靠地构建和部署高性能的微服务应用程序。

Spring Cloud Gateway具有以下特点:

  1. 基于WebFlux:Spring Cloud Gateway基于Reactive编程模型,利用WebFlux框架实现非阻塞、事件驱动的异步处理,可以提供更好的性能和并发处理能力。

  2. 灵活的路由规则:Spring Cloud Gateway支持基于URI、请求方法、请求头等多种维度的路由规则配置,使得对请求进行灵活的路由转发变得简单易用。

  3. 过滤器:Spring Cloud Gateway的过滤器功能可以实现对请求和响应的预处理和后处理,包括请求日志记录、鉴权、路由转发、重定向等功能,可以满足各种需求的定制化处理。

  4. 集成性:Spring Cloud Gateway可以与其他Spring Cloud组件和微服务框架无缝集成,例如Eureka、Consul、Ribbon等,能够灵活地进行微服务的注册、发现和负载均衡。

总之,Spring Cloud Gateway是一个灵活、高性能的API网关服务,能够帮助开发者快速构建和部署微服务应用,实现请求路由、负载均衡、安全验证等功能。

相关推荐
眠りたいです4 小时前
基于脚手架微服务的视频点播系统-数据管理与网络通信部分的预备工作
c++·qt·ui·微服务·云原生·架构·媒体
虫小宝5 小时前
返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
分布式·缓存·架构
一水鉴天5 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之6 拼句 之1 (豆包助手 之8)
架构·认知科学
纪元A梦6 小时前
Redis最佳实践——安全与稳定性保障之高可用架构详解
redis·安全·架构
Dontla6 小时前
流行的前端架构与后端架构介绍(Architecture)
前端·架构
熊文豪7 小时前
KingbaseES读写分离集群架构解析
数据库·架构·kingbasees·金仓数据库·电科金仓
往事随风去7 小时前
别再纠结了!IM场景下WebSocket和MQTT的正确选择姿势,一文讲透!
后端·websocket·架构
爱读源码的大都督7 小时前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
一水鉴天8 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之 元宇宙:三种“即是”逻辑与数据安全措施的适配(豆包助手 之10)
架构·认知科学