【SpringCloud】Sentinel 组件:blockHandler vs fallback


在Sentinel的异常处理机制中,blockHandler与fallback是两个高频使用,但极易混淆的核心配置,二者虽均用于异常兜底、避免服务崩溃或返回不友好响应,但在触发场景、处理范围、语法规范、使用优先级上截然不同,若使用不当会导致兜底失效、异常无法正常拦截等问题。

复制代码
======= 🌟 青柠来相伴,代码更简单。🌟 =======
📚 本文所有内容,我都整理在了 青柠合集 里。👇
🎯 搜索关注【青柠代码录】,即可查看所有合集文章 ~
======= 🌟 ================ 🌟 =======

一、Sentinel异常处理核心场景与执行流程

在讲解二者区别前,先明确Sentinel的两类核心异常场景及整体执行流程,这是理解blockHandler与fallback的基础:

流量控制/熔断降级异常(平台级拦截)

由Sentinel的五大核心规则(限流、熔断、热点、系统保护、授权规则)触发,抛出BlockException及其子类,具体包括: 此类异常属于"平台级拦截",触发时机早于业务逻辑执行,即Sentinel先拦截请求,再判断是否执行业务代码,因此业务逻辑未执行或未执行完毕。

  1. FlowException:限流异常(请求QPS/线程数超出阈值);
  2. DegradeException:熔断异常(服务调用失败率、响应时间超出阈值,触发熔断);
  3. ParamFlowException:热点参数限流异常(指定热点参数的请求超出阈值);
  4. SystemException:系统保护异常(系统负载、CPU、内存超出阈值,触发系统保护);
  5. AuthorityException:授权异常(请求来源未通过授权校验)。

业务逻辑异常(业务级错误)

业务代码执行过程中抛出的各类异常,包括但不限于: 此类异常属于"业务级错误",此时Sentinel规则未触发,业务逻辑已执行但出现异常,需通过兜底逻辑避免异常扩散。

  1. 基础异常: NullPointerException(空指针)、 IllegalArgumentException(参数非法);
  2. 业务异常:自定义业务异常(如 OrderNotFoundException订单不存在、 StockInsufficientException库存不足);
  3. 外部依赖异常: SQLException(数据库异常)、 FeignException(远程调用异常)、 TimeoutException(超时异常)。

Sentinel异常处理执行流程

请求进入Sentinel保护的资源 → Sentinel执行规则校验 → 若触发规则(限流/熔断等)→ 抛出BlockException → 优先执行blockHandler(若配置)→ 若未配置blockHandler → 执行fallback(若配置)→ 若均未配置 → 抛出原始BlockException

若未触发Sentinel规则 → 执行业务逻辑 → 业务逻辑抛出异常 → 执行fallback(若配置)→ 若未配置fallback → 抛出原始业务异常;

blockHandler与fallback的核心区别,本质就是对这两类异常的处理分工不同------前者专注处理平台级拦截异常,后者专注处理业务级错误,二者协同可构建完善的微服务容错体系,覆盖从请求拦截到业务执行的全链路异常兜底。

二、blockHandler 详解

2.1 核心定义

blockHandler是Sentinel提供的「平台级异常处理机制」,专门用于处理因Sentinel规则触发的BlockException及其子类异常,核心作用是当请求被限流、熔断、热点拦截、系统保护或授权拦截时,提供统一的兜底响应(如友好提示、默认数据),避免直接抛出原始异常给前端。

核心特点:仅响应Sentinel规则触发的异常,不处理业务逻辑本身的异常;触发时机早于业务逻辑执行(或业务逻辑未执行);优先级高于fallback;兜底方法需严格遵循语法规范,否则无法生效。

2.2 语法规范

