OpenFeign + Sentinel 触发降级时的默认策略

299 阅读7分钟

OpenFeign + Sentinel 触发降级时的默认策略

场景Sentinel 做了什么OpenFeign 默认的处理方式
流控 / 熔断 / 降级(即 SphU.entry(...)BlockException 拦截)产生 BlockException(如 FlowExceptionDegradeExceptionAuthorityException 等)直接抛出该异常,最终表现为 Feign 抛出 FeignException(或 SentinelClientException
如果 Feign 端没有配置 fallbackfallbackFactory,就没有任何“兜底”返回值,调用方只能捕获异常自行处理。

简而言之:默认情况下 没有兜底实现,Sentinel 拦截后会把异常往上抛,Feign 客户端会报错。

下面从源码、配置和实际使用三个层面来说明这个默认行为的来龙去脉,帮助你快速确认和自行扩展。


1️⃣ 代码层面的实现(Sentinel‑Feign 适配器)

Spring Cloud Alibaba 为 Feign 提供了 SentinelFeignInvocationHandler(或 SentinelCircuitBreakerInvocationHandler)来包装 Feign 调用。核心片段如下(精简版):

public class SentinelFeignInvocationHandler implements InvocationHandler {
    private final Feign.Target<T> target;
    private final FallbackFactory<T> fallbackFactory;   // @FeignClient(fallbackFactory=...)
    private final T fallbackInstance;                  // @FeignClient(fallback=...)
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // ① 处理 equals、hashCode、toString 之类的 Object 方法
        // ② 为每一次 Feign 调用生成 Sentinel 资源名,如: "user-service:GET:/user/{id}"
        String resourceName = target.name() + ":" + method.getName();

        try (Entry entry = SphU.entry(resourceName, EntryType.OUT, 1)) {
            // 进入成功 → 正常走 Feign 的 HTTP 调用
            return method.invoke(target, args);
        } catch (BlockException be) {
            // ------------ 进入到“降级/熔断”分支 -----------------
            // ① 有 fallbackFactory → 调用 fallbackFactory.create(be) → 走 fallback 方法
            if (fallbackFactory != null) {
                T fallback = fallbackFactory.create(be);
                return method.invoke(fallback, args);
            }
            // ② 有 fallback 类 → 直接实例化并调用
            if (fallbackInstance != null) {
                return method.invoke(fallbackInstance, args);
            }
            // ③ **都没有** → 直接把 BlockException 抛出
            //    Feign 框架会把它包装成 FeignException(或 SentinelClientException)
            throw be;
        } catch (Throwable t) {
            // 其它异常(网络错误、序列化错误等)直接向外传播
            throw t;
        }
    }
}

关键点

  • 只有在你显式提供 fallback fallbackFactory ,Sentinel 才会走自定义兜底逻辑。
  • 未提供兜底时BlockException 直接抛出 → Feign 抛异常,这就是默认降级策略。

2️⃣ Spring Cloud Alibaba 的默认配置

配置项默认值说明
feign.sentinel.enabledtrue (在引入 spring-cloud-starter-alibaba-sentinel 后默认开启)是否打开 Sentinel 对 Feign 的拦截
feign.sentinel.fallback.enabledfalse是否强制要求每个 Feign 客户端必须提供 fallback(默认不强制)
feign.sentinel.block-page.enabledfalse是否使用统一的 block 页面(只对 Web 请求有效)

没有 fallbackfallbackFactory 的 Feign 客户端,默认的降级策略即是“抛异常”。
这也是为什么在使用 Sentinel 时,官方文档会提醒:“若不提供 fallback,Sentinel 限流/降级会直接返回 500 异常”。


3️⃣ 实际使用示例

3.1 只使用 Sentinel(不写 fallback)——会抛异常

@FeignClient(name = "order-service")
public interface OrderClient {
    @GetMapping("/order/{id}")
    OrderDTO getOrder(@PathVariable("id") Long id);
}
  • order-service 触发流控或降级规则时,SphU.entryBlockExceptionFeignException 被抛出。
  • 调用方若不捕获,会导致 HTTP 500(Spring MVC)/异常向上冒泡。

3.2 配置 fallback —— 自定义降级返回

@FeignClient(name = "order-service",
             fallback = OrderClientFallback.class)   // ① fallback 实现类
public interface OrderClient {
    @GetMapping("/order/{id}")
    OrderDTO getOrder(@PathVariable("id") Long id);
}

