写在前面

接上篇文章:https://blog.csdn.net/a__int__/article/details/109438417

1、Sentinel

1.1、五种规则

五种规则分别是:流控规则、降级规则、热点规则、系统规则、授权规则

1.1.1、流控规则(根据流量大小限流)

在这里插入图片描述
里面有几个数据需要填
在这里插入图片描述
针对来源:里面填调用本微服务的微服务名字,默认的default,是对所有来源起作用

QBS:表示每秒请求进来的数量,单机阈值就是其值

线程数:表示最大并发线程,单机阈值就是其值

测试QBS直接用浏览器不断访问就可以测试,
测试多线程数用jmeter工具测试。

点开高级选项
在这里插入图片描述
里面有个流控模式
在这里插入图片描述

直接(默认):接口达到限流条件时,开启限流

关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]

链路:当从某个接口过来的资源达到限流条件时,开启限流

流控模式中间的关联表示,当关联的资源达到条件时,开始限流
我们用jmeter测试一下,
http://localhost:8091/order/msg1设置限流规则,http://localhost:8091/order/msg2设为关联,每秒2个
在这里插入图片描述
每0.2秒一个线程,也就是每秒5个同时去访问http://localhost:8091/order/msg2
在这里插入图片描述
在这里插入图片描述
接下来再访问http://localhost:8091/order/msg1
就一直是失败,因为/order/msg2每秒5个线程正在被请求
在这里插入图片描述
流控模式右边的链路
在这里插入图片描述
需要进行一些配置才能生效

从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应spring cloud alibaba的2.1.1.RELEASE),官方在CommonFilter 引入了WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。
spring cloud alibaba 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛

例:

1)编写一个service,在里面添加一个方法message

@Service
public class OrderServiceImpl3 {
    @SentinelResource("msg")
    public void msg() {
        System.out.println("msg");
    }
}

2)在Controller中声明两个方法,分别调用service中的方法 @RestController

@RestController
@Slf4j
public class OrderController3 {
    @Autowired
    private OrderServiceImpl3 orderServiceImpl3;

    @RequestMapping("/order/msg1")
    public String msg1() {
        orderServiceImpl3.msg();
        return "msg1";
    }

    @RequestMapping("/order/msg2")
    public String msg2() {
        orderServiceImpl3.msg();
        return "msg2";
    }
}

3)禁止收敛URL的入口 context

spring cloud alibaba 2.1.2.RELEASE之后的版本才能进行如下配置

在shop-order的配置文件中,关闭sentinel的CommonFilter实例化

spring.cloud.sentinel.web-context-unify=false

4)添加限流
在这里插入图片描述
5)分别通过/order/msg1 和/order/msg2 访问, 发现2没问题, 1的被限流了
在这里插入图片描述
链路是针对上级接口的,上级接口被限流了,自己也会被限流。
链路和针对来源注意区别,针对来源是针对上级微服务(针对更大一些)。

接下来看看最后一个流控效果,第一个快速失败
就是以上流控起效果时,访问连接显示失败
在这里插入图片描述
第二个warm up(预热),适用于突然增大的流量转换为缓步增长
第三个排队等待,访问被限制不会直接失败,会慢慢延迟访问

1.1.2、降级规则(根据响应时间、比例限流)

名词解释:
RT(慢调用比例 ):平均响应时间
在这里插入图片描述
RT单位是毫秒,里面填1000,意思是访问时间超过1s,进入准降级状态,再观察接下几个访问,如果都超过1s那就降级,比例阈值就是多长时间内访问超过RT,就降级。

RT上限是4900ms,可以通过启动项配置-Dcsp.sentinel.statistic.max.rt=xxx来改这个上限

异常比例:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态
在这里插入图片描述
1)首先模拟一个异常

int i = 0;
@RequestMapping("/order/msg2")
public String msg2() {
      i++;
      //异常比例为0.333
       if (i % 3 == 0) {
          throw new RuntimeException();
      }
      return "msg2";
}

规则如下
在这里插入图片描述
最后的异常数和异常比例差不多
在这里插入图片描述
降级和流控的异常页面一样,后面我们可以自定义异常页面