blockHandler的配置依赖@SentinelResource注解(Sentinel核心注解,用于定义受保护的资源),需严格遵循以下规范:

  1. 注解配置 :在需要保护的资源(接口方法、业务方法)上添加 @SentinelResource注解,通过 blockHandler属性指定兜底方法名(必须与兜底方法名完全一致,区分大小写);若兜底方法在其他类中,需配合 blockHandlerClass属性指定类对象(如 blockHandlerClass = SentinelBlockHandler.class)。
  2. 方法访问权限 :兜底方法必须是 public修饰(Sentinel底层通过反射调用,private/protected会导致反射失败);若配合 blockHandlerClass使用,兜底方法必须额外添加 static修饰(否则无法被Sentinel解析和调用)。
  3. 参数列表 :兜底方法的参数列表必须与原资源方法完全一致,且在最后额外添加一个 BlockException类型的参数,用于接收触发的具体异常(可通过该参数判断是限流、熔断还是其他平台级异常,从而返回更精准的提示)。
  4. 返回值 :兜底方法的返回值,必须与原资源方法的返回值完全一致(包括泛型类型),确保前后端响应格式统一,避免前端解析异常(如原方法返回 Result<OrderDTO>,兜底方法也必须返回 Result<OrderDTO>)。
  5. 方法位置 :默认情况下,兜底方法需与原资源方法在同一个类中;若指定 blockHandlerClass,则兜底方法需在该类中,且为静态方法;兜底方法名不可与原资源方法名重复。
  6. 异常捕获范围 :仅能捕获 BlockException及其子类,无法捕获业务逻辑异常(如NullPointerException),即使在兜底方法中捕获了业务异常,也无法生效。

2.3 实战案例

场景1:订单提交接口(高并发场景),配置Sentinel限流规则(QPS上限100)、熔断规则(失败率上限50%,熔断时长10秒),当请求超出限流阈值或触发熔断时,通过blockHandler返回统一的兜底提示,同时区分不同类型的BlockException,返回更精准的响应。

第一步:引入Sentinel依赖(SpringBoot+微服务项目)

复制代码
<!-- Sentinel 核心依赖(核心功能,必引) -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.6</version> <!-- 常用稳定版,兼容SpringBoot 2.6.x+ -->
</dependency>
<!-- Sentinel 注解支持(必需,用于@SentinelResource注解解析) -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.6</version>
</dependency>
<!-- SpringBoot 整合 Sentinel(自动配置,简化集成) -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.5.0</version> <!-- 兼容Spring Cloud Alibaba 2021版本 -->
</dependency>
<!-- Sentinel 控制台依赖(可选,用于可视化配置规则、监控流量,开发常用) -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.8.6</version>
</dependency>

第二步:配置Sentinel控制台(可选,可视化管理规则)

复制代码
# application.yml 配置
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080 # 控制台地址(需先启动Sentinel控制台)
        port: 8719 # 客户端与控制台通信端口,默认8719,若冲突可修改
      web-context-unify: false # 关闭web上下文统一,避免资源标识重复

第三步:编写资源方法与blockHandler兜底方法(同包同类,基础用法)

复制代码
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单接口 - Sentinel保护示例(blockHandler实战,基础用法)
 */
@RestController
public class OrderController {

    /**
     * 订单提交接口(高并发核心接口)
     * @SentinelResource:定义Sentinel资源,value为资源唯一标识(必需,不可重复)
     * blockHandler:指定限流/熔断等平台级异常的兜底方法名
     */
    @PostMapping("/order/submit")
    @SentinelResource(value = "orderSubmitResource", blockHandler = "orderSubmitBlockHandler")
    public Result<String> submitOrder(@RequestParam String orderId, @RequestParam String userId) {
        // 模拟业务逻辑:订单校验、库存扣减、支付回调、日志记录
        System.out.println("订单[" + orderId + "]提交中,用户ID:" + userId);
        // 模拟业务异常(用于测试:blockHandler不会处理该异常)
        // if ("TEST_ERROR".equals(orderId)) {
        //     throw new NullPointerException("订单ID为空异常");
        // }
        return Result.success("订单提交成功", orderId);
    }

    /**
     * blockHandler兜底方法(处理Sentinel规则触发的BlockException)
     * 规范:public、参数与原方法一致+BlockException、返回值与原方法一致
     * @param orderId 原方法参数1
     * @param userId 原方法参数2
     * @param e BlockException异常(用于区分异常类型,精准返回提示)
     * @return 统一响应结果(符合前后端分离格式)
     */
    public Result<String> orderSubmitBlockHandler(String orderId, String userId, BlockException e) {
        // 区分不同类型的BlockException,返回更精准的提示(开发常用场景)
        String message = "系统繁忙,请稍后再试(平台保护)";
        if (e instanceof FlowException) {
            message = "订单提交过于频繁,请10秒后重试(限流保护,QPS已达上限)";
        } else if (e instanceof DegradeException) {
            message = "订单服务暂时降级,请10秒后重试(熔断保护,服务调用失败率过高)";
        } else if (e instanceof ParamFlowException) {
            message = "热点参数请求过于频繁,请稍后再试(热点限流保护)";
        } else if (e instanceof SystemException) {
            message = "系统负载过高,请稍后再试(系统保护)";
        } else if (e instanceof AuthorityException) {
            message = "请求来源未授权,无法提交订单(授权保护)";
        }
        // 开发优化:打印异常日志,便于排查问题(不影响前端响应)
        System.err.println("订单提交触发平台级保护,订单ID:" + orderId + ",异常类型:" + e.getClass().getSimpleName());
        // 返回统一响应格式,状态码503(服务不可用,符合平台级异常规范)
        return Result.fail(503, message);
    }
}

// 统一响应结果类(开发通用,可直接复用)
class Result<T> {
    private int code;
    private String message;
    private T data;

    // 成功响应(带数据)
    public static <T> Result<T> success(String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage(message);
        result.setData(data);
        return result;
    }

    // 成功响应(不带数据)
    public static <T> Result<T> success(String message) {
        return success(message, null);
    }

    // 失败响应(带状态码和提示)
    public static <T> Result<T> fail(int code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    // getter/setter 省略(开发中需完善)
    public int getCode() { return code; }
    public void setCode(int code) { this.code = code; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}

第四步:配置Sentinel限流+熔断规则(代码方式,常用,支持动态配置,无需依赖控制台)

复制代码
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
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 org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

/**
 * Sentinel 规则配置类(实战:代码配置,支持动态更新,无需控制台)
 */
@Configuration
public class SentinelRuleConfig {

    /**
     * 初始化限流规则+熔断规则(@PostConstruct:容器启动后自动执行)
     */
    @PostConstruct
    public void initRules() {
        // 1. 初始化限流规则
        initFlowRules();
        // 2. 初始化熔断规则
        initDegradeRules();
    }

    /**
     * 初始化限流规则:订单提交接口QPS上限100
     */
    private void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();
        // 创建限流规则对象
        FlowRule flowRule = new FlowRule();
        // 指定需要限流的资源(与@SentinelResource的value完全一致)
        flowRule.setResource("orderSubmitResource");
        // 限流类型:QPS(每秒请求数),可选:0=线程数限流,1=QPS限流
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 限流阈值:100 QPS(根据实际业务调整)
        flowRule.setCount(100);
        // 限流策略:默认直接拒绝(可选:WARM_UP(预热)、RATE_LIMITER(匀速排队))
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
        // 添加规则到规则管理器
        rules.add(flowRule);
        FlowRuleManager.loadRules(rules);
    }

    /**
     * 初始化熔断规则:订单提交接口失败率上限50%,熔断时长10秒
     */
    private void initDegradeRules() {
        List<DegradeRule> rules = new ArrayList<>();
        // 创建熔断规则对象
        DegradeRule degradeRule = new DegradeRule();
        // 指定需要熔断的资源(与@SentinelResource的value完全一致)
        degradeRule.setResource("orderSubmitResource");
        // 熔断策略:失败率(可选:失败率、响应时间)
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
        // 失败率阈值:50%(请求失败率超过50%触发熔断)
        degradeRule.setCount(0.5);
        // 熔断时长:10秒(熔断后,10秒内不再调用该资源,之后尝试恢复)
        degradeRule.setTimeWindow(10);
        // 最小请求数:10(只有当请求数达到10时,才会计算失败率,避免少量请求误触发熔断)
        degradeRule.setMinRequestAmount(10);
        // 统计时长:1000毫秒(统计最近1秒的请求失败率)
        degradeRule.setStatIntervalMs(1000);
        // 添加规则到规则管理器
        rules.add(degradeRule);
        DegradeRuleManager.loadRules(rules);
    }
}

测试效果(3种核心场景):

