SpringCloud OpenFeign Ribbon Gateway

目录

1.0.Spring Cloud OpenFeign

1.1.OpenFeign应用

1.2.数据压缩

1.3.OpenFeign日志配置

 2.0.Spring Cloud Ribbon

2.1.Ribbon使用

2.2.Ribbon算法

2.3.自定义负载均衡策略

2.4.Gatewary负载均衡

3.0.SpringCloud Gateway

扫描二维码关注公众号,回复: 17046392 查看本文章

3.1.Gateway工作原理

3.2.Gateway动态路由

3.2.1.业务说明

3.2.2.基于配置路由设置

3.2.3.基于代码路由配置

3.2.4.Gateway-Predicate

1.Cookie:

2.Header匹配:

3.请求方式匹配:

3.3.Gateway过滤器

3.3.1.过滤器分类

3.3.2.默认过滤器

1.全局过滤器

2.局部过滤器

3.3.3.自定义过滤器

1.全局过滤器

3.4.跨域配置

3.5.限流

3.5.1.漏桶算法讲解

3.5.2.限流案例


1.0.Spring Cloud OpenFeign

Feign [feɪn] 译文 伪装。Feign是一个轻量级的Http封装工具对象,大大简化了Http请求,它的使用方法是定义一个接口,然后在上面添加注解。不需要拼接URL、参数等操作。项目主页:https://github.com/OpenFeign/feign 。

• 集成Ribbon负载均衡功能

• 集成了Hystrix熔断器功能

• 支持请求压缩

• 大大简化了远程调用的代码,同时功能还增强啦

• Feign以更加优雅的方式编写远程调用代码,并简化重复代码

创建测试项目producerconsumer项目学习OpenFeign的应用。

consumer

 在bootstrap.yml配置nacos服务地址:

server:
  port: 10088
spring:
  application:
    name: consumer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

producer

 在bootstrap.yml配置nacos服务地址:

server:
  port: 10086
spring:
  application:
    name: producer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

1.1.OpenFeign应用

使用OpenFeign实现服务之间调用,可以按照如下步骤实现:

1:导入feign依赖
2:编写Feign客户端接口-将请求地址写到该接口上
3:消费者启动引导类开启Feign功能注解
4:访问接口测试

1)导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

2)consumer创建Feign客户端接口

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "producer-server")
public interface ProducerFeign {

    @GetMapping("/producer/status/{status}")
    String consumer(@PathVariable(value = "status") String status);
}

参数说明:

Feign会通过动态代理,帮我们生成实现类。
注解@FeignClient声明Feign的客户端,注解value指明服务名称
接口定义的方法,采用SpringMVC的注解。Feign会根据注解去注册中心找到服务帮我们生成URL地址
注解@RequestMapping中的/producer,不要忘记。因为Feign需要拼接可访问地址

3)Consumer创建Controller调用

Controller中通知Producer已经做好了准备。

import dream.myself.consumer.api.ProducerFeign;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("consumer")
public class ConsumerController {
    private ProducerFeign producer;

    public ConsumerController(ProducerFeign producer) {
        this.producer = producer;
    }

    @GetMapping("getMessage")
    public String consumerMessage(){
        return producer.consumer("ready");
    }
}

Producer中实现Feign远程调用的接口,告知Consumer收到了信息。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("producer")
public class ProducerController {

    @GetMapping("/status/{status}")
    String consumer(@PathVariable(value = "status") String status) {
        return "Producer massage:" + status;
    }
}

4)启用OpenFeign

上面所有业务逻辑写完了,但OpenFeign还并未生效,我们需要在Consumer中开启OpenFeign,只需要在Consumer启动类上添加@EnableFeignClients(basePackages = "dream.myself.consumer.api")即可basePackages指向Feign接口所在的包路径。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableFeignClients(basePackages = "dream.myself.consumer.api")
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

在浏览器中调用Consumer接口http://127.0.0.1:10088/consumer/getMessage

1.2.数据压缩

用户在网络请求过程中,如果网络不佳、传输数据过大,会造成体验差的问题,我们需要将传输数据压缩提升体验。SpringCloud OpenFeign支持对请求响应进行GZIP压缩,以减少通信过程中的性能损耗

