1. Predicate机制

Predicate是Java8中引入的一个新功能,和我们平时写单元测试的时候Assertion差不多,Predicate是接收一个判断条件,返回一个ture或false的布尔值结果,告知调用发判断结果。也可以通过and、or和negative(非)三个操作符多个Predicate串联在一块共同判断

Predicate其实就是我们和Gateway对接的数据暗号,比如要求你的Request中必须带有某个指定的参数叫name,对应的值必须是一个指定的人名(Gavin),如果Request中没有包含name,或者名字不是Gavin,断言就失败了,只有标示和值都一样的情况下才会通过

2. 断言的作用阶段

一个请求在抵达网关层后,首先就要进行断言匹配,在满足所有断言之后才会进入Filter阶段

3. 常用断言介绍

Gateway提供了十多种内置断言,介绍一些常用的

3.1. 路径断言

Path断言是最常用的一个断言请求,几乎所有的路由都要用到

.route(r -> r.path("/gateway/**")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
.route(r -> r.path("/baidu")
						 .uri("http://www.baidu.com")
)

Path断言的使用非常简单,就像我们在Controller中配置@RequestPath的方式一样,在Path断言中填入一段URL匹配规则,当实际请求的URL和断言中的规则相匹配的时候,就下发到该路由中URI指定地址,这个地址可以是一个具体的HTTP地址,也可以是一个Eureka中注册的服务名称,路由规则可以一次编写多个绑定关系

3.2. Method断言

这个断言是专门验证HTTP Method的

.route(r -> r.path("/gateway/**")
						 .and().method(HttpMethod.GET)
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

将Path断言通过一个and连接符和method关联起来,我们如果访问/gateway/sample并且method是GET时才会适配上面的路由规则

3.3. RequestParam匹配

请求断言也是在业务中经常使用的,他会从ServerHttpRequest中的Parameters列表中查询指定的属性,

.route(r -> r.path("/gateway/**")
						 .and().method(HttpMethod.GET)
						 .and().query("name","test")
						 .and().query("age")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
  • 属性名验证,如query(“age”),此时断言只会验证QueryParameters列表中是否包含了一个叫age的属性,并不会验证他的值
  • 属性值验证,如query(“name”,“test”),它不仅会验证name属性是否存在,还会验证他的值是不是和断言相匹配,不会当前断言会验证参数中name的属性值是不是test

3.4. Header断言

header断言是检查头信息里是否携带了相关属性或令牌

.route(r -> r.path("/gateway/**")
						 .and().header("Authorization")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

3.5. Cookie断言

cookie验证的是cookie中保存的信息,cookie断言和上面介绍的几种断言方式都大同小异,唯一不同的是他必须连同属性值一起验证,不能单独只验证属性是否存在

.route(r -> r.path("/gateway/**")
						 .and().cookie("name","test")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

3.6. 时间片验证

时间匹配有三种模式,分别是Before、After和Between,这些断言指定了在什么时间范围内路由才会生效

.route(r -> r.path("/gateway/**")
						 .and().before(ZonedDateTime.now().plusMinutes(1))
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

4. 实现断言的配置

4.1. 在yaml里进行配置

去到gateway-server项目的yaml配置文件里进行配置

# 新的配置routes和discovery是平级的
# id是这个断言的唯一标识
# uri是如果匹配上所有断言,请求将转发到这里
# StripPrefix相当于把 localhost:50080/gavinrouter/sayhello替换成 FEIGN-CLIENT/sayhello
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
      - id: feignclient
        uri: lb://FEIGN-CLIENT
        predicates:
        - Path=/gavinrouter/**
        filters:
        - StripPrefix=1

配置完成后可以通过下面的路径访问

http://localhost:50080/gavinrouter/sayhello

4.1. 在Java程序里进行配置

创建一个config包,在里面创建GatewayConfiguration

package com.icodingedu.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/gatewayjava/**")
                             .and().method(HttpMethod.GET)
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}

修改后进行访问验证:http://localhost:50080/gavinjava/sayhello

5. After断言实现网关层秒杀

gateway调用的是feign-client的业务,我们就要到feign-client里创建一个controller实现相应的功能

这里面使用的product需要提前在feign-client-intf中定义好

package com.icodingedu.springcloud.pojo;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Product {

    private Long productId;
    private String description;
    private Long stock;
}

在feign-client中创建一个GatewayController

package com.icodingedu.springcloud.controller;

import com.icodingedu.springcloud.pojo.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@Slf4j
@RequestMapping("gateway")
public class GatewayController {

    //Product要在feign-client-intf里提前定义好
    public static final Map<Long, Product> items = new ConcurrentHashMap<>();

    @GetMapping("detail")
    public Product getProduct(Long pid){
        //如果第一次没有就先创建一个
        if(!items.containsKey(pid)){
            Product product = Product.builder().productId(pid)
                    .description("new arrival")
                    .stock(100L).build();
            items.putIfAbsent(pid,product);
        }
        return items.get(pid);
    }

    @GetMapping("placeOrder")
    public String buy(Long pid){
        Product product = items.get(pid);
        if(product==null){
            return "Product Not Found";
        }else if(product.getStock()<=0L){
            return "Sold Out";
        }
        //如果是单体应用,即便是集群也可以保留这个代码,集群解决需要用到分布式锁将控制放到中心节点即可
        synchronized (product){
            if(product.getStock()<=0L){
                return "Sold Out";
            }
            product.setStock(product.getStock()-1);
        }
        return "Order Placed";
    }
}

回到Gateway-server项目里,按照时间顺延的方式做断言定义

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/gavinjava/**")
                             .and().method(HttpMethod.POST)
                             .and().query("name","gavin")
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.now().plusSeconds(30))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT")
                )
                .build();
    }
}

可以精确的定义时间节点

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        LocalDateTime ldt = LocalDateTime.of(2020,10,24,20,31,10);
        return builder.routes()
                .route(r -> r.path("/gavinjava/**")
                             .and().method(HttpMethod.POST)
                             .and().query("name","gavin")
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT")
                )
                .build();
    }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