  1. 限流触发:当请求QPS超过100时,请求被Sentinel拦截,触发 orderSubmitBlockHandler方法,返回"订单提交过于频繁,请10秒后重试(限流保护,QPS已达上限)",避免直接抛出 FlowException异常;
  2. 熔断触发:当订单提交接口失败率超过50%(且请求数≥10),触发熔断,请求被拦截,返回"订单服务暂时降级,请10秒后重试(熔断保护,服务调用失败率过高)";
  3. 业务异常测试:若放开代码中"模拟业务异常"的注释,当orderId为"TEST_ERROR"时,抛出NullPointerException,此时blockHandler不生效(因不处理业务异常),若未配置fallback,会直接抛出该异常。

2.4 进阶用法:blockHandlerClass(解耦兜底逻辑,推荐)

当多个资源(接口)需要共用相同的blockHandler逻辑时(如多个接口的限流、熔断兜底提示一致),可将兜底方法抽取到独立的处理类中,通过blockHandlerClass指定,实现业务代码与兜底逻辑的解耦,提升代码可维护性(开发推荐做法,避免代码冗余)。

复制代码
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemException;

/**
 * Sentinel 统一BlockException处理类(抽取通用兜底逻辑,供多个资源复用)
 */
public class SentinelBlockHandler {

    /**
     * 通用限流/熔断兜底方法(必须是static,否则无法被Sentinel解析)
     * 适用于所有返回Result<String>类型的资源方法(可根据实际返回值重载)
     * @param orderId 原方法参数1(与原资源方法参数一致)
     * @param userId 原方法参数2(与原资源方法参数一致)
     * @param e BlockException异常(用于区分异常类型)
     * @return 统一响应结果
     */
    public static Result<String> commonBlockHandler(String orderId, String userId, BlockException e) {
        String message = "系统繁忙,请稍后再试(平台保护)";
        if (e instanceof FlowException) {
            message = "请求过于频繁,请10秒后重试(限流保护)";
        } else if (e instanceof DegradeException) {
            message = "服务暂时降级,请10秒后重试(熔断保护)";
        } else if (e instanceof ParamFlowException) {
            message = "热点参数请求过于频繁,请稍后再试";
        } else if (e instanceof SystemException) {
            message = "系统负载过高,请稍后再试";
        } else if (e instanceof AuthorityException) {
            message = "请求来源未授权,无法操作";
        }
        // 打印异常日志,便于排查
        System.err.println("触发平台级保护,参数:orderId=" + orderId + ", userId=" + userId + ",异常:" + e.getMessage());
        return Result.fail(503, message);
    }

    /**
     * 重载兜底方法(适用于返回Result<OrderDTO>类型的资源方法)
     * 解决不同返回值类型的资源共用兜底逻辑的问题
     */
    public static Result<OrderDTO> commonBlockHandlerForOrder(String orderId, BlockException e) {
        String message = "系统繁忙,请稍后再试(平台保护)";
        if (e instanceof FlowException) {
            message = "请求过于频繁,请10秒后重试(限流保护)";
        }
        // 兜底返回默认订单信息,避免返回null
        OrderDTO defaultOrder = new OrderDTO();
        defaultOrder.setOrderId(orderId);
        defaultOrder.setOrderStatus("请求被拦截,暂无法处理");
        return Result.success(message, defaultOrder);
    }
}

// 改造OrderController中的@SentinelResource注解(复用通用兜底方法)
@PostMapping("/order/submit")
@SentinelResource(
        value = "orderSubmitResource",
        blockHandler = "commonBlockHandler", // 指定独立类中的兜底方法名
        blockHandlerClass = SentinelBlockHandler.class // 指定兜底方法所在类
)
public Result<String> submitOrder(@RequestParam String orderId, @RequestParam String userId) {
    // 业务逻辑不变
    System.out.println("订单[" + orderId + "]提交中,用户ID:" + userId);
    return Result.success("订单提交成功", orderId);
}

// 新增订单查询接口(复用通用兜底方法,不同返回值类型)
@GetMapping("/order/query")
@SentinelResource(
        value = "orderQueryResource",
        blockHandler = "commonBlockHandlerForOrder",
        blockHandlerClass = SentinelBlockHandler.class
)
public Result<OrderDTO> queryOrder(@RequestParam String orderId) {
    // 业务逻辑
    OrderDTO orderDTO = new OrderDTO();
    orderDTO.setOrderId(orderId);
    orderDTO.setUserId("10086");
    orderDTO.setOrderStatus("已支付");
    return Result.success("查询成功", orderDTO);
}

三、fallback 详解

3.1 核心定义

fallback是Sentinel提供的「业务级异常处理机制」,专门用于处理业务逻辑执行过程中,抛出的异常(不包括BlockException,若未配置blockHandler,才会处理BlockException)。

核心作用是当业务代码出现错误(如数据库异常、远程调用失败、自定义业务异常)时,提供兜底逻辑(如返回默认数据、友好提示),避免服务崩溃或返回不友好的异常信息,保证业务链路的稳定性。

核心特点:仅响应业务逻辑异常,不响应Sentinel规则触发的异常(除非未配置blockHandler);触发时机晚于业务逻辑执行(业务逻辑已执行但出错);优先级低于blockHandler;兜底方法语法规范相对灵活,无需强制添加异常参数。

3.2 语法规范

fallback同样依赖@SentinelResource注解,语法规范与blockHandler有相似之处,但核心差异明显,结合实战总结如下(重点区分二者差异):