通过配置开启请求与响应的压缩功能:

server:
  port: 10088
spring:
  application:
    name: consumer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
    response:
      enabled: true # 开启响应压缩

也可以对请求的数据类型,以及触发压缩的大小下限进行设置:

server:
  port: 10088
spring:
  application:
    name: consumer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
      # 设置压缩的数据类型(默认值)
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限(默认值)
      min-request-size: 2048
    response:
      enabled: true # 开启响应压缩

1.3.OpenFeign日志配置

通过loggin.level.xx=debug来设置日志级别。

1)普通日志等级配置

Consumer的配置文件中设置dream.bemyself包下的日志级别都为debug

server:
  port: 10088
spring:
  application:
    name: consumer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
      # 设置压缩的数据类型(默认值)
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限(默认值)
      min-request-size: 2048
    response:
      enabled: true # 开启响应压缩

logging:
  level:
    dream.myself.consumer: debug

然而这个对Feign客户端不会产生效果。因为@FeignClient注解修饰的客户端在被代理时,都会创建一个新的Feign.Logger实例。我们需要额外通过配置类的方式指定这个日志的级别才可以。

实现步骤:

1. 在yml配置文件中开启日志级别配置
2. 编写配置类,定义日志级别bean
3. 在接口的@FeignClient中指定配置类
4. 重启项目,测试访问

2)Feign日志等级配置

Consumer启动类中创建日志级别。

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableFeignClients(basePackages = "dream.myself.consumer.api")
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

日志级别说明:

Feign支持4中级别:
    NONE:不记录任何日志,默认值
    BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
    HEADERS:在BASIC基础上,额外记录了请求响应的头信息
    FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

重启Consumer,即可看到每次访问的日志

 2.0.Spring Cloud Ribbon

什么是RibbonRibbonNetflix发布的负载均衡器,有助于控制HTTP客户端行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于负载均衡算法,自动帮助服务消费者请求。Ribbon默认提供的负载均衡算法:轮询随机重试法加权。当然,我们可用自己定义负载均衡算法

2.1.Ribbon使用

1)复制多个Producer启动项并分别配置不同的端口号。

Producer中获取当前服务的端口号,并告知Consumer端口号。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("producer")
public class ProducerController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ProducerController.class);

    private final Environment environment;

    public ProducerController(Environment environment) {
        this.environment = environment;
    }

    @GetMapping("/status/{status}")
    String consumer(@PathVariable(value = "status") String status) {
        String port = environment.getProperty("server.port");
        LOGGER.info("server.port:" + port);
        return "Producer massage:" + status + ", Receive request server port:" + port;
    }
}

2)调用测试

访问http://127.0.0.1:10088/consumer/getMessage,可以在Consumer的日志日志中发现已经实现负载均衡了,几个服务会轮询着调用。

 2.2.Ribbon算法

我们上面没做任何相关操作,只是把服务换成了多个就实现了负载均衡,这是因为OpenFeign默认使用了Ribbon轮询算法,如下图依赖包,引入OpenFeign的时候会传递依赖Ribbon包:

 我们如果想改变相关算法,可以直接在.yml中配置算法即可。

server:
  port: 10088
spring:
  application:
    name: consumer-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
      # 设置压缩的数据类型(默认值)
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限(默认值)
      min-request-size: 2048
    response:
      enabled: true # 开启响应压缩

logging:
  level:
    dream.myself.consumer: debug

producer-server:
  ribbon:
    #轮询
    #NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    #随机算法
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    #重试算法,该算法先按照轮询的策略获取服务,如果获取服务失败则在指定的时间内会进行重试,获取可用的服务
    #NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule
    #加权法,会根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越大。刚启动时如果同统计信息不足,则使用轮询的策略,等统计信息足够会切换到自身规则。
    #NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