1.1.2、热点规则(根据url中的参数限流)

热点规则可以对url中的参数进行限流

在orderController3里,编写代码

@RequestMapping("/order/msg3")
@SentinelResource("message3")//注意这里必须使用这个注解标识,热点规则不生效
public String msg3(String name, Integer age) {
    return name + age;
}

对/order/msg3中的第0个参数进行限流
在这里插入图片描述
除了可以这样简单的对参数限流
点开高级选项,还可以有更多的设定
在这里插入图片描述

1.1.3、授权规则(黑名单、白名单)

相当于配置黑白名单
在这里插入图片描述
上图的流控应用怎么填写呢?

Sentinel提供了RequestOriginParser 接口来处理来源。
只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser 的实现类去解析访问来源。

1)自定义来源处理规则,新建RequestOriginParserDefinition.java

@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
          String serviceName = request.getParameter("serviceName");
          return serviceName;
    }
}

2)授权规则配置
这个配置的意思是只有 参数serviceName=pc 不能访问(黑名单)
在这里插入图片描述
3)访问 http://localhost:8091/order/msg1 ?serviceName=pc观察结果
在这里插入图片描述

1.1.4、系统规则(判断系统环境进行限流)

系统规则一般是运维层的东西,这里只介绍,不操作

系统规则是从应用级别的入口流量进行控制,从单台机器总体的Load、RT、入口QPS、CPU使用和线程数五个维度监控应用数据,让系统尽可能在最大吞吐量的同时保证系统稳定性。针对应用整体维度,不针对资源维度,且仅对入口流量生效。

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。
1.2、自定义异常
1.2.1、在配置类中定义返回内容

首先我们要拿到所有规则产生的异常,新建一个类实现接口BlockExceptionHandler中的handle方法

sentinel 1.8版本以下是实现接口UrlBlockHandler中的blocked方法

@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException ex) throws Exception {
        String msg = null;
        if (ex instanceof FlowException) {
            msg = "接口被限流了";
        } else if (ex instanceof DegradeException) {
            msg = "降级了";
        } else if (ex instanceof ParamFlowException) {
            msg = "热点参数限流";
        } else if (ex instanceof SystemBlockException) {
            msg = "系统规则(负载/...不满足要求)";
        } else if (ex instanceof AuthorityException) {
            msg = "授权规则不通过";
        }
        // http状态码
        response.setStatus(500);
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setContentType("application/json;charset=utf-8");

        response.getWriter().write(JSON.toJSONString(msg));

    }
}

也可以定义一个封装类,封装多个参数进去

@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException ex) throws Exception {

        ResponseData m = null;
        if (ex instanceof FlowException) {
            m = new ResponseData(-1,"接口被限流了");
        } else if (ex instanceof DegradeException) {
            m = new ResponseData(-2,"降级了");
        } else if (ex instanceof ParamFlowException) {
            m = new ResponseData(-3,"热点参数限流了");
        } else if (ex instanceof SystemBlockException) {
            m = new ResponseData(-4,"系统规则限流了");
        } else if (ex instanceof AuthorityException) {
            m = new ResponseData(-5,"授权规则不通过");
        }

        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setContentType("application/json;charset=utf-8");

        response.getWriter().write(JSON.toJSONString(m));

    }
}

@Data
@AllArgsConstructor   //全参数构造函数
@NoArgsConstructor    //无参构造
class ResponseData{
    private int code;
    private String msg;
}

然后我们写一个流控规则试一试
在这里插入图片描述
访问
在这里插入图片描述

1.2.2、异常处理逻辑@SentinelResource

在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能通过@SentinelResource来指定出现异常时的处理策略。

修改OrderServiceImpl3.java

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderServiceImpl3 {
    @SentinelResource(
            value = "msg",
            blockHandler = "blockHandler",
            fallback = "fallback"
    )
    public String msg(String name) {
        System.out.println("msg");
        return name;
    }

    // 这里的blockHandler的返回值和参数,必须和原方法一致
    // 允许在参数列表的最后加一个参数 BlockException,用来接收在原方法中发生的异常。
    public String blockHandler(String name, BlockException e){
        log.error("触发了BlockException : {}",e);
        return "BlockException";
    }

    public String fallback(String name, Throwable e){
        log.error("触发了Throwable : {}",e);
        return "Throwable";
    }
}