  1. 注解配置 :通过@SentinelResourcefallback属性指定兜底方法名(区分大小写,与兜底方法名完全一致);若兜底方法在其他类中,需配合fallbackClass属性指定类对象(如fallbackClass = SentinelFallback.class);1.8.0+版本可通过defaultFallback配置通用兜底方法。
  2. 方法访问权限 :兜底方法无需强制是public修饰(private、protected均可,Sentinel反射可调用);若配合fallbackClass使用,兜底方法必须是static修饰(与blockHandlerClass要求一致)。
  3. 参数列表 :兜底方法的参数列表与原资源方法完全一致,或在最后额外添加一个Throwable类型的参数(用于接收业务异常信息,便于打印日志、排查问题),无需强制添加异常参数(这是与blockHandler的核心差异之一)。
  4. 返回值:与原资源方法的返回值完全一致(包括泛型类型),确保前后端响应格式统一,避免前端解析异常。
  5. 异常范围
  6. 默认处理:所有业务异常(如NullPointerException、SQLException、自定义异常),以及BlockException(若未配置blockHandler);
  7. 排除异常:可通过exceptionsToIgnore属性排除指定异常(如参数校验异常,无需兜底,直接抛出);
  8. 注意:部分Sentinel版本(1.6.0-1.7.0)无法排除BlockException,即使配置了exceptionsToIgnore = {BlockException.class},也无法生效。
  9. 方法位置 :默认与原资源方法在同一个类中;若指定fallbackClass,则兜底方法需在该类中,且为静态方法;可与原资源方法名重复(但不推荐,避免混淆)。

3.3 实战案例

场景2:订单查询接口,业务逻辑中需查询数据库(可能出现数据库连接超时、订单不存在等异常),同时调用远程用户服务(可能出现远程调用失败、超时异常),通过fallback提供兜底响应(返回默认数据或友好提示),同时排除参数校验异常(无需兜底)。

复制代码
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单接口 - Sentinel保护示例(fallback实战,多异常场景)
 */
@RestController
public class OrderQueryController {

    // 模拟远程用户服务(开发中通常是Feign调用)
    @Autowired
    private UserService userService;

    /**
     * 订单查询接口
     * fallback:指定业务异常兜底方法名
     * exceptionsToIgnore:排除不需要处理的异常(此处排除参数校验异常)
     * 注:未配置blockHandler,若触发Sentinel规则,会触发fallback
     */
    @GetMapping("/order/query")
    @SentinelResource(
            value = "orderQueryResource",
            fallback = "orderQueryFallback",
            exceptionsToIgnore = {IllegalArgumentException.class}
    )
    public Result<OrderDTO> queryOrder(@RequestParam String orderId) {
        // 1. 参数校验(异常被exceptionsToIgnore排除,不进入fallback)
        if (orderId == null || orderId.isEmpty()) {
            throw new IllegalArgumentException("订单ID不能为空(参数校验失败)");
        }
        // 2. 模拟数据库查询异常(如连接超时、订单不存在)
        if ("DB_ERROR".equals(orderId)) {
            throw new RuntimeException("数据库连接超时,无法查询订单");
        }
        if ("NOT_FOUND".equals(orderId)) {
            throw new OrderNotFoundException("订单不存在,订单ID:" + orderId); // 自定义业务异常
        }
        // 3. 模拟远程调用异常(调用用户服务失败)
        String userName = userService.getUserName("10086"); // 远程调用
        if (userName == null) {
            throw new FeignException("远程调用用户服务失败,无法获取用户名");
        }
        // 4. 正常业务逻辑:返回订单信息
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setOrderId(orderId);
        orderDTO.setUserId("10086");
        orderDTO.setUserName(userName);
        orderDTO.setOrderStatus("已支付");
        return Result.success("查询成功", orderDTO);
    }