负载均衡算法在代码中可以看到默认的是轮询算法:

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {

    private static Logger logger = LoggerFactory.getLogger(BaseLoadBalancer.class);
    private final static IRule DEFAULT_RULE = new RoundRobinRule();
    private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
    private static final String DEFAULT_NAME = "default";
    private static final String PREFIX = "LoadBalancer_";

    protected IRule rule = DEFAULT_RULE;

    protected IPingStrategy pingStrategy = DEFAULT_PING_STRATEGY;

    protected IPing ping = null;

此时再次访问Consumer服务可以看到服务调用已经是随机调用了。

2.3.自定义负载均衡策略

自定义负载均衡策略只需要实现IRule接口即可,IRule接口就是实现负载均衡功能的接口。

public interface IRule{
    public Server choose(Object key);
    public void setLoadBalancer(ILoadBalancer lb);
    public ILoadBalancer getLoadBalancer();    
}

自定义负载均衡算法:

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;

import java.util.List;

/**
 * 自定义负载均衡算法
 */
public class MyRule implements IRule {
    private ILoadBalancer loadBalancer;

    @Override
    public Server choose(Object key) {
        List<Server> serverList = getLoadBalancer().getAllServers();
        // 只获取第一个服务处理任务
        return serverList.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer loadBalancer) {
        this.loadBalancer = loadBalancer;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return this.loadBalancer;
    }
}

在启动类中设置好自定义负载均衡算法即可

import com.netflix.loadbalancer.IRule;
import dream.myself.consumer.controller.MyRule;
import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableFeignClients(basePackages = "dream.myself.consumer.api")
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public IRule rule() {
        return new MyRule();
    }
}

自定义负载均衡算法每次只会将请求交给第一个服务处理请求,此时不管访问多少次都是同一个服务处理请求。

 2.4.Gatewary负载均衡

Gatewary的过滤器中的负载均衡也是Ribbon实现的,追踪自定义过滤器filter

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class PayFilter implements GatewayFilter, Ordered {
    /***
     * 过滤器执行拦截
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("GatewayFilter拦截器执行-----PayFilter");
        return chain.filter(exchange);
    }

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

追踪进去会看到过滤器中有负载均衡策略

 直接跟踪LoadBalancerClientFilter中filter方法:

@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
   String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
   if (url == null
         || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
      return chain.filter(exchange);
   }
   // preserve the original url
   addOriginalRequestUrl(exchange, url);

   if (log.isTraceEnabled()) {
      log.trace("LoadBalancerClientFilter url before: " + url);
   }

   final ServiceInstance instance = choose(exchange);

   if (instance == null) {
      throw NotFoundException.create(properties.isUse404(),
            "Unable to find instance for " + url.getHost());
   }

   URI uri = exchange.getRequest().getURI();

   // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
   // if the loadbalancer doesn't provide one.
   String overrideScheme = instance.isSecure() ? "https" : "http";
   if (schemePrefix != null) {
      overrideScheme = url.getScheme();
   }

   URI requestUrl = loadBalancer.reconstructURI(
         new DelegatingServiceInstance(instance, overrideScheme), uri);

   if (log.isTraceEnabled()) {
      log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
   }

   exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
   return chain.filter(exchange);
}


protected ServiceInstance choose(ServerWebExchange exchange) {
   return loadBalancer.choose(
         ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}

一路跟踪choose到RibbonLoadBalancerClient可以看到通过负载均衡策略获取到了服务。

public ServiceInstance choose(String serviceId) {
    Server server = this.getLoadBalancer().chooseServer(serviceId);
    return server != null ? new RibbonServer(serviceId, server) : null;
}

3.0.SpringCloud Gateway

        Spring Cloud GatewaySpring Cloud团队的一个全新项目,基于Spring 5.0、SpringBoot2.0、Project Reactor 等技术开发的网关。旨在为微服务架构提供一种简单有效统一的API路由管理方式

        Spring Cloud Gateway 作为SpringCloud生态系统中的网关,目标是替代Netflix ZuulGateway不仅提供统一路由方式,并且基于Filter链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。

3.1.Gateway工作原理

我们在学习Gateway之前,先弄清楚Gateway的工作原理,后面使用它的各个功能时,就知道该如何使用了,工作流程图如下:

Gateway的执行流程如下:

1:Gateway的客户端回向Spring Cloud Gateway发起请求,请求首先会被HttpWebHandlerAdapter进行提取组装成网关的上下文,然后网关的上下文会传递到DispatcherHandler。

2:DispatcherHandler是所有请求的分发处理器,DispatcherHandler主要负责分发请求对应的处理器,比如将请求分发到对应RoutePredicateHandlerMapping(路由断言处理器映射器)

3:路由断言处理映射器主要用于路由的查找,以及找到路由后返回对应的FilteringWebHandler。

4:FilteringWebHandler主要负责组装Filter链表并调用Filter执行一系列Filter处理,然后把请求转到后端对应的代理服务处理,处理完毕后,将Response返回到Gateway客户端。
在Filter链中,通过虚线分割Filter的原因是,过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的Pre类型的Filter执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求完毕之后,才会执行Post类型的过滤器。

3.2.Gateway动态路由

        Gateway路由配置分为基于配置的设置和基于代码动态路由配置,静态路由是指在application.yml中把路由信息配置好了,而动态路由则是从数据库中加载而来,我们接下来把这2种路由操作都实现一次。

maven:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

3.2.1.业务说明

如上图:

1:用户所有请求以/order开始的请求,都路由到 hailtaxi-order服务
2:用户所有请求以/driver开始的请求,都路由到 hailtaxi-driver服务
3:用户所有请求以/oat开始的请求,都路由到 hailtaxi-pay服务

3.2.2.基于配置路由设置

 如上图所示,正是Gateway静态路由配置:

1:用户所有请求以/order开始的请求,都路由到 hailtaxi-order服务
2:用户所有请求以/driver开始的请求,都路由到 hailtaxi-driver服务
3:用户所有请求以/oat开始的请求,都路由到 hailtaxi-pay服务

配置参数说明:

routes:路由配置
- id:唯一标识符
uri:路由地址,可以是 lb://IP:端口     也可以是   lb://${spring.application.name}
predicates:断言,是指路由条件
- Path=/driver/**:路由条件。Predicate 接受一个输入参数,返回一个布尔值结果。这里表示匹配所有以driver开始的请求。

3.2.3.基于代码路由配置

我们同样实现上面的功能,但这里基于代码方式实现。所有路由规则我们可以从数据库中读取并加载到程序中。

创建一个Gateway项目,用来学习Gateway

bootstrap.yml配置nacos服务地址:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848

基于代码的路由配置我们只需要创建RouteLocator并添加路由配置即可,Gateway项目中添加如下代码:

/***
 * 路由配置
 */
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(router -> router.path("/consumer/**").uri("lb://consumer-server"))
            .route(router -> router.path("/producer/**").uri("lb://producer-server"))
            .build();
}

        在真实场景中,基于配置文件的方式更直观、简介,但代码的路由配置是更强大,可以实现很丰富的功能,可以把路由规则存在数据库中,每次直接从数据库中加载规则,这样的好处是可以动态刷新路由规则,通常应用于权限系统动态配置新系统。

