Spring Cloud Gateway从注册中心自动注册配置路由信息

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

环境信息

Spring Boot2.0.8.RELEASE

Spring Boot内置的tomcattomcat-embed-core 8.5.37

Spring Cloud Gateway2.0.4.RELEASE

Nacos2.0.4.RELEASE

需求

Spring Cloud Gateway注册到注册中心(这里是Nacos其它注册中心也一样如EurekaConsulZookeeper自动从注册中心里获取注册上去的服务并使用负载均衡策略将对应的请求路由到对应的服务上。

目前注册到注册中心的服务服务名和服务的根路径是一样的比如服务名service-a根路径是/service-a。

访问路径规则http://网关ip端口/目标服务名/目标服务具体路径

自动重定向到http://目标服务ip端口/目标服务名/目标服务具体路径

访问网关的路径http://localhost:8000/service-a/logSetting/getLogLevel

直接访问目标服务service-a的路径http://localhost:6001/service-a/logSetting/getLogLevel

分析

Spring Cloud Gateway官方提供了从注册中心自动获取服务并注册路由信息的功能前提是gateway网关工程注册到了注册中心(如Eureka、Consul、Nacos、Zookeeper等

官方文档Spring Cloud Gateway

这种自动注册的路由信息

断言predicates是Path规则是"'/'+serviceId+'/**'"即匹配服务名

过滤器filters是RewritePath规则是

regexp: "'/' + serviceId + '/(?<remaining>.*)'"

replacement: "'/${remaining}'"

比如

访问网关的路径http://localhost:8000/service-a/logSetting/getLogLevel

会重定向到http://localhost:6001/logSetting/getLogLevel

和需求相比目标路径少了/service-a会导致无法访问到目标服务报错404

因此需要设置自定义规则用来代替默认的规则。

以下是官方文档里的说明、示例

By default, the gateway defines a single predicate and filter for routes created with a DiscoveryClient.
The default predicate is a path predicate defined with the pattern /serviceId/**, where serviceId is the ID of the service from the DiscoveryClient.
The default filter is a rewrite path filter with the regex /serviceId/(?<remaining>.*) and the replacement /${remaining}. This strips the service ID from the path before the request is sent downstream.
If you want to customize the predicates or filters used by the DiscoveryClient routes, set spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y]. When doing so, you need to make sure to include the default predicate and filter shown earlier, if you want to retain that functionality. The following example shows what this looks like:

Example 71. application.properties

spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
spring.cloud.gateway.discovery.locator.predicates[1].name: Host
spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"

但是要注意

  1. 官网上的配置的格式有问题不能直接使用。

  1. 使用了自定义配置之后默认的配置就不会生效了。如果需要和默认配置一样的功能那么需要手动配置。

如果是applicaton.properties格式的配置文件那么和默认配置一样的功能的话应该这样子配置注意args[pattern]等的值不能有双引号(")这些值是SPEL表达式。

# 断言配置
spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: '/api/'+serviceId+'/**'
# 过滤器设置
spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/${remaining}'

如果是application.yml格式的配置文件的话

注意args的pattern、regex等的值要有双引号(")这些值是SPEL表达式。

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true
          predicates:
            - name: Path
              args:
                pattern: "'/'+serviceId+'/**'"
          filters:
            - name: RewritePath
              args:
                regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
                replacement: "'/' + '/${remaining}'"

实现

官方文档Spring Cloud Gateway

  1. 开启自动注册功能

在application.properties里配置属性

# 开启自动注册
spring.cloud.gateway.discovery.locator.enabled=true
# serviceId使用小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
  1. 配置自定义规则。

application.properties版主要是配置了路径重写过滤器不将serviceId过滤掉。注意不要有双引号

spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/' + serviceId + '/${remaining}

其它常见需求

  1. 手动注册路由信息

如果需要手动注册路由信息而且访问路径还是和自动注册的一样(即http://网关ip端口/目标服务名/目标服务具体路径那么需要设置路由的优先级order比0小因为自动注册的路由信息优先级order是0order值越小优先级越高。如果没配置order默认是0但是手动注册的路由信息优先级会低于自动注册的具体原因后面说。

spring.cloud.gateway.routes[0].id=service-a
spring.cloud.gateway.routes[0].uri=http://localhost:6001/service-a,http://localhost:16001/service-a
spring.cloud.gateway.routes[0].predicates[0]=Path=/service-a/**
spring.cloud.gateway.routes[0].order=-1
  1. 配置自动注册的服务

如果需要从注册中心里自动注册路由但是想要排除一些不需要暴露给网关的服务那么可以使用一下方式来排除

spring.cloud.gateway.discovery.locator.includeExpression=!serviceId.contains('service-b')

扩展includeExpression的值是SPEL表达式serviceId是注册中心里的服务id。

表达式可以用contains、eq等等来实现不同的筛选。

这个属性对应的是DiscoveryLocatorProperties类

@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {
    private boolean enabled = false;
    private String routeIdPrefix;
    private String includeExpression = "true";
    private String urlExpression = "'lb://'+serviceId";
    private boolean lowerCaseServiceId = false;
    private List<PredicateDefinition> predicates = new ArrayList();
    private List<FilterDefinition> filters = new ArrayList();
    // 具体代码略...
}
  1. 配置全局的过滤器

spring:
  cloud:
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Vary, RETAIN_UNIQUE
  1. 配置全局的cors

spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowedOrigin: "*"
            allowCredentials: true
            maxAge: 360000

完整版的application.properties

# 开启自动注册
spring.cloud.gateway.discovery.locator.enabled=true
# serviceId使用小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true

# 过滤器设置
spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/' + serviceId + '/${remaining}'

# 自定义路由信息
spring.cloud.gateway.routes[0].id=service-a
#spring.cloud.gateway.routes[0].uri=lb://service-a
spring.cloud.gateway.routes[0].uri=http://localhost:6001/service-a,http://localhost:16001/service-a
spring.cloud.gateway.routes[0].predicates[0]=Path=/service-a/**
spring.cloud.gateway.routes[0].order=-1

spring.cloud.gateway.default-filters[0]=DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Vary, RETAIN_UNIQUE

application.yml

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true
          predicates:
            - name: Path
              args:
                pattern: "'/'+serviceId+'/**'"
          filters:
            - name: RewritePath
              args:
                regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
                replacement: "'/' + serviceId + '/${remaining}'"
      routes:
        - id: service-a
#          uri: lb://service-a
          uri: http://localhost:6001/service-a,http://localhost:16001/service-a
          predicates:
            - Path=/tfb-biz-common-service-app/**
          order: -1

源码解析

配置相关类

  1. GatewayDiscoveryClientAutoConfiguration

主要是根据属性spring.cloud.gateway.discovery.locator.enabled=true开启自动注册并初始化自动注册的默认断言、过滤器

  1. DiscoveryLocatorProperties

@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {
    private boolean enabled = false;
    private String routeIdPrefix;
    private String includeExpression = "true";
    private String urlExpression = "'lb://'+serviceId";
    private boolean lowerCaseServiceId = false;
    private List<PredicateDefinition> predicates = new ArrayList();
    private List<FilterDefinition> filters = new ArrayList();

    public DiscoveryLocatorProperties() {
    }// 59

GatewayDiscoveryClientAutoConfiguration类里的discoveryLocatorProperties()初始化了默认配置

    @Bean
    public DiscoveryLocatorProperties discoveryLocatorProperties() {
        DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();// 66
        properties.setPredicates(initPredicates());// 67
        properties.setFilters(initFilters());// 68
        return properties;// 69
    }

DiscoveryLocatorProperties初始化的时候又因为@ConfigurationProperties("spring.cloud.gateway.discovery.locator")会加载配置文件里的spring.cloud.gateway.discovery.locator对应的属性从而覆盖掉默认配置。

上面常见需求里说的用includeExpression来设置需要注册或不注册的服务就是在这个类里的属性。

RouteDefinitionRouteLocator

  1. RouteDefinitionLocator

路由定义定位器接口有多个实现类

CachingRouteDefinitionLocator
CompositeRouteDefinitionLocator
DiscoveryClientRouteDefinitionLocator
InMemoryRouteDefinitionRepository
PropertiesRouteDefinitionLocator
RouteDefinitionRepository
  1. DiscoveryClientRouteDefinitionLocator

自动注册的路由信息具体是在这里设置的。

网关请求的流程

路由入口org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute

RoutePredicateHandlerMapping

接着访问org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes

这里的routeDefinitionLocator是CompositeRouteDefinitionLocator对象

是一个代理对象包含了一个List

iterable = {ArrayList@9085} size = 3
0 = {DiscoveryClientRouteDefinitionLocator@9087}
1 = {PropertiesRouteDefinitionLocator@9088}
2 = {InMemoryRouteDefinitionRepository@9089}

由此也可以看出先使用自动注册的路由信息再使用配置文件里配置的路由信息。

这三个路由定义定位器都是实现了RouteDefinitionLocator接口

DiscoveryClientRouteDefinitionLocator这个是从注册中心里主动注册的路由定义定位器

里面有个属性是DiscoveryLocatorProperties在getRouteDefinitions()里有对于该属性的详细使用方式比如includeExpression属性、lowerCaseServiceId属性等

PropertiesRouteDefinitionLocator是基于属性的路由定义定位器

public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
    private final GatewayProperties properties;

    public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
        this.properties = properties;// 33
    }// 34

    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(this.properties.getRoutes());// 38
    }
}

属性GatewayProperties

@ConfigurationProperties("spring.cloud.gateway")
@Validated
public class GatewayProperties {
    private final Log logger = LogFactory.getLog(this.getClass());
    @NotNull
    @Valid
    private List<RouteDefinition> routes = new ArrayList();
    private List<FilterDefinition> defaultFilters = new ArrayList();
    private List<MediaType> streamingMediaTypes;

    public GatewayProperties() {
        this.streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);// 55
    }
    // 省略
}

RouteDefinition是路由定义信息不管是注册中心自动配置的还是通过属性文件配置的或者通过代码方式配置的

@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;
    // 省略
}

回到入口RoutePredicateHandlerMapping#lookupRoute里的具体代码

获取到路由信息之后会调用路由信息的断言predicate的apply(T t)方法来判断该请求是否满足断言。如果满足那么就会使用该路由信息来进行路由转发。

比如使用路径断言(predicate的name是Path对应的是PathRoutePredicateFactory

RouteDefinitionRouteLocator#getRoutes这里还调用了convertToRoute()里面调用了getFilters这个是过滤器

    public Flux<Route> getRoutes() {
        return this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute).map((route) -> {// 109 110 112
            if (this.logger.isDebugEnabled()) {// 113
                this.logger.debug("RouteDefinition matched: " + route.getId());// 114
            }

            return route;// 116
        });
    }

    private Route convertToRoute(RouteDefinition routeDefinition) {
        AsyncPredicate<ServerWebExchange> predicate = this.combinePredicates(routeDefinition);// 127
        List<GatewayFilter> gatewayFilters = this.getFilters(routeDefinition);// 128
        return ((AsyncBuilder)Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters)).build();// 130 131 132 133
    }

后续的处理会调用到各个过滤器

RouteDefinitionRouteLocator.loadGatewayFilters()重点在于调用了GatewayFilter gatewayFilter = factory.apply(configuration);

    private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
        List<GatewayFilter> filters = (List)filterDefinitions.stream().map((definition) -> {// 138 139
            GatewayFilterFactory factory = (GatewayFilterFactory)this.gatewayFilterFactories.get(definition.getName());// 140
            if (factory == null) {// 141
                throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());// 142
            } else {
                Map<String, String> args = definition.getArgs();// 144
                if (this.logger.isDebugEnabled()) {// 145
                    this.logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());// 146
                }

                Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);// 149
                Object configuration = factory.newConfig();// 151
                ConfigurationUtils.bind(configuration, properties, factory.shortcutFieldPrefix(), definition.getName(), this.validator);// 153 154
                GatewayFilter gatewayFilter = factory.apply(configuration);// 156
                if (this.publisher != null) {// 157
                    this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));// 158
                }

                return gatewayFilter;// 160
            }
        }).collect(Collectors.toList());// 162
        ArrayList<GatewayFilter> ordered = new ArrayList(filters.size());// 164

        for(int i = 0; i < filters.size(); ++i) {// 165
            GatewayFilter gatewayFilter = (GatewayFilter)filters.get(i);// 166
            if (gatewayFilter instanceof Ordered) {// 167
                ordered.add(gatewayFilter);// 168
            } else {
                ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));// 171
            }
        }

        return ordered;// 175
    }

比如说常见的路径重写过滤器RewritePathGatewayFilterFactory。

可以看到这里有两个参数regexp和replacement

public class RewritePathGatewayFilterFactory extends AbstractGatewayFilterFactory<RewritePathGatewayFilterFactory.Config> {
    public static final String REGEXP_KEY = "regexp";
    public static final String REPLACEMENT_KEY = "replacement";

    public RewritePathGatewayFilterFactory() {
        super(RewritePathGatewayFilterFactory.Config.class);// 38
    }// 39

    public List<String> shortcutFieldOrder() {
        return Arrays.asList("regexp", "replacement");// 43
    }

    public GatewayFilter apply(RewritePathGatewayFilterFactory.Config config) {
        String replacement = config.replacement.replace("$\\", "$");// 48
        return (exchange, chain) -> {// 49
            ServerHttpRequest req = exchange.getRequest();// 50
            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, req.getURI());// 51
            String path = req.getURI().getRawPath();// 52
            String newPath = path.replaceAll(config.regexp, replacement);// 53
            ServerHttpRequest request = req.mutate().path(newPath).build();// 55 56 57
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, request.getURI());// 59
            return chain.filter(exchange.mutate().request(request).build());// 61
        };
    }
    // 省略
}

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring