Sentinel之自定义错误处理

260 阅读5分钟
前置条件

依赖

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

配置yml

server:
  port: 10001
spring:
  application:
    name: cloud-consumer-feign
  cloud:
    nacos:
      discovery:
        namespace: public
        server-addr: localhost:8848
    //[0]    
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
  • [0]处 为配置sentinel,dashboard时sentinel后台管理页面地址,port为sentinel api 端口
实践—自定义错误处理

之前接口在限流或者熔断等之后都会返回一个Blocked by sentinel(flow limiting)。下面就阐述如何自定义返回的内容以便给用户一个友好提示。

@SentinelResource 注解

注意:注解方式埋点不支持 private 方法。

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:

  • value:资源名称,必需项(不能为空)

  • entryType:entry 类型,可选项(默认为 EntryType.OUT

  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:

    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

  • blockHandler演示

    • 代码部分
    @GetMapping("/testB")
    @SentinelResource(value = "testB", blockHandler = "handler")
    public ResponseEntity<String> testB() {
        return ResponseEntity.ok().body("test B port:" + port);
    }
    ​
    public ResponseEntity<String> handler(BlockException exception) {
        return ResponseEntity.status(444).body(exception.getClass().getCanonicalName()+ " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
    }
    
    • dashboard配置

      配置了QPS限流,注意资源名不是 /testB 而是testB,和我们@SentinelResource注解中的value值一样。如果此时资源名使用接口url则发生限流之后不会进入我们的blockHadnler方法

      image-20220826164503706

    • 测试

      可以看到触发流控规则之后,返回的不再是默认的错误提示了

      image-20220826164519998

  • blockHandlerClass演示

    上面的方式会使得错误处理方法和业务代码耦合的太紧密。等日后业务代码多了之后,会变得更加难以管理。这种方式可以实现错误处理与业务分离。

    • 代码部分

      @GetMapping("/testB")
      @SentinelResource(value = "testB", blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handler")
      public ResponseEntity<String> testB() {
          return ResponseEntity.ok().body("test B port:" + port);
      }
      
      public class CustomerBlockHandler {
          // 必须是public
         public static ResponseEntity<String> handler(BlockException exception) {
              return ResponseEntity.status(444).body(exception.getClass().getCanonicalName()+ " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
          }
      ​
      }
      
    • 测试

      image-20220826164546345

  • fallback演示

    • 代码

      @GetMapping("/testC")
      @SentinelResource(value = "testC", fallback = "fallbackHandler")
      public ResponseEntity<String> testC(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test C port:" + port);
      }
      ​
      public ResponseEntity<String> fallbackHandler(String id) {
          return ResponseEntity.status(444).body("我是自定义的异常处理  port:" + port + " id:" + id);
      }
      
    • 测试

      image-20220826164554453

  • fallbackClass演示

    • 代码部分

      @GetMapping("/testD")
      @SentinelResource(value = "testD", fallbackClass = FallbackConfig.class,fallback = "fallbackHandlerInClass")
      public ResponseEntity<String> testD(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test C port:" + port);
      }
      
      public class FallbackConfig {
          public static ResponseEntity<String> fallbackHandlerInClass(String id) {
              return ResponseEntity.status(444).body("我是自定义的异常处理 id:" + id);
          }
      }
      
    • 测试

      image-20220826164614077

  • fallback,blockhandler同时配置

    • 代码部分

      @GetMapping("/testE")
      @SentinelResource(value = "testE", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handler1",fallbackClass = FallbackConfig.class,fallback = "fallbackHandlerInClass")
      public ResponseEntity<String> testE(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test E port:" + port);
      }
      
      // 规则一场处理类
      public class CustomerBlockHandler {
          public static ResponseEntity<String> handler1(String id, BlockException exception) {
              return ResponseEntity.status(444).body(exception.getClass().getCanonicalName() + " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
          }
      }
      
      // 异常处理类
      public class FallbackConfig {
          public static ResponseEntity<String> fallbackHandlerInClass(String id) {
              return ResponseEntity.status(444).body("我是自定义的异常处理 id:" + id);
          }
      }
      
  • 全局配置处理

    有没有感觉这样很烦每个都要处理,那么这就是究极偷懒的方法。

    • 代码部分

      实现BlockExceptionHandler再注入springIOC容器即可

      @GetMapping("/testF")
      // 没有@SentinelResource,所以资源名就是url
      public ResponseEntity<String> testF() {
          throw new IllegalArgumentException();
      }
      
      @Configuration
      public class GlobalBlockHandler implements BlockExceptionHandler {
      ​
          @Override
          public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
              if (e instanceof FlowException) {
                  //流控规则异常
                  response.setStatus(445);
      ​
                  PrintWriter out = response.getWriter();
                  out.print("i am custom BlockException handler");
                  out.flush();
                  out.close();
              }
      ​
              if (e instanceof AuthorityException) {
                  //授权规则异常
                  System.out.println("i am flow AuthorityException");
              }
      ​
              if (e instanceof DegradeException) {
                  // 熔断规则
                  System.out.println("i am flow DegradeException");
              }
      ​
      ​
              if (e instanceof ParamFlowException) {
                  // 热点规则
                  System.out.println("i am flow ParamFlowException");
              }
      ​
              if (e instanceof SystemBlockException) {
                  // 系统规则
                  System.out.println("i am flow SystemBlockException");
              }
          }
      }
      
    • dashboard配置

      image-20220826164627309

    • 测试

      image-20220826164634710

  • 总结

    • blockHandler/blockHandler负责配置违规

    • fallback/fallbackClass负责业务异常

    • 如果两个都配置了,则被限流降级而抛出的BlockException时只会进入blockHandler处理逻辑

    • 本质上违反了配置规则就会抛出BlockException,而此时若配置了blockHandler就会调用blockhandler指定的方法处理这个异常的。若没有配置blockhandler则所有异常则归fallback处理

    • 实在不想写可以配置全局异常处理。

    • exceptionsToIgnore里的异常不会处理

      @GetMapping("/testE")
      @SentinelResource(value = "testE",fallbackClass=FallbackConfig.class,fallback"fallbackHandlerInClass",
                        exceptionsToIgnore = {IllegalArgumentException.class})
      public ResponseEntity<String> testE(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test E port:" + port);
      }