3.2.4.Gateway-Predicate

        上面路由匹配规则中我们都用了- Path方式,其实就是路径匹配方式,除了路径匹配方式,Gateway还支持很多丰富的匹配方式,我们对这些方式分别进行讲解。

        关于Predicate学习地址,可以参考官网https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories

        routes下面的属性含义如下:

id:我们自定义的路由 ID,保持唯一
uri:目标服务地址
predicates:路由条件,Predicate接受一个输入参数,返回一个布尔值结果。该属性包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)

        Predicate来源于Java 8,Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)

        在Spring Cloud GatewaySpring利用Predicate的特性实现了各种路由匹配规则,通过Header、请求参数等不同的条件来作为条件匹配到对应的路由。

        下面的一张图(来自网络)总结了Spring Cloud内置的几种Predicate的实现:

我们在这里讲解几个断言匹配方式。

1.Cookie:

GatewayCookie匹配接收两个参数:一个是Cookie name ,一个是正则表达式。路由规则就是通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。如下配置:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      #路由配置(断言也是一种过滤器)
      routes:
        - id:
          #唯一标识符
        - id: producer-server
          uri: lb://producer-server
          #路由断言
          predicates:
            - Path=/producer/**
            - Cookie=username,bemyself

这里表示请求携带了cookieusername的数据,并且值为bemyself,就允许通过。

2.Header匹配:

Header匹配和Cookie匹配 一样,也是接收两个参数,一个header中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。配置如下:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      #路由配置(断言也是一种过滤器)
      routes:
        - id:
          #唯一标识符
        - id: producer-server
          uri: lb://producer-server
          #路由断言
          predicates:
            - Path=/producer/**
            - Cookie=username,bemyself
            - Header=token,^(?!\d+$)[\da-zA-Z]+$

上面的匹配规则,就是请求头要有token属性,并且值必须为数字字母组合的正则表达式,例如携带token=19and30就可以通过访问。

3.请求方式匹配:

通过请求的方式是POSTGETPUTDELETE等进行路由。配置如下:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      #路由配置(断言也是一种过滤器)
      routes:
        - id:
          #唯一标识符
        - id: producer-server
          uri: lb://producer-server
          #路由断言
          predicates:
            - Path=/producer/**
            - Cookie=username,bemyself
            - Header=token,^(?!\d+$)[\da-zA-Z]+$
            - Method=GET

3.3.Gateway过滤器

        Spring Cloud Gateway根据作用范围划分为GatewayFilterGlobalFilter,二者区别如下:

        • GatewayFilter : 需要通过spring.cloud.routes.filters配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上

        • GlobalFilter : 全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上

        过滤器作为Gateway的重要功能。常用于请求鉴权服务调用时长统计修改请求或响应header、限流、去除路径等等。

        关于Gateway过滤器的更多使用,大家可以参考官方地址:

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.1.RELEASE/single/spring-cloud-gateway.html#_gatewayfilter_factories

3.3.1.过滤器分类

默认过滤器:出厂自带,实现好了拿来就用,不需要实现
  全局默认过滤器
  局部默认过滤器
自定义过滤器:根据需求自己实现,实现后需配置,然后才能用哦。
  全局过滤器:作用在所有路由上。
  局部过滤器:配置在具体路由下,只作用在当前路由上。

        默认过滤器几十个,常见如下:

过滤器名称

说明

AddRequestHeader

对匹配上的请求加上Header

AddRequestParameters

对匹配上的请求路由添加key/value参数到所有匹配请求的下游请求的查询字符串中

AddResponseHeader

对从网关返回的响应添加Header(如点赞操作场景)

StripPrefix

对匹配上的请求路径去除前缀

PrefixPath

对匹配上的请求路径添加前缀

3.3.2.默认过滤器

默认过滤器有两种:全局默认过滤器局部默认过滤器

1.全局过滤器

对输出响应头设置属性对输出的响应设置其头部属性名称为X-Response-Default-MyName,值为bemyself,修改配置文件,配置如下:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/gateway/**
      # 配置全局默认过滤器
      default-filters:
        - AddResponseHeader=X-Response-Default-MyName,bemyself

请求http://127.0.0.1:10084/gateway/getMessage匹配到了路由,响应数据中添加了X-Response-Default-MyName: bemyself

注意点:Gatewayspringboot依赖冲突,同时Gateway不支持javax.servlet.http.HttpServletRequest(servlet依赖),如果要使用request可以使用org.springframework.http.server.reactive.ServerHttpRequest。

2.局部过滤器

        通过局部默认过滤器,修改请求路径。局部过滤器在这里介绍两种:去除路径前缀添加路径前缀

        在项目中做开发对接接口的时候,我们很多时候需要统一API路径,比如统一以/api开始的请求调producer-server服务,但真实服务接口地址又没有/api路径,我们可以使用Gateway的过滤器处理请求路径。

        在gateway中可以通过配置路由的过滤器StripPrefix实现映射路径中的前缀处理,我们来使用一下该过滤器,再进一步做说明。

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/gateway/**
          filters:
            - StripPrefix=1

        此处- StripPrefix=1表示真实请求地址是当前用户请求以/gateway开始的uri中去除第1个路径/gateway

        真实controller中的请求路径:

@RestController
public class GatewayController {
    @GetMapping("getMessage")
    public String getMessage(ServerHttpResponse response) {
        return "I am a Controller!";
    }

    @PostMapping("postMessage")
    public String postMessage(ServerHttpResponse response, @RequestParam String message) {
        return "I get the word:" + message;
    }
}

前端url请求路径为:http://127.0.0.1:10084/gateway/getMessage,请求路径中的gateway被路由去除后,访问到真实服务url路径为http://127.0.0.1:10084/getMessage。

 有时候为了简化用户请求地址,比如用户请求http://localhost:8001/info/1我们想统一路由到http://localhost:18081/driver/info/1,可以使用PrefixPath过滤器增加前缀。

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/**
          filters:
            - PrefixPath=/gateway

真实controller中的请求路径:

@RestController
@RequestMapping("gateway")
public class GatewayController {
    @GetMapping("getMessage")
    public String getMessage(ServerHttpResponse response) {
        return "I am a Controller!";
    }

    @PostMapping("postMessage")
    public String postMessage(ServerHttpResponse response, @RequestParam String message) {
        return "I get the word:" + message;
    }
}

前端url请求路径为:http://127.0.0.1:10084/getMessage,在请求路径中添加gateway前缀除后,访问到真实服务url路径为http://127.0.0.1:10084、gateway/getMessage。

3.3.3.自定义过滤器

自定义过滤器也有两类:全局自定义过滤器,和局部自定义过滤器

1.全局过滤器

在项目中很多地方都有可能会使用,因此我们来学习下如何定义全局过滤器,并使用全局过滤器。

定义全局过滤器需要实现GlobalFilter,Ordered接口:

GlobalFilter:过滤器拦截处理方法
Ordered:过滤器也有多个,这里主要定义过滤器执行顺序,里面有个方法getOrder()会返回过滤器执行顺序,返回值越小,越靠前执行

我们创建全局过滤器并完成常见业务用户权限校验。

基本逻辑:如果请求中有Token参数,则认为请求有效放行,如果没有则拦截提示授权无效。

创建全局过滤器:com.bemyself.filter.RouterFilter,代码如下:

import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class RouterFilter implements GlobalFilter, Ordered {
    /**
     * 路由拦截
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求参数
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        //如果token为空,则表示没有登录
        if (StringUtils.isEmpty(token)) {
            //没登录,状态设置403
            exchange.getResponse().setStatusCode(HttpStatus.PAYLOAD_TOO_LARGE);
            //结束请求
            return exchange.getResponse().setComplete();
        }
        //放行
        return chain.filter(exchange);
    }

    /**
     * 拦截器顺序
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

路由配置:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/gateway/**
          filters:
            - StripPrefix=1

注意:这里的过滤器必须匹配上路由后才能生效。

此时请求,我们不携带token参数会收到一个413的错误状态码,效果如下:

        我们携带token参数则可以正常访问,效果如下:

2.局部过滤器

        局部过滤器定义一般作用在某一个路由上,需要实例化创建才能使用,局部过滤器需要实现接口GatewayFilterOrdered

        创建com.bemyself.filter.PayFilter代码如下:

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class PayFilter implements GatewayFilter, Ordered {
    /***
     * 过滤器执行拦截
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("GatewayFilter拦截器执行-----PayFilter");
        return chain.filter(exchange);
    }

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

使用局部过滤器:(使用下面RouteLocator的时候,配置文件中的路由记得注释或删除)

/***
 * 路由配置
 */
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(router -> router.path("/consumer/**").uri("lb://consumer-server"))
            .route(router -> router.path("/producer/**").uri("lb://producer-server"))
            .route(router -> router.path("/gateway/**").uri("lb://gateway-server").filter(new PayFilter()))
            .build();
}

        controller访问实例:

@RestController
@RequestMapping("gateway")
public class GatewayController {
    @GetMapping("getMessage")
    public String getMessage(ServerHttpResponse response) {
        return "I am a Controller!";
    }

    @PostMapping("postMessage")
    public String postMessage(ServerHttpResponse response, @RequestParam String message) {
        return "I get the word:" + message;
    }
}

        为了更好看到效果,我们在RouterFilter添加System.out.println("GlobalFilter拦截器执行");再访问测试。

@Component
public class RouterFilter implements GlobalFilter, Ordered {

    /**
     * 路由拦截
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求参数
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        //如果token为空,则表示没有登录
        if (StringUtils.isEmpty(token)) {
            //没登录,状态设置403
            exchange.getResponse().setStatusCode(HttpStatus.PAYLOAD_TOO_LARGE);
            //结束请求
            return exchange.getResponse().setComplete();
        }
        System.out.println("GlobalFilter拦截器执行");
        //放行
        return chain.filter(exchange);
    }

    /**
     * 拦截器顺序
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

    访问:http://localhost:10084/consumer?token=aa 只执行了RouterFilter

  访问:http://localhost:10084/gateway/status/1?token=1 先执行了GlobalFilter,后执行了GatewayFilter

        如果定义局部过滤器,在配置文件中使用,可以继承AbstractGatewayFilterFactory<T>抽象类:

@Component
public class PayGatewayFilterFactory extends AbstractGatewayFilterFactory<PayGatewayFilterFactory.Config> {

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

    /**
     * 执行拦截
     */
    @Override
    public GatewayFilter apply(PayGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            String paymethod = config.getPayMethod();
            // 将paymethod添加到请求头中
            exchange.getRequest().mutate().header("payMethod", paymethod);
            return chain.filter(exchange);
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        ArrayList<String> strings = new ArrayList<>();
        strings.add("payMethod");
        return strings;
    }