/** 降级实现 */
@Component
public class OrderClientFallback implements OrderClient {
    @Override
    public OrderDTO getOrder(Long id) {
        // 这里可以返回安全的默认值、缓存数据、或者直接抛业务异常
        return new OrderDTO(id, "default-order", 0);
    }
}
  • 触发 BlockException 时,Sentinel 调用 OrderClientFallback.getOrder → 返回默认对象。
  • 业务方不需要关心异常,即可直接得到“兜底”结果。

3.3 使用 fallbackFactory(可以拿到 BlockException 进行细粒度处理)

@FeignClient(name = "order-service",
             fallbackFactory = OrderClientFallbackFactory.class)
public interface OrderClient {
    @GetMapping("/order/{id}")
    OrderDTO getOrder(@PathVariable("id") Long id);
}

/** 可获取 BlockException 的工厂 */
@Component
public class OrderClientFallbackFactory implements FallbackFactory<OrderClient> {
    @Override
    public OrderClient create(Throwable cause) {
        return new OrderClient() {
            @Override
            public OrderDTO getOrder(Long id) {
                if (cause instanceof FlowException) {
                    // 只针对流控做特殊返回
                    return new OrderDTO(id, "flow-blocked", -1);
                }
                // 其他异常统一返回
                return new OrderDTO(id, "fallback", -2);
            }
        };
    }
}
  • causeBlockException,可根据 instanceof 判断是 流控降级 还是 授权,返回不同的兜底。

4️⃣ 为什么默认是“抛异常”,而不是直接返回 null 或空对象?

  1. 保持 Feign 原有的错误语义:Feign 本身在底层网络错误、超时等情况下会抛异常,业务层往往已经有统一的异常捕获/统一返回体(如 Result<T>)处理逻辑。Sentinel 直接抛异常可以无缝接入这种链路。
  2. 避免误用空对象:在复杂业务场景里,返回 null 或空对象会导致 NPE 隐蔽错误。显式异常能让开发者立即认识到“此请求被 Sentinel 限流/降级”。
  3. 兼容性:在不依赖 Spring Cloud Alibaba(仅使用原生 Sentinel)时,同样是 BlockException 直接抛出,这保持了一致的降级/熔断行为。

5️⃣ 如何自定义默认降级策略(如果你不想每个 Feign 都写 fallback)

5.1 全局 fallback(统一兜底实现)

@Component
public class GlobalSentinelFeignFallbackFactory implements FallbackFactory<Object> {
    @Override
    public Object create(Throwable cause) {
        // 返回一个动态代理,所有方法默认返回 null / 默认值
        return Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class<?>[]{/* 这里填入所有 Feign 接口的父类 */},
                (proxy, method, args) -> {
                    // 根据返回类型返回适当的默认值
                    Class<?> rt = method.getReturnType();
                    if (rt.isPrimitive()) {
                        if (rt == boolean.class) return false;
                        if (rt == void.class) return null;
                        return 0; // byte, short, int, long, float, double
                    }
                    return null; // 对象类型默认 null
                });
    }
}

然后在 @FeignClient 中使用:

@FeignClient(name = "order-service",
             fallbackFactory = GlobalSentinelFeignFallbackFactory.class)
public interface OrderClient { … }

注意:Spring Cloud Alibaba 并没有提供“全局 fallbackFactory” 的自动注入机制,需要在每个 @FeignClient 上显式声明,除非你自定义 SentinelFeignBuilder(见下一小节)。

5.2 替换 SentinelFeignBuilder(更高级)

如果你想让 所有 Feign 客户端默认拥有同一套 fallback(不必在每个 @FeignClient 上写 fallback/fallbackFactory),可以自定义一个 Feign.Builder Bean:

@Configuration
public class SentinelFeignConfiguration {

    @Bean
    public Feign.Builder sentinelFeignBuilder(
            ObjectProvider<FallbackFactory<?>> fallbackFactories,
            // 其他 Sentinel 需要的依赖 ...
    ) {
        return SentinelFeign.builder()
                .fallbackFactory((target, cause) -> {
                    // 这里返回统一的动态代理(类似 5.1 中的实现)
                    return (target.getType()) // target 是 Feign.Target<T>
                            .map(this::createUniversalFallback)
                            .orElseThrow(() -> new IllegalStateException("No fallback"));
                });
    }

    private <T> T createUniversalFallback(Class<T> type) {
        // 用 Proxy 生成统一的默认实现(返回空/null)
        return (T) Proxy.newProxyInstance(
                type.getClassLoader(),
                new Class[]{type},
                (proxy, method, args) -> {
                    Class<?> rt = method.getReturnType();
                    if (rt.isPrimitive()) {
                        if (rt == boolean.class) return false;
                        if (rt == void.class) return null;
                        return 0;
                    }
                    return null;
                });
    }
}