blockHandler用于接收sentinel报的异常
fallback用于接收其他异常
在这里插入图片描述

修改OrderController3.java
在这里插入图片描述
然后重启项目
访问
在这里插入图片描述
然后写一个限流
在这里插入图片描述
再访问就是走的blockHandler
在这里插入图片描述
如果我们把blockHandler注释掉
在这里插入图片描述
还是同样的限流,这次访问出来是
在这里插入图片描述
所以我们发现BlockException是包含在Throwable中的,Throwable范围更广。

这里blockHandler、Throwable都被我们写成了方法,其实还可写出类

我们先新建一个OrderServiceImpl3BlockHandle.java,内容如下

这里面的方法必须是静态的

import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class OrderServiceImpl3BlockHandle {
    public static String blockHandler(String name, BlockException e){
        log.error("触发了BlockException : {}",e);
        return "BlockException";
    }
}

再建一个OrderServiceImpl3Fallback.java

这里面的方法必须是静态的

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class OrderServiceImpl3Fallback {
    public static String fallback(String name, Throwable e){
        log.error("触发了Throwable : {}",e);
        return "Throwable";
    }
}

修改OrderServiceImpl3如下
在这里插入图片描述
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下:
在这里插入图片描述

1.3、sentinel规则持久化

现在我们仅仅通过web界面设置的规则是保存在内存中的,一旦重新启动就会丢失
在这里插入图片描述1) shop-order下的config里,新建一个处理类FilePersistence

package com.haha.config;

import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;

import java.io.File;
import java.io.IOException;
import java.util.List;

public class FilePersistence implements InitFunc {

    @Value("spring.application.name")
    private String appcationName;

    @Override
    public void init() throws Exception {
        String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName;
        String flowRulePath = ruleDir + "/flow-rule.json";
        String degradeRulePath = ruleDir + "/degrade-rule.json";
        String systemRulePath = ruleDir + "/system-rule.json";
        String authorityRulePath = ruleDir + "/authority-rule.json";
        String paramFlowRulePath = ruleDir + "/param-flow-rule.json";

        this.mkdirIfNotExits(ruleDir);
        this.createFileIfNotExits(flowRulePath);
        this.createFileIfNotExits(degradeRulePath);
        this.createFileIfNotExits(systemRulePath);
        this.createFileIfNotExits(authorityRulePath);
        this.createFileIfNotExits(paramFlowRulePath);

        // 流控规则
        ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
                flowRulePath,
                flowRuleListParser
        );
        FlowRuleManager.register2Property(flowRuleRDS.getProperty());
        WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
                flowRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);

        // 降级规则
        ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
                degradeRulePath,
                degradeRuleListParser
        );
        DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
        WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
                degradeRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);

        // 系统规则
        ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
                systemRulePath,
                systemRuleListParser
        );
        SystemRuleManager.register2Property(systemRuleRDS.getProperty());
        WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
                systemRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);

        // 授权规则
        ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
                authorityRulePath,
                authorityRuleListParser
        );
        AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
        WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
                authorityRulePath,
                this::encodeJson
        );
        WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);

        // 热点参数规则
        ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
                paramFlowRulePath,
                paramFlowRuleListParser
        );
        ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
        WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
                paramFlowRulePath,
                this::encodeJson
        );
        ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
    }

    private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
            source,
            new TypeReference<List<FlowRule>>() {
            }
    );
    private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
            source,
            new TypeReference<List<DegradeRule>>() {
            }
    );
    private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
            source,
            new TypeReference<List<SystemRule>>() {
            }
    );

    private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
            source,
            new TypeReference<List<AuthorityRule>>() {
            }
    );

    private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
            source,
            new TypeReference<List<ParamFlowRule>>() {
            }
    );

    private void mkdirIfNotExits(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    private void createFileIfNotExits(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            file.createNewFile();
        }
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }
}

