1、Zuul

我们已经知道,在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请
求,客户端可能通过调用N个微服务的接口完成一个用户请求。比如:用户查看一个商品的信息,它可能包含商品
基本信息、价格信息、评论信息、折扣信息、库存信息等等,而这些信息获取则来源于不同的微服务,诸如产品系
统、价格系统、评论系统、促销系统、库存系统等等,那么要完成用户信息查看则需要调用多个微服务,这样会带
来几个问题:
    1、客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
    2、认证繁杂,访问每个服务都要进行一次认证
    3、每个服务都通过http访问,导致http请求增加,效率不高拖慢系统性能
    4、多个服务存在跨域请求问题,处理起来比较复杂

我们该如何解决这些问题呢?我们可以尝试想一下,不要让前端直接知道后台诸多微服务的存在,我们的系统本身
就是从业务领域的层次上进行划分,形成多个微服务,这是后台的处理方式。对于前台而言,后台应该仍然类似于
单体应用一样,一次请求即可,于是我们可以在客户端和服务端之间增加一个API网关,所有的外部请求先通过这
个微服务网关。它只需跟网关进行交互,而由网关进行各个微服务的调用。

1、基本使用

1、新建项目,添加pom依赖
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
     </dependency>

2、编写配置文件
server:
  port: 9001

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
  instance:
    prefer-ip-address: true
    instance-id: gateway-zuul-9001

spring:
  application:
    name: gateway-zuul-9001
zuul:
  routes:
    #路径映射,访问9001下的provider/**就会转发给provider-payment-service服务
    provider-payment-service: /provider/**  
  #忽略所有的直接以服务名调用的请求 
  ignored-services: "*"
  #以什么开头,http://localhost:9001/zuul/provider/xxx
  prefix: /zuul
  #默认路径是/zuul
  servlet-path: /

2、网关过滤器

Zuul提供的过滤器是一个父类。父类是ZuulFilter。通过父类中定义的抽象方法filterType,来决定当前的
Filter种类是什么。有前置过滤、路由后过滤、后置过滤、异常过滤。
    1、pre:在请求发出之前执行过滤,如果要进行访问,肯定在请求前设置头信息
    2、route:在进行路由请求的时候被调用;
    3、post:在路由之后发送请求信息的时候被调用;
    4、error:出现错误之后进行调用

在父类中提供了4个抽象方法,分别是:filterOrder, shouldFilter, run。其功能分别是:
filterOrder:返回int数据,用于为同filterType的多个过滤器定制执行顺序,返回值越小,执行顺序越优先。
shouldFilter:返回boolean数据,代表当前filter是否生效。
run:具体的过滤执行逻辑。如pre类型的过滤器,可以通过对请求的验证来决定是否将请求路由到服务上;        如post类型的过滤器,可以对服务响应结果做加工处理(如为每个响应增加footer数据)。
1、新建类继承FallbackProvider,实现两个方法,加@Component

2、主要实现fallbackResponse方法
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause)

3、根据不同的异常来返回不同的信息
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("*****fallbackResponse,route:"+route);
        if(cause instanceof NullPointerException){
            List<Map<String, Object>> result = new ArrayList<>();
            Map<String, Object> data = new HashMap<>();
            data.put("message", "网关超时,请稍后重试");
            result.add(data);

            ObjectMapper mapper = new ObjectMapper();

            String msg = "";
            try {
                msg = mapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                msg = "";
            }
            return this.executeFallback(HttpStatus.GATEWAY_TIMEOUT,
                    msg, "application", "json", "utf-8");
        }else{
            return this.fallbackResponse();
        }
    }

    public ClientHttpResponse fallbackResponse() {
        System.out.println("ClientHttpResponse fallbackResponse()");
        List<Map<String, Object>> result = new ArrayList<>();
        Map<String, Object> data = new HashMap<>();
        data.put("message", "服务正忙,请稍后重试");
        result.add(data);

        ObjectMapper mapper = new ObjectMapper();

        String msg = "";
        try {
            msg = mapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            msg = "";
        }
        return this.executeFallback(HttpStatus.OK, msg,
                "application", "json", "utf-8");
    }

    /**
     * 具体处理过程。
     * @param status 容错处理后的返回状态,如200正常GET请求结果,201正常POST请求结果,404资源
找不到错误等。
     *  使用spring提供的枚举类型对象实现。HttpStatus
     * @param contentMsg 自定义的响应内容。就是反馈给客户端的数据。
     * @param mediaType 响应类型,是响应的主类型, 如: application、text、media。
     * @param subMediaType 响应类型,是响应的子类型, 如: json、stream、html、plain、jpeg、png等。
     * @param charsetName 响应结果的字符集。这里只传递字符集名称,如: utf-8、gbk、big5等。
     * @return ClientHttpResponse 就是响应的具体内容。
     *  相当于一个HttpServletResponse。
     */
    private final ClientHttpResponse executeFallback(final HttpStatus status,
                                                     String contentMsg, String mediaType, String subMediaType, String charsetName) {
        return new ClientHttpResponse() {
            /**
             * 设置响应的头信息
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders header = new HttpHeaders();
                MediaType mt = new MediaType(mediaType, subMediaType, Charset.forName(charsetName));
                header.setContentType(mt);
                return header;
            }

            /**
             * 设置响应体
             * zuul会将本方法返回的输入流数据读取,并通过HttpServletResponse的输出流输出到客户端。
             */
            @Override
            public InputStream getBody() throws IOException {
                String content = contentMsg;
                return new ByteArrayInputStream(content.getBytes());
            }

            /**
             * ClientHttpResponse的fallback的状态码 返回String
             */
            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            /**
             * ClientHttpResponse的fallback的状态码 返回HttpStatus
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

            /**
             * ClientHttpResponse的fallback的状态码 返回int
             */
            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            /**
             * 回收资源方法
             * 用于回收当前fallback逻辑开启的资源对象的。
             * 不要关闭getBody方法返回的那个输入流对象。
             */
            @Override
            public void close() {
            }
        };
    }