    /**
     * fallback兜底方法(处理业务逻辑异常)
     * 规范:参数与原方法一致,返回值与原方法一致,可添加Throwable参数接收异常
     * @param orderId 原方法参数
     * @param e 业务异常(可选,用于打印异常日志、排查问题)
     * @return 兜底响应(返回默认订单信息,避免返回异常)
     */
    private Result<OrderDTO> orderQueryFallback(String orderId, Throwable e) {
        // 开发常用:打印异常日志(包含异常堆栈,便于排查)
        System.err.println("订单查询业务异常,订单ID:" + orderId);
        e.printStackTrace();
        // 兜底逻辑:返回默认订单信息(根据业务需求调整,避免返回null)
        OrderDTO defaultOrder = new OrderDTO();
        defaultOrder.setOrderId(orderId);
        defaultOrder.setUserId("unknown");
        defaultOrder.setUserName("未知用户");
        defaultOrder.setOrderStatus("查询异常,暂无法获取订单信息");
        return Result.success("查询异常,已返回默认信息", defaultOrder);
    }

    // 自定义业务异常(开发常用,区分不同业务错误)
    static class OrderNotFoundException extends RuntimeException {
        public OrderNotFoundException(String message) {
            super(message);
        }
    }

    // 模拟远程调用异常(简化,对应Feign调用异常)
    static class FeignException extends RuntimeException {
        public FeignException(String message) {
            super(message);
        }
    }

    // 模拟远程用户服务(简化)
    static class UserService {
        public String getUserName(String userId) {
            // 模拟远程调用失败
            if ("10086".equals(userId)) {
                return "青柠代码录";
            }
            return null;
        }
    }
}

// 订单DTO(开发常用实体类,完善getter/setter)
class OrderDTO {
    private String orderId;
    private String userId;
    private String userName;
    private String orderStatus;

    // getter/setter 完善
    public String getOrderId() { return orderId; }
    public void setOrderId(String orderId) { this.orderId = orderId; }
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getUserName() { return userName; }
    public void setUserName(String userName) { this.userName = userName; }
    public String getOrderStatus() { return orderStatus; }
    public void setOrderStatus(String orderStatus) { this.orderStatus = orderStatus; }
}

测试效果(4种核心场景):

  1. 数据库异常:当orderId为"DB_ERROR"时,抛出RuntimeException,触发fallback方法,返回默认订单信息,日志打印异常堆栈;
  2. 自定义业务异常:当orderId为"NOT_FOUND"时,抛出OrderNotFoundException,触发fallback方法,返回默认订单信息;
  3. 远程调用异常:当userService返回null时,抛出FeignException,触发fallback方法,返回默认订单信息;
  4. 参数校验异常:当orderId为空时,抛出IllegalArgumentException,该异常被 exceptionsToIgnore排除,不触发fallback,直接抛出异常给前端;
  5. 限流触发(未配置blockHandler):若给该接口配置限流规则,触发限流时,会抛出FlowException,因未配置blockHandler,会触发fallback方法,返回默认订单信息。

3.4 进阶用法:fallbackClass + defaultFallback

  1. fallbackClass:与blockHandlerClass类似,用于将fallback兜底方法抽取到独立的类中,实现业务代码与兜底逻辑的解耦,兜底方法需为static修饰,适用于多个资源共用同一兜底逻辑的场景;
  2. defaultFallback:Sentinel 1.8.0及以上版本新增,用于配置通用的fallback逻辑(适用于多个资源共用同一兜底逻辑,且无需区分参数),语法规范如下:
  • 返回值与原资源方法一致;

  • 参数列表可为空,或仅添加一个 Throwable参数(用于接收异常信息);

  • 若同时配置fallback和defaultFallback,仅fallback生效(fallback优先级高于defaultFallback);

  • 若配合fallbackClass使用,defaultFallback方法也需为static修饰。

    import com.alibaba.csp.sentinel.annotation.SentinelResource;

    /**

    • 通用fallback处理类(抽取通用兜底逻辑,供多个资源复用)
      */
      public class SentinelFallback {
      // 通用fallback方法(static,用于fallbackClass,适用于返回Result<OrderDTO>的资源)
      public static Result commonOrderFallback(String orderId, Throwable e) {
      System.err.println("订单业务异常,订单ID:" + orderId + ",异常信息:" + e.getMessage());
      e.printStackTrace();
      OrderDTO defaultOrder = new OrderDTO();
      defaultOrder.setOrderId(orderId);
      defaultOrder.setOrderStatus("查询异常,已兜底");
      return Result.success("业务异常,已返回默认信息", defaultOrder);
      }