2)添加配置
在resources下创建配置目录META-INF/services,
然后在该文件夹下创建文件com.alibaba.csp.sentinel.init.InitFunc
里面的内容是FilePersistence的全路径:com.haha.config.FilePersistence
在这里插入图片描述

注意com.alibaba.csp.sentinel.init.InitFunc这个文件名前后不能有空格,不然可能会提示你No service provider…

然后启动项目,看看是不是能够持久化

先添加一个规则
在这里插入图片描述
访问发现规则起效
在这里插入图片描述
重启项目,再访问,规则如果继续起效,那就是持久化成功了

在这里插入图片描述

1.4、feign整合sentinel

第一步,为shop-order添加sentinel的依赖(之前我们已经添加了)
在这里插入图片描述
第二步,在配置文件中开启Feign对Sentinel的支持

feign:
  sentinel:
    enabled: true

第三步,在service目录下新建fallback目录,创建容错类ProductServiceFallBack

//容错类要求必须实现被容错的接口,并为每个方法实现容错方案
import com.haha.domain.Product;
import com.haha.service.ProductFeignService;
import org.springframework.stereotype.Service;


//这是一个容错类  需要实现fegin所在的接口,并去实现接口中的所有方法
//一旦fegin远程调用出现问题,就会进入当前类中同名方法,执行容错逻辑

@Service
public class ProductServiceFallback implements ProductFeignService {
    @Override
    public Product findByPid(Integer pid) {
        // 容错逻辑
        Product product = new Product();
        product.setPid(-100);
        product.setPname("远程调用商品微服务出现异常了,进入了容错逻辑");
        return product;
    }
}

第四步,为 FeignClient 所在的接口指定容错类

//value用于指定调用nacos下哪个微服务
//fallback用于指定容错类
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)//声明调用的提供者的name
public interface ProductFeignService{
    //指定调用提供者的哪个方法
    //@FeignClient+@GetMapping 就是一个完整的请求路径 http://serviceproduct/product/{pid}
    @GetMapping(value = "/product/{pid}")
    Product findByPid (@PathVariable("pid") Integer pid);
}

在这里插入图片描述

当fegin进行远程调用的过程中,如果出现问题,就进入这个容错类中的方法

第五步,接下来在OrderController.java中判断,如果调用到容错逻辑返回的内容,就返回一个“下单失败”的order

首先打开之前注释掉的@RestController
在这里插入图片描述
打开后idea会报这个错误,不用理会
在这里插入图片描述
写代码
在这里插入图片描述
然后重启shop-order
访问http://localhost:8091/order/prod/1
在这里插入图片描述
虽然我们现在进入了容错类的逻辑,但是我们并不知道出错原因是什么,
接下我们新建一个类ProductServiceFallBackFactory.java,把报错原因打出来

import com.haha.domain.Product;
import com.haha.service.ProductService;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

// FallbackFactory<ProductService> 这个接口用来抛出容错类的异常
@Slf4j
@Service
public class ProductServiceFallBackFactory implements FallbackFactory<ProductService> {
    @Override
    public ProductService create(Throwable throwable) {

        return new ProductService() {
            @Override
            public Product findByPid(Integer pid) {
                log.error("fegin容错类抛出的异常:{}",throwable);

                // 容错逻辑
                Product product = new Product();
                product.setPid(-100);
                product.setPname("远程调用商品微服务出现异常了,进入了容错逻辑");
                return product;
            }
        };
    }
}

然后ProductFeignService中加入fallbackFactory
在这里插入图片描述
接下来把两个shop-product停掉,观察容错效果
重启shop-order
再访问
在这里插入图片描述
后台就会出现对应的异常信息
在这里插入图片描述

2、总结一下前面的知识

1、nacos 注册中心 :作为springcloud的服务治理、注册中心,使用它首先要下载一个nocos-server服务端。然后在微服务里面使用nocos-client 。

  • nocos-server下载后解压,进入bin目录,服务端启动指令startup.cmd -m standalone