2、Gateway

1、基本使用

1、新建项目,添加pom依赖,不能加入web模块
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-gateway</artifactId>
     </dependency>

2、编写配置文件
spring:
  application:
    name: gateway-gateway-9001
  cloud:
    gateway:
      routes:
        - id: payment_get #payment_route     #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001        #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**           #断言,路径相匹配的进行路由

3、启动Eureka,8001和本项目

4、访问路径http://localhost:xxx/payment/get/xxx即可

2、动态路由

1、将8002也加入服务

2、修改配置文件
spring:
  application:
    name: gateway-gateway-9001
  cloud:
    gateway:
      #开启动态路由,根据服务名调用
      discovery:
        locator:
          enabled: true
      routes:
        - id: payment_get #payment_route     #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://provider-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**           # 断言,路径相匹配的进行路由

3、常用的断言

1、Path
    符合路径匹配即可通过,- Path=/payment/get/**

2、After
    在某一时刻后访问为true,之前为false,- After=2020-06-21T18:25:10.016+08:00[Asia/Shanghai]

3、Before
    在某一时刻前访问为true,之后为false,- Before=2020-06-28T18:25:10.016+08:00[Asia/Shanghai]

4、Between
    在两个时间之间为true,- Between=2020-06-21T18:25:10.016+08:00[Asia/Shanghai],2020-06-
28T18:25:10.016+08:00[Asia/Shanghai]

5、Cookie
    匹配请求中存在Cookie中符合规则的,- Cookie=chocolate, value,存在cookie中chocolate=value,其中value是正则表达式

6、Header
    匹配请求中携带符合的请求头的请求,- Header=X-Request-Id, \d+的作用是:匹配请求头中存在符合条件
“key为X-Request-Id,value为数字”的所有请求,\d+是正则表达式

7、Query
    匹配请求中携带符合的参数的请求,- Query=username,value的作用是: 匹配所有含有请求参数username且它的值符合正则表达式value的请求,value是正则表达式,可以省略

8、Method
    匹配请求中请求方式符合的请求,- Method=GET的作用是:匹配所有是GET请求的请求

9、Host
    匹配所有主机符合的请求,- Host=**.somehost.org:例如www.somehost.org,aaa.somehost.org

10、RemoteAddr
    匹配所有ip符合的请求,- RemoteAddr=192.168.1.1/24:匹配对应的ip地址

11、如果在一个路由转发的地方设置多个断言,需要所有的断言成功才会进行路由转发

4、过滤器

1、新建类,实现两个接口,在filter里进行鉴权或校验
/**
 * Description:
 * Author:逗你妹
 * Date:2020/6/21
 */
@Component
@Slf4j
public class GatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("***********come in MyLogGateWayFilter:  "+new Date());
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        if(StringUtils.isEmpty(username)){
            log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
            exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

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

5、代码路由转发

1、路由转发有两种方式,第一种就是上述演示的配置文件配置,另外一种就是java类的配置

2、新建一个配置类,添加多个RouteLocator实例即可

/**
 * Description:
 * Author:逗你妹
 * Date:2020/6/21
 */
@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator routeLocator1(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("baidu_guonei",
                f -> f.path("/guonei")
                        .uri("http://news.baidu.com/guonei"));
        return routes.build();
    }

    @Bean
    public RouteLocator routeLocator2(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("baidu_guoji",
                f -> f.path("/guoji")
                        .uri("http://news.baidu.com/guoji"));
        return routes.build();
    }

}