只要把该 Builder 注入到 Spring 容器,后续 @FeignClient 会自动使用它。这样所有的 Feign 客户端在没有自行配置 fallback 时,会走 统一的全局 fallback(返回 null/默认值),而不是直接抛异常。


6️⃣ 小结—默认降级策略要点

项目默认行为
是否需要降级实现不强制;如果未提供 fallback,Sentinel 仅拦截并抛 BlockException
异常类型com.alibaba.csp.sentinel.slots.block.BlockException(子类包括 FlowException, DegradeException, AuthorityException)。
Feign 最终表现FeignException(包装了 BlockException),或直接抛 BlockException(取决于 Feign 版本)。
返回值无返回(除非配置 fallback),调用方只能捕获异常或让统一异常处理器统一返回错误信息。
如何自定义- 在 @FeignClient 上配置 fallbackfallbackFactory
- 通过全局 FallbackFactory / SentinelFeignBuilder 实现统一兜底。
推荐实践对外提供的 Feign 客户端 强烈建议 配置 fallback(或 fallbackFactory),否则在流控/降级时会直接返回 500,影响用户体验。

示例:完整的“默认抛异常 VS 自定义 fallback”对比

// ----------- 1. 没有 fallback(默认) -----------------
@FeignClient(name = "inventory-service")
public interface InventoryClient {
    @GetMapping("/stock/{sku}")
    Integer getStock(@PathVariable("sku") String sku);
}

// 调用
try {
    Integer stock = inventoryClient.getStock("ABC123");
} catch (FeignException e) {
    // 这里会捕获到 BlockException 包装后的异常
    log.warn("库存服务被 Sentinel 限流/降级: {}", e.getMessage());
}

// ----------- 2. 配置了 fallback -----------------
@FeignClient(name = "inventory-service",
             fallback = InventoryClientFallback.class)
public interface InventoryClient {
    @GetMapping("/stock/{sku}")
    Integer getStock(@PathVariable("sku") String sku);
}

@Component
class InventoryClientFallback implements InventoryClient {
    @Override
    public Integer getStock(String sku) {
        // 业务层约定:返回 -1 表示“不可用,使用默认库存”
        return -1;
    }
}

// 调用
Integer stock = inventoryClient.getStock("ABC123"); // 永远返回 -1(降级时)或真实库存

7️⃣ 常见错误 & 排查技巧

症状可能原因排查/解决办法
调用 Feign 时收到 500 Internal Server Error,日志里只看到 BlockException未配置 fallback,Sentinel 拦截后直接抛异常为对应 @FeignClient 配置 fallbackfallbackFactory
想在降级时打印 BlockException 信息,发现 fallback 中拿不到异常对象使用了 fallback(类)而不是 fallbackFactory,后者才能拿到 Throwable 参数改为 fallbackFactory 并在 create(Throwable cause) 中记录日志
多个 Feign 客户端都需要统一的降级返回,手写 fallback 太繁琐全局默认 fallback 未实现参考章节 5.2,通过自定义 SentinelFeignBuilder 实现统一兜底
Sentince 报 NoSuchBeanDefinitionException,提示找不到 fallback bean仍然在 @FeignClient 中声明了 fallbackfallbackFactory,但对应实现类未交给 Spring 管理确保 fallback 实现类加 @Component(或 @Service)注解,或在 @Configuration 中手动 @Bean 注册
想让降级返回 null 而不是抛异常,结果仍然抛异常仍然未配置任何 fallback必须提供 fallback(返回 null),或自行实现全局 fallback 代理

8️⃣ 参考文档链接(便于进一步阅读)

文档链接
Spring Cloud Alibaba Sentinel 官方文档(Feign 集成章节)github.com/alibaba/spr…
SentinelFeignInvocationHandler 源码(GitHub)github.com/alibaba/spr…
Feign fallbackFactory 用法示例cloud.spring.io/spring-clou…
Sentinel BlockException 体系github.com/alibaba/Sen…
Spring Cloud Alibaba Sentinel 2.x 升级指南github.com/alibaba/spr…

🎯 结论

  • 默认降级策略 = 抛异常BlockExceptionFeignException),不返回任何兜底值。
  • 想要真正的“降级返回”,必须在 @FeignClient 上配置 fallbackfallbackFactory,或者通过自定义 SentinelFeignBuilder 实现全局统一的兜底。