后台访问地址:http://127.0.0.1:8848/nacos/index.html(账号密码都是nacos)

  • nocos-client在各个微服务里,先导入依赖,再向其启动类加入@EnableDiscoveryClient注解。
  • 在各个微服务配置文件配置nacos服务地址。
  • 最后在控制类里就可以使用nacos调用微服务了。

2、Ribbon 负载均衡 (接口选择工具):Ribbon有助于控制HTTP和TCP的客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。

做接口选择(接口选择时就是在做负载均衡),使用Ribbon不需要额外加入依赖

  • 启动类里,在restTemplate()上添加注解@LoadBalanced
  • 接下来我们就可以把微服务的 “ IP+端口 ” 改为 “微服务名称”
  • 如果要更改接口选择策略(负载均衡策略),在配置里面写service-product.ribbon.NFLoadBalancerRuleClassName: com.netflix.loadbalancer.策略类

3、Fegin http客户端(接口调用工具):Nacos很好的兼容了fegin,fegin内部集成了Ribbon。

feign 和 ribbon 是两个实现软负载均衡的组件,Feign 是在 Ribbon 的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建 http 请求。

  • 导入依赖,往启动类添加 @EnableFeignClients 注解
  • 然后添加一个需要调用的服务的“服务类”,在该类上添加
    @FeignClient(value = “服务名”)注解,就可以直接通过该类调用服务

4、Sentinel 服务容错(解决高并发问题):以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

  • 下载sentinel 控制台,启动指令:java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.jar

后台访问地址:http://localhost:8080/(账号密码都是sentinel )

  • 在微服务中配置Sentinel的交流端口、控制台地址

3、Gateway (客户端网关API)

Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。

Gateway优点:
1、性能好:时Zuul的1.6倍
2、内置了很多功能:转发、监控、限流
3、设计优雅:容易扩展

缺点:
1、其实现依赖Netty与WebFlux,不是传统的servlet模型,学习成本高
2、不能将其部署在Tomcat、Jetty等servlet容器里,只能打包成jar包执行
3、需要spring boot 2.0以上
在这里插入图片描述

3.1、Gateway模块搭建

搭建 Gateway模块 需要注意的是不要导入web

第一步:创建模块api-gateway

在这里插入图片描述
第二步:导入依赖

    <dependencies>
		<!--   gateway  注意此模块不能引入starter-web   -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>

第三步:新建启动类ApiGatewayApplication.java

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

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

第四步:新建配置文件application.yaml
routes: 路由,其下的值是一个列表,有四个值id、uri、order、predicates。
id:当前路由发的标识,要求唯。
uri:请求最终要求被发送到的地址。
order:路由优先级级,数字越小优先级越高。
predicates:断言,请求要满足的条件。
filters:过滤器,可以删掉url中的某一段

