不用 @CircuitBreaker 注解,自定义 Resilience4j 熔断器 + AOP 实现

前一篇写了 基于 Feign + Resilience4j 的微服务熔断防雪崩优化方案。这次同一套系统又要加熔断,但走 Feign 那条路走不通了。

三个接口直连 HTTP 调外部数据接口,返回体用业务码表达失败,不抛异常。现成的@CircuitBreaker注解只认异常,用不上,只能自己搭一套。

一、场景

三个接口都调外部数据服务,返回统一封装:

classRemoteResult<T>{privateintcode;// 0=成功,500106=远程服务异常,其他=业务码privateTdata;...}

调用方拿到结果自己判code。HTTP 200 也可能是失败。跟前一篇 Feign 那套差在这里:

  • 上一篇走 Feign,用ErrorDecoder在解码阶段把"系统异常码"转成特殊异常抛给 Resilience4j,异常路径触发熔断。
  • 这一篇直连 HTTP,RestTemplate拿回一个对象,方法里没有异常,熔断器数不到失败次数。

Resilience4j 的@CircuitBreaker注解默认只识别异常。要让它认业务码,得走recordResult回调——一个Predicate<Object>,让你自己定义什么样的返回值算失败。所以这套方案没用注解,而是包了一层自己的装饰器。

二、类图

业务代码只调代理 Bean,注解标在代理 Bean 的方法上,切面拦截并交给底层熔断器执行。

三、调用时序

熔断打开时,切面反射从方法签名RemoteResult<T>里拿到 T,new T()包一个空的RemoteResult.success(...)作为兜底。业务代码看到的永远是同类型的返回值,不用判空、不用 catch。

四、核心代码

注解——空注解,参数全靠约定:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceRemoteCircuitBreakerProtected{}

代理 Bean——集中管理受保护的调用,解决 AOP self-invocation 问题:

@ComponentpublicclassRemoteFacade{@ResourceprivateRemoteUtilremoteUtil;@RemoteCircuitBreakerProtectedpublicRemoteResult<RespA>queryA(ReqAreq){returnremoteUtil.queryA(req);}@RemoteCircuitBreakerProtectedpublicRemoteResult<RespB>queryB(ReqBreq){returnremoteUtil.queryB(req);}// ...}

切面——统一 fallback 反射构造:

@Around("@annotation(com.example.cb.RemoteCircuitBreakerProtected)")publicObjectaround(ProceedingJoinPointpjp){Methodmethod=((MethodSignature)pjp.getSignature()).getMethod();returnbreaker.decorateSupplier(()->proceed(pjp),e->buildFallback(method));}privateObjectbuildFallback(Methodmethod){// 从 RemoteResult<T> 拿到 TParameterizedTypetype=(ParameterizedType)method.getGenericReturnType();Class<?>dataClass=(Class<?>)type.getActualTypeArguments()[0];try{returnRemoteResult.success(dataClass.getDeclaredConstructor().newInstance());}catch(ReflectiveOperationExceptione){returnRemoteResult.success(null);}}

熔断器工厂——recordResult 定义什么样的返回值算失败:

CircuitBreakerConfigconfig=CircuitBreakerConfig.custom().failureRateThreshold(60).minimumNumberOfCalls(6).slidingWindowSize(10).waitDurationInOpenState(Duration.ofSeconds(10)).permittedNumberOfCallsInHalfOpenState(3).slowCallRateThreshold(80).slowCallDurationThreshold(Duration.ofSeconds(10))// ↓↓↓ 关键.recordResult(response->{if(responseinstanceofRemoteResult<?>r&&r.getCode()==REMOTE_SERVICE_ERROR_CODE){returntrue;// 业务码算失败}returnfalse;}).build();

五、recordResult 的边界

recordResult只处理调用正常返回、但返回值本身表示业务失败的情况。如果方法内部抛了异常(RestTemplate 超时、连接失败),Resilience4j 默认会把这次调用记为失败,recordResult根本不会被调用。

所以这套方案实际上覆盖两条失败路径:

  • 抛异常 → 走 Resilience4j 默认失败识别(超时、网络异常等)
  • 不抛异常但业务码是失败 → 走 recordResult 兜底

两条合起来才不会漏掉"HTTP 200 也可能失败"的情况。

六、小结

这套方案落到代码上就三件事:

  • recordResult负责识别业务码失败,补上"HTTP 200 也是失败"这条路径
  • AOP 切面从方法签名的泛型里反射构造 fallback,业务代码不用写 lambda
  • 代理 Bean 把受保护的调用集中起来,同时避开 AOP self-invocation 的坑

再加个其他接口,只需要往代理 Bean 加一个方法、标个注解,调用方走代理 Bean。熔断的判定阈值、fallback 语义、日志格式都不用重写。

和前一篇 Feign 方案配起来,两条外部调用形态都有解法:能拦截解码的走ErrorDecoder,直接拿到封装对象的走recordResult+ AOP。