    /**
     * 获取指定属性值
     */
    @Data
    public static class Config {
        private String payMethod;
    }
}

        在配置文件中配置自定义过滤器:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/gateway/**
          filters:
            - StripPrefix=1
            - Pay=PayGatewayFilterFactory

3.4.跨域配置

        出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

        在Spring Cloud Gateway中配置跨域是非常简单的,如下面application.yml所示:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 所有的请求
            allowedOrigins: "*" # 全部允许跨域
            allowedMethods: #允许跨域请求方式
              - GET
              - POST
              - PUT

        但如果涉及到Cookie跨域,上面的配置就不生效了,如果涉及到Cookie跨域,需要创建CorsWebFilter过滤器,代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

public class PayFilter {
    /**
     * 配置跨域
     */
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // cookie跨域
        config.setAllowCredentials(Boolean.TRUE);
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        // 配置前端js允许访问的自定义响应头
        config.addExposedHeader("Authorization");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

3.5.限流

        网关可以做很多的事情,比如,限流,当我们的系统被频繁的请求的时候,就有可能将系统压垮,所以为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。

3.5.1.漏桶算法讲解

漏桶算法是常见的限流算法之一,我们讲解一下漏桶算法:

1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

漏桶算法的实现,有很多技术,Guaua是其中之一,redis客户端也有其实现。