      // 通用fallback方法(重载,适用于返回Result<String>的资源)
      public static Result commonStringFallback(String orderId, String userId, Throwable e) {
      System.err.println("业务异常,orderId:" + orderId + ",userId:" + userId);
      return Result.success("业务异常,已兜底", "兜底成功");
      }

      // 默认fallback方法(通用,适用于所有返回Result的资源,无需区分参数)
      public static Result defaultFallback(Throwable e) {
      System.err.println("通用兜底:业务异常,异常信息:" + e.getMessage());
      OrderDTO defaultOrder = new OrderDTO();
      defaultOrder.setOrderId("unknown");
      defaultOrder.setOrderStatus("查询异常,通用兜底");
      return Result.success("业务异常,已兜底", defaultOrder);
      }
      }

    // 改造OrderQueryController中的注解(复用通用兜底方法)
    @GetMapping("/order/query")
    @SentinelResource(
    value = "orderQueryResource",
    fallbackClass = SentinelFallback.class, // 指定fallback所在类
    fallback = "commonOrderFallback", // 指定具体兜底方法(优先级高于defaultFallback)
    defaultFallback = "defaultFallback", // 通用兜底(当前配置下不会生效)
    exceptionsToIgnore = {IllegalArgumentException.class}
    )
    public Result queryOrder(@RequestParam String orderId) {
    // 业务逻辑不变
    }

