乐趣区

关于java:看完就会的Spring-Cloud-Gateway

在后面几节,我给大家介绍了当一个零碎拆分成微服务后,会产生的问题与解决方案:服务如何发现与治理(Nacos 注册核心实战),服务与服务如何通信(Ribbon,Feign 实战)

明天咱们就来聊一聊另一个问题:客户端如何拜访?

在单体架构时,咱们的零碎只有一个入口,前端人员调用起来非常的简略。

然而当咱们拆分为一个微服务零碎后,每个服务都有属于本人 ip 和端口号,咱们不可能跟前端说:诶,调用这个接口的时候你就应用这个地址哈。

前端:

既然这样不行的话,那咱们能不能利用已有的常识想一个解决方案呢?

不是真的能用的解决方案

其实咱们很容易的就能想到,咱们的服务是具备相互发现及通信的能力的,那么,咱们是不是能够搞一个相似对立入口(网关)样的服务,前端只申请这个服务,由这个服务去调用实在服务的 Feign 接口。

举个例子:

  • 商品服务的获取商品接口:localhost:8080/get/goods
  • 订单服务的下订单接口:localhost:8081/order

当初有个网关服务, 外面有两个接口:localhost:5555/get/goods,localhost:5555/order

前端调用获取商品接口时,拜访:localhost:5555/get/goods,而后网关服务调用商品服务的 Feign 接口

下单时:拜访:localhost:5555/order,而后网关服务调用订单服务的 Feign 接口

小结一下:

这个计划是否解决了服务入口对立的问题:解决了

能用吗:能用,但不是齐全能用

因为这样会有一个问题,服务写的每一个接口,都须要给出一个 Feign 接口,给咱们的网关服务调用。

真正的解决方案

Spring Cloud 为咱们提供了一个解决方案:Spring Cloud Gateway

Spring Cloud Gateway 提供了一个建设在 Spring 生态系统之上的 API 网关,可能简略而无效的形式来路由到 API,并基于 Filter 的形式提供一些性能,如:平安、监控。

Spring Cloud Gateway 是由 Spring Boot 2.x、Spring WebFlux 和 Reactor 实现的,须要 Spring Boot 和 Spring Webflux 提供的 Netty 运行环境。它不能在传统的 Servlet 容器中工作,也不能在以 WAR 模式构建时工作。

官网文档:https://docs.spring.io/spring…

概念

Route(路由):网关的根本构件,它由一个 ID、一个目的地 URI、一个断言汇合和一个过滤器汇合定义。如果汇合断言为真,则路由被匹配。

Predicate(断言):Java 8 断言函数。参数类型是 Spring Framework ServerWebExchange。能够让开发者在 HTTP 申请中的任何内容上进行匹配,比方头文件或参数。

Filter(过滤):由特定的工厂构建的 GatewayFilter 的实例,与传统的 Filter 一样,可能申请前后对申请就行解决。

工作原理

客户端向 Spring Cloud Gateway 发出请求。如果 Gateway 处理程序映射确定一个申请与路由相匹配,它将被发送到 Gateway Web 处理程序。这个处理程序通过一个特定于该申请的过滤器链来运行该申请。

过滤器能够在代理申请发送之前和之后运行 pre 和 post 逻辑。

简略应用

筹备

事后筹备一个服务,用来测试路由

我这里筹备了个一个商品服务,并提供了一个接口:http://localhost:8082/goods/g…

当初,开始编写网关服务

引入依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

编写配置

bootstrap.yaml

server:
  port: 5555
spring:
  application:
    name: my-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: public
        username: nacos
        password: nacos

logging:
  level:
    org.springframework.cloud.gateway: info
    com.alibaba.nacos.client.naming: warn

application.yaml

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/goods/**

启动类

package com.my.micro.service.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Zijian Liao
 * @since 1.0.0
 */
@SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);
    }
}

测试

启动服务,并拜访:http://localhost:5555/goods/g…

能够看到,服务胜利被路由了

一个简略的网关服务就这样实现了,小伙伴看完过有没有对网关的概念更加粗浅呢?

断言

在下面的例子中,咱们就用到了一个断言工厂:Path

在 Spring Cloud Gateway 中,所有的断言工厂都是继承于AbstractRoutePredicateFactory, 并且命名规定为:XxxRoutePredicateFactory, 比方 Path 的类名为:PathRoutePredicateFactory

那么,Spring Cloud Gateway 给咱们内置了哪些断言工厂呢?

文档:https://docs.spring.io/spring…

以下展现我感觉罕用的断言工厂,更多的内容还请小伙伴本人查看文档

After

匹配在某个工夫(ZonedDateTime)后的申请

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/goods/**
            # 匹配 23:05 分后的申请
            - After=2021-08-08T23:05:13.605+08:00[Asia/Shanghai]

咱们在 23:03 进行测试

拜访失败了

Before

匹配在某个工夫(ZonedDateTime)前的申请

与 After 类似,不再演示

Between

匹配在某个时间段(ZonedDateTime)的申请

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/goods/**
            # 匹配 23:05-23:10 的申请
            - Between=2021-08-08T23:05:13.605+08:00[Asia/Shanghai],2021-08-08T23:10:13.605+08:00[Asia/Shanghai]

Host

匹配某个 Host 的申请

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/goods/**
            #配置 host 为 192.168.1.105 申请
            - Host=192.168.1.105

留神,测试时须要将端口号改为 80

尝试应用 127.0.0.1 发动调用

改为 192.168.1.105 进行调用

RemoteAddr

匹配指定的近程源地址

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/goods/**
            #配置 RemoteAddr 为 192.168.1 网段的地址
            - RemoteAddr=192.168.1.1/24

测试

启用内网穿透测试

拜访失败了

过滤器

对于过滤器这块我举个例子,更多的内容请小伙伴本人查阅文档

官网文档:https://docs.spring.io/spring…

举一个用的比拟多的过滤器:

StripPrefix

顾名思义,除去前缀的过滤器,将匹配的申请的前缀去除,将去除后的申请转发给上游服务

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/api/goods/**
          filters:
            # 1 示意去除一个前缀
            - StripPrefix=1

组合来看,意思是当客户端发动申请:http://localhost:5555/api/goo…, 匹配该路由,而后将第一个前缀 api 去除,而后转发给商品服务,转发的门路为:/goods/get-goods

测试

自定义断言工厂

下面提到过:所有的断言工厂都是继承于AbstractRoutePredicateFactory, 并且命名规定为:XxxRoutePredicateFactory, 比方 Path 的类名为:PathRoutePredicateFactory

咱们当初就来尝试实现一个自定义的申请头断言工厂吧

编写代码

package com.my.micro.service.gateway.filter;

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;

/**
 * @author Zijian Liao
 * @since 1.0.0
 */
@Component
public class MyHeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<MyHeaderRoutePredicateFactory.Config> {

    /**
     * Header key.
     */
    public static final String HEADER_KEY = "header";

    /**
     * Regexp key.
     */
    public static final String REGEXP_KEY = "regexp";


    public MyHeaderRoutePredicateFactory() {super(MyHeaderRoutePredicateFactory.Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {return Arrays.asList(HEADER_KEY, REGEXP_KEY);
    }

    @Override
    public Predicate<ServerWebExchange> apply(MyHeaderRoutePredicateFactory.Config config) {return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                // 获取申请头
                List<String> values = exchange.getRequest().getHeaders()
                        .getOrDefault(config.header, Collections.emptyList());
                if (values.isEmpty()) {return false;}
                // 判断申请头中的值是否与配置匹配
                return values.stream()
                        .anyMatch(value -> value.matches(config.regexp));
            }

            @Override
            public String toString() {return String.format("Header: %s=%s", config.header, config.regexp);
            }
        };
    }

    public static class Config {
        private String header;
        private String regexp;

        public String getHeader() {return header;}

        public void setHeader(String header) {this.header = header;}

        public String getRegexp() {return regexp;}

        public void setRegexp(String regexp) {this.regexp = regexp;}
    }
}

编写配置

spring:
  cloud:
    gateway:
      # 路由配置
      routes:
        # 路由 id, 保障唯一性
        - id: my-goods
          # 路由的地址,格局:协定:// 服务名 lb: load balance,my-goods: 商品服务名
          uri: lb://my-goods
          # 断言
          predicates:
            # 匹配 goods 结尾的申请
            - Path=/api/goods/**
            # 匹配 header 为 name=aljian 的申请
            - MyHeader=name,ajian
          filters:
            # 1 示意去除一个前缀
            - StripPrefix=1

测试

间接在浏览器中拜访

改用 postman 拜访

自定义过滤器

自定义过滤器的形式与自定义断言工厂的形式大致相同,所以过滤器继承于 AbstractGatewayFilterFactory 或者AbstractNameValueGatewayFilterFactory, 命名规定为XxxGatewayFilterFactory

比方内置的增加申请头过滤器

public class AddRequestHeaderGatewayFilterFactory
        extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange,
                    GatewayFilterChain chain) {
        // 获取到须要增加的 header value
                String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
                // 将 header 增加到 request 中
        ServerHttpRequest request = exchange.getRequest().mutate()
                        .header(config.getName(), value).build();
                // 从新构建出一个 exchange
                return chain.filter(exchange.mutate().request(request).build());
            }

            @Override
            public String toString() {return filterToStringCreator(AddRequestHeaderGatewayFilterFactory.this)
                        .append(config.getName(), config.getValue()).toString();}
        };
    }

}

全局过滤器

以上内容都是针对于每一个 router,Spring Cloud Gateway 提供了一个针对所有 router 的全局过滤器

实现形式如下

package com.my.micro.service.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author Zijian Liao
 * @since 1.0.0
 */
@Slf4j
@Component
public class MyGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();
        log.info("进入全局过滤器,申请门路为:{}", path);
        // 编写任何你想要实现的逻辑,比方权限校验
        return chain.filter(exchange);
    }
}

测试

自定义异样处理器

小伙伴应该发现了,在遇到谬误时,Spring Cloud Gateway 返回给客户端的异样并不优雅,所以咱们须要自定义异样解决

编写自定义异样处理器

package com.my.micro.service.gateway.exception;

import com.my.micro.service.gateway.result.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

/**
 * @author Zijian Liao
 */
@Slf4j
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {

    /**
     * Create a new {@code DefaultErrorWebExceptionHandler} instance.
     *
     * @param errorAttributes    the error attributes
     * @param resourceProperties the resources configuration properties
     * @param errorProperties    the error configuration properties
     * @param applicationContext the current application context
     */
    public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                          ErrorProperties errorProperties, ApplicationContext applicationContext) {super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    @NonNull
    @Override
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {Throwable throwable = getError(request);
        return ServerResponse.status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(this.handleError(throwable)));
    }

    private BaseResult<Void> handleError(Throwable throwable){return BaseResult.failure(throwable.getMessage());
    }
}

BaseResult

package com.my.micro.service.gateway.result;

import lombok.Data;

/**
 * @author Zijian Liao
 * @since 1.0.0
 */
@Data
public class BaseResult<T> {

    private Integer code;

    private String message;

    public BaseResult(Integer code, String message){
        this.code = code;
        this.message = message;
    }

    public static <T> BaseResult<T> failure(String  message){return new BaseResult<>(-1, message);
    }
}

编写配置类

package com.my.micro.service.gateway.exception;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.stream.Collectors;

/**
 * @author Zijian Liao
 * @since 1.0.0
 */
@Configuration
public class ExceptionConfiguration {

    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, ServerProperties serverProperties, ResourceProperties resourceProperties,
                                                             ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
                                                             ApplicationContext applicationContext) {
        DefaultErrorWebExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
                resourceProperties, serverProperties.getError(), applicationContext);
        exceptionHandler.setViewResolvers(viewResolversProvider.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

测试

小结

本编介绍了对于微服务架构中——客户端如何拜访的解决方案:Spring Cloud Gateway

其中介绍了 Gateway 的三个外围概念:Route,Predicate,Filter。并演示了如何配置及应用他们,还解说了如何自定义 Predicate 和 Filter。

最初介绍了 Spring Cloud Gateway 的全局过滤器,以及如何实现自定义异样解决。

以上

看完之后想必有所播种吧~ 想要理解更多精彩内容,欢送关注公众号:程序员阿鉴,阿鉴在公众号欢送你的到来~

集体博客空间:https://zijiancode.cn/archive…

gittee: https://gitee.com/lzj960515/m…

退出移动版