众所周知,netflix OSS 2.0 难产了,上一代的zuul网关虽说不错,但其并不是异步的。所以,Spring团队推出了基于Spring Webflux的全新异步的网关–Spring Cloud Gateway。
本文内容基于Spring Cloud Gateway 2.1.0.GA
 
来跟着我一步步,探索它的魅力坑吧!
 
环境搭建 与所有的微服务组件一样,demo总是很简单的,如果你要启用,创建时勾上相关依赖即可。
就像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <dependencies >          <dependency >              <groupId > org.springframework.cloud</groupId >              <artifactId > spring-cloud-starter-gateway</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.cloud</groupId >              <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.cloud</groupId >              <artifactId > spring-cloud-starter-netflix-hystrix</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-configuration-processor</artifactId >              <optional > true</optional >          </dependency >          <dependency >              <groupId > org.projectlombok</groupId >              <artifactId > lombok</artifactId >              <optional > true</optional >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-test</artifactId >              <scope > test</scope >          </dependency >      </dependencies > 
 
其中lombok是我的习惯,你可以选择不添加。
启动类修改为@SpringCloudApplication。
1 2 3 4 5 6 @SpringCloudApplication public  class  SpringCloudGatewayApplication  {     public  static  void  main (String[] args)  {         SpringApplication.run(SpringCloudGatewayApplication.class, args);     } }
 
添加些简单配置(一个路由),跳转到我的博客,直接填写了url。由于只涉及网关,所以我把不必要的eureka关了,但实际开发中需要使用它,并添加ribbon路由lb://<service-name>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 spring:    cloud:      gateway:        routes:          -  id:  app            uri:  http://www.dnocm.com                                  predicates:              -  Path=/app/**            filters:                           -  StripPrefix=1              eureka:    client:      enabled:  false 
 
就此完成,启动运行。当你访问localhost:8080/app/**路由时,都会调整至www.dnocm.com/**。这是因为我设置了http一律302跳转至https。所以,这证明我们的网关搭建完成啦!!
Route Predicate Factory id uri顾名思义,不多说,但predicates是什么呢?predicates做动词有使基于; 使以…为依据; 表明; 阐明; 断言;的意思,简单说,用于表明在那种条件下,该路由配置生效。
官方提供给我了许多的predicates
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 spring:    cloud:      gateway:        routes:        -  id:  example          uri:  http://example.org          predicates:                   -  After=2017-01-20T17:42:47.789-07:00[America/Denver]                   -  Before=2017-01-20T17:42:47.789-07:00[America/Denver]                   -  Between=2017-01-20T17:42:47.789-07:00[America/Denver],  2017-01-21T17:42:47.789-07:00 [America/Denver ]                  -  Cookie=chocolate,  ch.p                   -  Header=X-Request-Id,  \d+                   -  Host=**.somehost.org,**.anotherhost.org                   -  Host={sub}.myhost.org                   -  Method=GET                   -  Path=/foo/{segment},/bar/{segment}                   -  Query=baz                   -  Query=foo,  ba.                   -  RemoteAddr=192.168.1.1/24 
 
官方几乎提供了我们所需的全部功能,这点值得鼓掌👏,然而假如遇到无法满足的情况呢?我们翻阅文档,发现自定义部分是大写的TBD待定ヾ(。`Д´。)。
那么怎么办呢?我们从官方的Predicate Factory看起,去学习。
挑个简单的HeaderRoutePredicateFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public  class  HeaderRoutePredicateFactory  extends  AbstractRoutePredicateFactory <HeaderRoutePredicateFactory.Config> {   public  static  final  String  HEADER_KEY  =  "header" ;   public  static  final  String  REGEXP_KEY  =  "regexp" ;   public  HeaderRoutePredicateFactory ()  {     super (Config.class);   }   @Override    public  List<String> shortcutFieldOrder ()  {     return  Arrays.asList(HEADER_KEY, REGEXP_KEY);   }   @Override    public  Predicate<ServerWebExchange> apply (Config config)  {     boolean  hasRegex  =  !StringUtils.isEmpty(config.regexp);     return  exchange -> {       List<String> values = exchange.getRequest().getHeaders()           .getOrDefault(config.header, Collections.emptyList());       if  (values.isEmpty()) {         return  false ;       }              if  (hasRegex) {                  return  values.stream().anyMatch(value -> value.matches(config.regexp));       }              return  true ;     };   }   @Validated    public  static  class  Config  {     @NotEmpty      private  String header;     private  String regexp;     public  String getHeader ()  {       return  header;     }     public  Config setHeader (String header)  {       this .header = header;       return  this ;     }     public  String getRegexp ()  {       return  regexp;     }     public  Config setRegexp (String regexp)  {       this .regexp = regexp;       return  this ;     }   } }
 
上面的例子,我们可以看出
HeaderRoutePredicateFactory的构造方式与继承类视乎是固定的,目的是传递配置类 
需要实现Predicate<ServerWebExchange> apply(Consumer<C> consumer) 
shortcutFieldOrder()似乎是为了配置值与配置类属性对应的 
需要定义接受的配置类 
查看继承可以发现,它通过NameUtils.normalizeRoutePredicateName(this.getClass())来获取配置文件中的名称 
 
Okay,验证上面的内容,我们重新编写一个NonHeaderRoutePredicateFactory,与head取反。同时配置类属性的交换位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public  class  NonHeaderRoutePredicateFactory  extends  AbstractRoutePredicateFactory <NonHeaderRoutePredicateFactory.Config> {   public  static  final  String  HEADER_KEY  =  "header" ;   public  static  final  String  REGEXP_KEY  =  "regexp" ;   public  NonHeaderRoutePredicateFactory ()  {     super (Config.class);   }   @Override    public  List<String> shortcutFieldOrder ()  {     return  Arrays.asList(HEADER_KEY, REGEXP_KEY);   }   @Override    public  Predicate<ServerWebExchange> apply (Config config)  {     boolean  hasRegex  =  !StringUtils.isEmpty(config.regexp);     return  exchange -> {       List<String> values = exchange.getRequest().getHeaders().getOrDefault(config.header, Collections.emptyList());       if  (values.isEmpty()) {         return  true ;       }       if  (hasRegex) {         return  values.stream().noneMatch(value -> value.matches(config.regexp));       }       return  false ;     };   }   @Data    @Validated    public  static  class  Config  {     private  String regexp;     @NotEmpty      private  String header;   } }
 
配置添加- NonHeader=tt,当存在ttheader时 404 ERROR,不存在时,正常访问。符合推测!
GatewayFilter Factory 除此predicates外,还有filter,用于过滤请求。与predicates一样,Spring官方也提供了需要内置的过滤器。过滤器部分相对于predicates来说难得多,有全局的也有可配置的。甚至一些过滤器不支持通过配置文件来修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 spring:    cloud:      gateway:        routes:        -  id:  example          uri:  http://example.org          filters:                                  -  AddRequestHeader=X-Request-Foo,  Bar            -  AddResponseHeader=X-Response-Foo,  Bar            -  RemoveRequestHeader=X-Request-Foo            -  RemoveResponseHeader=X-Response-Foo            -  RewriteResponseHeader=X-Response-Foo,  ,  password=[^&]+,  password=***            -  SetResponseHeader=X-Response-Foo,  Bar            -  PreserveHostHeader                       -  AddRequestParameter=foo,  bar                       -  PrefixPath=/mypath            -  RewritePath=/foo/(?<segment>.*),  /$\{segment}            -  SetPath=/{segment}            -  StripPrefix=2            -  SetStatus=BAD_REQUEST            -  SetStatus=401            -  RedirectTo=302,  http://acme.org                       -  SaveSession                       -  name:  RequestSize              args:                maxSize:  5000000                       -  name:  Retry              args:                retries:  3                statuses:  BAD_GATEWAY                                           -  Hystrix=myCommandName                       -  name:  Hystrix              args:                name:  fallbackcmd                fallbackUri:  forward:/incaseoffailureusethis                       -  name:  FallbackHeaders              args:                executionExceptionTypeHeaderName:  Test-Header                executionExceptionMessageHeaderName:  Test-Header                rootCauseExceptionTypeHeaderName:  Test-Header                rootCauseExceptionMessageHeaderName:  Test-Header                                                         -  name:  RequestRateLimiter              args:                redis-rate-limiter.replenishRate:  10                redis-rate-limiter.burstCapacity:  20            -  name:  RequestRateLimiter              args:                rate-limiter:  "#{@myRateLimiter}"                key-resolver:  "#{@userKeyResolver}" 
 
KeyResolver的实现参考
1 2 3 4 @Bean  KeyResolver userKeyResolver ()  {     return  exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user" )); }
 
除此外还有两个“特别”的过滤器,modifyRequestBody modifyResponseBody他们只能使用在Fluent Java Routes API中。例如:
1 2 3 4 5 6 7 8 9 @Bean public  RouteLocator routes (RouteLocatorBuilder builder)  {     return  builder.routes()         .route("rewrite_request_obj" , r -> r.host("*.rewriterequestobj.org" )             .filters(f -> f.prefixPath("/httpbin" )                 .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,                     (exchange, s) -> return  Mono.just(new  Hello (s.toUpperCase())))).uri(uri))         .build(); }
 
此外,这两个过滤器目前处在Beta中,不稳定。而且,Spring团队对于Body的处理十分愚蠢,我会在Others章节提及。
对于全局过滤器,目前系统提供的一般都用于支持基础功能。如负载均衡、路由转换、生成Response等等。对于我们来说,需要关心这些全局过滤器的顺序,毕竟他们与上面的过滤器会一同工作。
与predicates类似,filter也提供了自定义的能力,相对于鸡肋的predicate的自定义,filter显得有用的多。也可能因此,它居然有官方文档介绍(在predicate中是TBD)。我们可以使用它来完成权限的鉴定与下发,一个好的方案是,网关与客户端之间通过session保存用户的登录状态,在网关内,微服务间的沟通使用JWT来认证安全信息。那么我们需要由过滤器来完成这些工作,一个例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Configuration public  class  GenerateJwtGatewayFilterFactory  extends  AbstractGatewayFilterFactory <GenerateJwtGatewayFilterFactory.Config> {     @Resource      private  JwtProperties properties;     @Resource      private  JwtAuthServer jwtAuthServer;     public  GenerateJwtGatewayFilterFactory ()  {         super (Config.class);     }     @Override      public  String name ()  {         return  "GenerateJwt" ;     }     @Override      public  GatewayFilter apply (Config config)  {         String[] place = config.getPlace().split(":" );         return  (exchange, chain) -> {             return  Mono                     .defer(() -> {                         if  ("session" .equals(place[0 ])) {                             return  exchange.getSession().map(webSession -> {                                 return  webSession.getAttributes().getOrDefault(place[1 ], "" );                             });                         }                         if  ("query" .equals(place[0 ])) {                             String  first  =  exchange.getRequest().getQueryParams().getFirst(place[1 ]);                             return  Mono.justOrEmpty(first);                         }                         if  ("form" .equals(place[0 ])) {                                                          return  new  DefaultServerRequest (exchange).bodyToMono(new  ParameterizedTypeReference <MultiValueMap<String, String>>() {}).map(formData -> {                                 String  first  =  formData.getFirst(place[1 ]);                                 return  Optional.ofNullable(first).orElse("" );                             });                         }                         throw  new  BaimiException ("不支持的类型!" );                     })                     .filter(sub -> !StringUtils.isEmpty(sub))                     .map(sub -> jwtAuthServer.generate(config.getAudience(), config.getPrefix() + ":"  + sub))                     .map(token -> exchange.getRequest().mutate().header(properties.getHeaderName(), properties.getHeaderPrefix() + token).build())                     .map(req -> exchange.mutate().request(req).build())                     .then(chain.filter(exchange));         };     }     @Override      public  List<String> shortcutFieldOrder ()  {         return  Arrays.asList("place" , "audience" , "prefix" );     }     @Data      static  class  Config  {                  private  String  place  =  "session:user" ;                  private  String  audience  =  "system" ;                  private  String  prefix  =  "id" ;     } }
 
在配置文件中,直接使用,更多代码见下面参考中的项目源码。
1 2 3 4 5 6 7 8 spring:    cloud:      gateway:        routes:          -  id:  example            uri:  http://example.org            filters:              -  GenerateJwt=form:id,system,id 
 
全局的过滤器也能自定义,像下面一样
1 2 3 4 5 6 7 8 9 10 @Bean @Order(1) public  GlobalFilter c ()  {     return  (exchange, chain) -> {         log.info("third pre filter" );         return  chain.filter(exchange).then(Mono.fromRunnable(() -> {             log.info("first post filter" );         }));     }; }
 
Others Fluent Java Routes API 关于Java DSL,个人是极度不推荐使用。由于修改后需要重新打包部署。如果由配置文件决定,我们仅需修改配置文件,重新运行即可,程序会更加稳定,因为它仅提供功能给配置文件使用。
Request/Response Body 
IllegalStateException 问题范围为 Spring Cloud Gateway 2.0.0 至 2.1.1,1.x 理论上正常但未测试,2.1.2已修复。
 
关于Body,Spring对于其的操作是,在最初始化阶段,读取Body内容放入Flux流中。之后都是对其操作。详细可以看下AdaptCachedBodyGlobalFilter全局过滤器的源码。
似乎没什么问题是吧,我们就应该在这个操作流内不断的修改Body的内容,直至其被最终消费(转发)。但是当我们在过滤中使用exchange.getRequest().getBody()或者exchange.getFormData()之后,我们期望后续Spring是读取我们所产生的流,然而事实上,它仍然产生调用getBody()获取最初的流。流是线性的,已消费过的不能再次被消费!所以,我们无法方便的使用它达到我们的目的(当然Java DSL内有提供内置的过滤器,但我不推荐Java DSL本身)。
对此,我们有两种方案解决这个问题
处理完成后的流放入Request/Response中,以便其后续的消费 
修改getBody()的行为,缓存body内容,且每次生成新的流支持后续操作 
 
由于Request/Response对应的Builder不支持放入Body,所有,方案一每次都需要重新构建Body解码器,就像modifyRequestBody做的一样。。。在不需要修改Body的内容的前提(大部分都是这样的)下,方案二我们可以写成通用的Factory,在适当的位置添加即可,显得更加可操作。
下面是一个filter,用于支持RequestBody的缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Configuration public  class  CacheRequestGatewayFilterFactory  extends  AbstractGatewayFilterFactory <CacheRequestGatewayFilterFactory.Config> {     public  CacheRequestGatewayFilterFactory ()  {         super (Config.class);     }     @Override      public  String name ()  {         return  "CacheRequest" ;     }     @Override      public  GatewayFilter apply (Config config)  {         CacheRequestGatewayFilter  cacheRequestGatewayFilter  =  new  CacheRequestGatewayFilter ();         Integer  order  =  config.getOrder();         if  (order == null ) {             return  cacheRequestGatewayFilter;         }         return  new  OrderedGatewayFilter (cacheRequestGatewayFilter, order);     }     public  static  class  CacheRequestGatewayFilter  implements  GatewayFilter  {         @Override          public  Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain)  {                          HttpMethod  method  =  exchange.getRequest().getMethod();             if  (method == null  || method.matches("GET" ) || method.matches("DELETE" )) {                 return  chain.filter(exchange);             }             return  DataBufferUtils.join(exchange.getRequest().getBody())                     .map(dataBuffer -> {                         byte [] bytes = new  byte [dataBuffer.readableByteCount()];                         dataBuffer.read(bytes);                         DataBufferUtils.release(dataBuffer);                         return  bytes;                     })                     .defaultIfEmpty(new  byte [0 ])                     .flatMap(bytes -> {                         DataBufferFactory  dataBufferFactory  =  exchange.getResponse().bufferFactory();                         ServerHttpRequestDecorator  decorator  =  new  ServerHttpRequestDecorator (exchange.getRequest()) {                             @Override                              public  Flux<DataBuffer> getBody ()  {                                 if  (bytes.length > 0 ) {                                     return  Flux.just(dataBufferFactory.wrap(bytes));                                 }                                 return  Flux.empty();                             }                         };                         return  chain.filter(exchange.mutate().request(decorator).build());                     });         }     }     @Override      public  List<String> shortcutFieldOrder ()  {         return  Collections.singletonList("order" );     }     @Data      static  class  Config  {         private  Integer order;     } }
 
配置文件添加CacheRequest,用于添加过滤器(如果不加,从form中读取数据是会报错的)
1 2 3 4 5 6 7 8 9 10 11 spring:    cloud:      gateway:        routes:          -  id:  jwt            uri:  lb://app-name            predicates:              -  Path=/jwt/**            filters:              -  CacheRequest              -  GenerateJwt=form:id,system,id 
 
当然,exchange.getFormData()的问题没有解决,需要对Body操作,请使用exchange.getRequest().getBody()
在下方 issues:946 提了简化操作的建议,然后官方添加了相关Cache方法,然后发现不使用这个方法也不出问题。。。问题原因就是AdaptCachedBodyGlobalFilter对body解码器的封装,默认情况下2.1.2中不做处理,所以好了。。。
 
参考