predicates下面也是一个列表
-Path=/product-serv/** #当请求路径满足path指定的规则时,此路径信息才会正常转发

filters:
- StripPrefix=1 #在请求转发之前去掉一层路径
在这里插入图片描述

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: product_route
          uri: http://localhost:8081
          order: 1
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1

然后开始测试,先把product 跑起来,再把ApiGateway跑起来

在这里插入图片描述
跑起来后先不通过网关,直接访问试试
在这里插入图片描述
再通过网关试试:http://localhost:7000/product-serv/product/1

在这里插入图片描述
能够通过网关请求微服务。

3.2、Gateway与nacos结合

在上面的配置中,我们把uri写死了的,并未通过nacos去管理,接下来我们使用nacos去管理。

第一步,在ApiGateway中加入依赖

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

在这里插入图片描述
第二步,在ApiGatewayApplication上添加注解@EnableDiscoveryClient
在这里插入图片描述
第三步,修改配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: product_route
#          uri: http://localhost:8081
          uri: lb://service-product
          order: 1
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1

在这里插入图片描述
然后重启ApiGatewayApplication,再访问
在这里插入图片描述
相当于把手动填写的服务地址,换成了由nacos提供的服务
在这里插入图片描述
还可以为predicates添加一个Method
在这里插入图片描述

3.3、配置简写

就是不写路由,它有默认的路由实现

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true

重启,访问:
在这里插入图片描述
它的默认uri就是service-“nacos中微服务名”
在这里插入图片描述

3.4、执行流程

在这里插入图片描述
在这里插入图片描述

3.5、断言

Predicate(谓语、断言):路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等。

只有predicates断言下的条件为true时,才会执行路由

内置断言

根据path断言

spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Path=/foo/{segment},/bar/{segment}

断言的类型除了Path还有9种:
第一大类:基于DateTime

  • 1.After 在时间之后 才能路由

  • 2.Before 在时间之前 才能路由

  • 3.Between 在时间区间内才能路由

#After,在该日期以后请求才被匹配
spring.cloud.gateway.routes[0].predicates[1]=After=2019-09-12T17:42:47.789-07:00
#Before,在该日期之前才被匹配
spring.cloud.gateway.routes[0].predicates[1]=Before=2019-09-12T17:42:47.789-07:00
#Between,在两个时间范围内的请求才被匹配
spring.cloud.gateway.routes[0].predicates[1]=Between=2019-09-12T17:42:47.789-07:00,2019-10-12T17:42:47.789-07:00

yaml这么配置

spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        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]

这里时间类是ZonedDateTime,如果想使用其它的时间格式,得需要自己实现一个单独的转化类了。

第二大类:基于Cookie

  • 4.Cookie 路由断言工厂会取两个参数 - cookie 名称对应的的key 和value,当cookie 和 Cookied 断言工厂配置的cookie 一致则匹配成功
spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Cookie=username, wgslcuky

第三大类:基于Header

  • 5.Header 根据header 配置 ,如果携带的header 和 断言工厂的一致则匹配成功,否则匹配失败
spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Header=X-Request-Id, \d+

上面的配置示例表示Header中必须有X-Request-Id,且它的值必须是数字。
如果没有配置正则表达式的值,断言也是返回true(断方只检测带正则表达式的配置)

第四大类:基于Host

  • 6.Host 对请求中Host 进行断言,如果成功则 路由
spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Host=**.somehost.org,**.anotherhost.org,{sub}.myhost.org

Host对应的URI模板变量同样也支持{sub}.myhost.org格式的配置,
如果请求的Header中的Host的值是www.sonmehost.org,abc.somehost.org,api.anotherhost.org,这个断言就会返回true,将请求路由到//service-product
注意,在配置Host的时候,如果Host不是80端口,在配置的时候也需要添加上端口。如:localhost:8080

第五大类:基于请求类型

  • 7.Mehod 对请求的方法 进行配置,如果成功则路由。如 Post,Get
spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Method=GET

如果请求方法是GET才能正确返回

  • 8.Query 对请求参数进行断言,如果成功则路由。如 localhost:80/id?foo:bb

它有两个参数,一个是参数(param),这个是必须的,另一个是可选参数,是一个正则表达式。

spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Query=baz

如果请求的参数中包含baz参数,断言将返回true,

spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - Query=foo, ba.

如果请求的参数中包含foo,且它的值匹配ba.,断言将返回true

  • 9.RemoteAddr 对网段字符串或者ip进行断言 ,成功则路由。
spring:
  cloud:
    gateway:
      routes:
      - id: product_route
        uri: lb://service-product
        predicates:
        - RemoteAddr=192.168.1.1/24

如果请求的客户端的ip地址是192.168.1.1到192.168.1.24的范围,此断言返回true

第六大类:按照权重转发Weight
在这里插入图片描述

自定义断言

首先设定一个场景:假设我们的应用仅仅让age在(min,max)之间的人来访问。

第一步,在配置文件中添加一个Age断言
在这里插入图片描述
第二步,自定义配置类AgeRoutePredicateFactory

注意:这个配置类名不能随便起,开头必须是Age,后面必须是RoutePredicateFactory
必须继承AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config>
Config是一个内部类

首先我们要用到lombok,所以先导入依赖
在这里插入图片描述

AgeRoutePredicateFactory.java

import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

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


// 自定义的路由断言
// 1\名字必须是Age + RoutePredicateFactory
// 2\必须继承自 AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config>

@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {

    // 构造函数
    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }

    // 读取配置文件中的参数值  给他赋值到配置类中的属性上
    public List<String> shortcutFieldOrder() {
        //参数顺序和配置文件中一致
        return Arrays.asList("minAge","maxAge");
    }

    //断言逻辑
    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //接收前台传入的age参数
                String age = serverWebExchange.getRequest().getQueryParams().getFirst("age");

                //1\先判断是否为空
                if(StringUtils.isNotEmpty(age)){
                    int a = Integer.parseInt(age);
                    if(a < config.getMaxAge() && a > config.getMinAge()){
                        return true;
                    }else {
                        return false;
                    }
                }
                //2\如果不为空,再进行路由逻辑判断


                return false;
            }
        };

    }

    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;
        private int maxAge;
    }
}

启动ApiGatewayApplication、ProductApplication
在这里插入图片描述

3.6、过滤器

过滤器作用:在请求传递过程中,对请求和响应做一些手脚
生命周期:Pre 、 Post

pre: 这种过滤器在请求被路由之前调用。利用它做身份验证、在集群中选择微服务、记录调试信息等。
Post:这种过滤器在被路由之后执行,利用它为响应添加标准的HTTP Header、收集信息或指标、将响应从微服务发给客户端
在这里插入图片描述
分类:局部过滤器(作用在某一个路由上)、全局路由器(作用在全部路由上)

局部过滤器

局部过滤器:针对单个路由起作用。

下面是Spring Cloud Gateway内置的过滤器工厂:
在这里插入图片描述
在这里插入图片描述

接下来使用SetStatus修改网页的状态码演示
首先修改之前,我们看看状态码
在这里插入图片描述
修改配置文件

在这里插入图片描述
然后重启ApiGatewayApplication
在这里插入图片描述
除了它内置的这些工厂外,还可以自定义局部过滤器

例,我们实现控制log开启或关闭

第一步修改配置文件,添加Log
在这里插入图片描述
第二步自定义配置类LogGatewayFilterFactory

注意:这个配置类名不能随便起,开头必须是Log,后面必须是GatewayFilterFactory
必须继承AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config>
Config是一个内部类

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
    // 构造函数
    public LogGatewayFilterFactory(){
        super(LogGatewayFilterFactory.Config.class);
    }
    // 读取配置文件中的参数,赋值到配置中
    public List<String> shortcutFieldOrder(){
        return Arrays.asList("consoleLog","cacheLog");
    }
    // 过滤逻辑
    @Override
    public GatewayFilter apply(LogGatewayFilterFactory.Config config){
        return new GatewayFilter(){

            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if(config.isCacheLog()){
                    System.out.println("cache日志开启");
                }
                if (config.isConsoleLog()){
                    System.out.println("Console日志开启");
                }
                return chain.filter(exchange);
            }
        };
    }
    // 配置类
    @Data
    @NoArgsConstructor
    public static class Config{
        private boolean consoleLog;
        private boolean cacheLog;
    }
}

然后重启ApiGatewayApplication、ProductApplication
在这里插入图片描述

全局过滤器

全局过滤器针对所有路由。
在这里插入图片描述
上面是内置全局过滤器,接下来我们自定义全局过滤器

先看一下开发中网关的鉴权逻辑
在这里插入图片描述

首先,客户端请求服务时进过网关,
网关向授权中心请求授权,授权完成给网关颁发token凭证
以后客户端每次请求都会携带token

接下来我们自定义一个全局过滤器AuthGlobalFilter

必须实现 GlobalFilter, Ordered两个接口

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.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;

// 自定义全局过滤器(作用:统一鉴权)
@Slf4j
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    // 过滤器逻辑
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 统一鉴权
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if(!StringUtils.equals("admin",token)){
            // 认证失败
            log.info("全局过滤器:token,认证失败");
            // 设置错误信息状态码
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        // 放行
        return chain.filter(exchange);
    }
    // 标识当前过滤器的优先级,返回值越小 优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

然后重启ApiGatewayApplication
直接访问就会报错
在这里插入图片描述
我们再加上token=admin试试

在这里插入图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