3.5.2.限流案例

1)引入依赖

        spring cloud gateway默认使用redisRateLimter限流算法来实现。所以我们要使用首先需要引入redis的依赖:

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

        同时不要忘记Redis配置:

  redis:
    host: 123.60.23.244
    port: 6379

2)定义KeyResolver

        在Applicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key

        我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayApplication定义key的获取,获取客户端IP,将IP作为key,如下代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    /***
     * IP限流
     */
    @Bean(name = "ipKeyResolver")
    public KeyResolver userKeyResolver() {
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                //获取远程客户端IP
                String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
                System.out.println("hostName:" + hostName);
                return Mono.just(hostName);
            }
        };
    }
}

路由配置如下:

server:
  port: 10084
spring:
  application:
    name: gateway-server
  cloud:
    nacos:
      config:
        server-addr: 123.60.23.244:8848
      discovery:
        server-addr: 123.60.23.244:8848
    gateway:
      routes:
        - id: producer-server
          uri: lb://gateway-server
          predicates:
            - Path=/gateway/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter #请求数限流 名字不能随便写 使用默认的facatory
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
  redis:
    host: 123.60.23.244
    port: 6379

参数说明:

redis-rate-limiter.replenishRate是您希望允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率
redis-rate-limiter.burstCapacity是指令牌桶的容量,允许在一秒钟内完成的最大请求数,将此值设置为零将阻止所有请求。
key-resolver: “#{@ipKeyResolver}” 用于通过SPEL表达式来指定使用哪一个KeyResolver.

如上配置:表示一秒内,允许一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。最大突发状况也只允许 一秒内有一次请求,可以根据业务来调整 。

我们请求http://127.0.0.1:10084/gateway/getMessage执行测试,1秒钟内只能请求一次是成功响应的。效果如下:

 打开redis可视化工具,可以看到redis中存放了令牌信息。 

猜你喜欢

转载自blog.csdn.net/qq_43460743/article/details/131461323