    // 新增订单取消接口(复用通用兜底方法,不同返回值类型)
    @PostMapping("/order/cancel")
    @SentinelResource(
    value = "orderCancelResource",
    fallbackClass = SentinelFallback.class,
    fallback = "commonStringFallback"
    )
    public Result cancelOrder(@RequestParam String orderId, @RequestParam String userId) {
    // 模拟业务异常
    if ("ERROR".equals(orderId)) {
    throw new RuntimeException("订单取消失败,订单已支付");
    }
    return Result.success("订单取消成功", orderId);
    }

四、核心对比:blockHandler vs fallback

为了方便大家快速区分和记忆,整理了以下对比表格,覆盖核心维度:

对比维度 blockHandler fallback 面试考点补充
核心作用 处理Sentinel规则触发的平台级异常(BlockException及其子类) 处理业务逻辑执行过程中的业务级异常(含自定义异常) 二者分工:平台级拦截用blockHandler,业务级错误用fallback
触发时机 请求被Sentinel拦截,业务逻辑未执行或未执行完毕 业务逻辑已执行,执行过程中抛出异常 触发顺序:blockHandler先于fallback
异常类型 仅处理BlockException及其子类(限流、熔断、热点等) 处理所有业务异常;未配置blockHandler时,也处理BlockException fallback不处理BlockException(有blockHandler时)
语法规范(参数) 必须包含BlockException参数(最后一位) 可选包含Throwable参数(最后一位),非必需 blockHandler参数必须包含BlockException
访问权限 必须是public;配合blockHandlerClass时需为static 无强制要求(private也可);配合fallbackClass时需为static blockHandler必须是public,fallback无要求
优先级 高于fallback(同时触发BlockException时,仅执行blockHandler) 低于blockHandler(未触发BlockException或未配置blockHandler时执行) 核心考点:优先级对比
应用场景 秒杀、大促等高频并发场景,处理限流、熔断兜底 数据库查询、远程调用等业务场景,处理业务异常兜底 结合场景说明二者用法
版本依赖 所有Sentinel版本均支持,无版本限制 1.6.0+支持处理所有业务异常;1.8.0+支持defaultFallback 版本差异是面试高频考点
兜底逻辑要求 轻量,仅返回提示/默认数据,不执行复杂逻辑 轻量,避免二次异常,可打印日志排查问题

补充说明:兜底逻辑的核心原则是"轻量无依赖",无论blockHandler还是fallback,都应避免在兜底方法中执行数据库操作、远程调用等复杂逻辑,否则会导致兜底方法自身抛出异常,反而破坏接口稳定性,这也是开发中兜底逻辑的核心规范。

五、最佳实践

结合前文的语法规范、实战案例及踩坑点,总结开发中blockHandler与fallback的最佳使用方式,覆盖单接口、多接口、复杂场景的适配,确保兜底逻辑生效。

5.1 单接口兜底配置

对于单个核心接口(如订单提交、支付回调),需同时配置blockHandler(处理平台级异常)和fallback(处理业务级异常),明确分工、双重兜底,确保接口在高并发和业务异常场景下均能稳定响应,示例如下:

复制代码
@PostMapping("/order/pay")
@SentinelResource(
        value = "orderPayResource", // 资源唯一标识,建议与接口路径保持一致,便于排查
        blockHandler = "orderPayBlockHandler", // 平台级异常兜底
        fallback = "orderPayFallback", // 业务级异常兜底
        exceptionsToIgnore = {IllegalArgumentException.class} // 排除参数校验异常
)
public Result<String> payOrder(@RequestParam String orderId, @RequestParam String payType) {
    // 1. 参数校验(异常被排除,不进入fallback)
    if (payType == null || !Arrays.asList("WECHAT", "ALIPAY").contains(payType)) {
        throw new IllegalArgumentException("支付方式非法,仅支持微信、支付宝");
    }
    // 2. 业务逻辑:调用支付接口、更新订单状态、记录支付日志
    boolean paySuccess = payService.doPay(orderId, payType);
    if (!paySuccess) {
        throw new PayFailedException("支付失败,请重新尝试"); // 自定义业务异常
    }
    return Result.success("支付成功", orderId);
}

// blockHandler兜底(平台级异常:限流、熔断等)
public Result<String> orderPayBlockHandler(String orderId, String payType, BlockException e) {
    String message = "系统繁忙,请稍后再试";
    if (e instanceof FlowException) {
        message = "支付请求过于频繁,请10秒后重试";
    } else if (e instanceof DegradeException) {
        message = "支付服务暂时降级,请稍后再试";
    }
    System.err.println("支付触发平台级保护,orderId:" + orderId + ",异常:" + e.getClass().getSimpleName());
    return Result.fail(503, message);
}

// fallback兜底(业务级异常:支付失败、自定义异常等)
private Result<String> orderPayFallback(String orderId, String payType, Throwable e) {
    System.err.println("支付业务异常,orderId:" + orderId + ",异常信息:" + e.getMessage());
    e.printStackTrace();
    // 兜底逻辑:返回支付失败提示,引导用户重新支付
    return Result.success("支付异常,请重新尝试支付", orderId);
}

5.2 多接口通用兜底配置

当多个接口(如订单相关、用户相关接口)的兜底逻辑一致时,推荐将blockHandler和fallback兜底方法分别抽取到独立的处理类中,通过blockHandlerClass和fallbackClass引用,实现业务代码与兜底逻辑的解耦,提升代码可维护性,避免重复开发。

核心规范:

  1. 通用blockHandler处理类:存放所有接口的平台级异常兜底方法,方法为static、public,根据返回值类型重载;
  2. 通用fallback处理类:存放所有接口的业务级异常兜底方法,方法为static,可根据返回值类型重载,支持defaultFallback通用兜底;
  3. 资源标识(@SentinelResource的value):建议采用"模块名+接口名"的格式(如order:submit、user:query),便于规则配置和监控排查。

本文由mdnice多平台发布

相关推荐
干到60岁退休的码农11 分钟前
SpringBoot整合Mybatis-plus
spring boot·后端·mybatis
掘金者阿豪13 分钟前
从Oracle迁到金仓,第一个月踩的最大的坑就是它
前端·后端
aramae17 分钟前
C++11:现代C++的里程碑
c语言·开发语言·c++·windows·git·后端
无相求码42 分钟前
为什么你的第二个 scanf 还没输入就直接跳过了?C语言输入缓冲区陷阱
c语言·后端
叼馒女友郭芙蓉42 分钟前
Function Calling理解
后端
玉宇夕落43 分钟前
LangChain RAG Retriever 的学习
后端
用户298698530141 小时前
Python 一键导出 Excel 中的图表和形状为图片
后端·python·excel
用户8130077816371 小时前
PageHelper分页查询步骤
后端
是小李呀1 小时前
MyBatis、MyBatis-Plus 与全自动 ORM 的本质区别
后端
小堂子这厢有礼了1 小时前
Chet.Admin 权限模型:菜单级 + 按钮级 + 行级三层防护
前端·后端·信息可视化·前端框架·.net