
前一篇写了 基于 Feign Resilience4j 的微服务熔断防雪崩优化方案。这次同一套系统又要加熔断但走 Feign 那条路走不通了。三个接口直连 HTTP 调外部数据接口返回体用业务码表达失败不抛异常。现成的CircuitBreaker注解只认异常用不上只能自己搭一套。一、场景三个接口都调外部数据服务返回统一封装classRemoteResultT{privateintcode;// 0成功500106远程服务异常其他业务码privateTdata;...}调用方拿到结果自己判code。HTTP 200 也可能是失败。跟前一篇 Feign 那套差在这里上一篇走 Feign用ErrorDecoder在解码阶段把系统异常码转成特殊异常抛给 Resilience4j异常路径触发熔断。这一篇直连 HTTPRestTemplate拿回一个对象方法里没有异常熔断器数不到失败次数。Resilience4j 的CircuitBreaker注解默认只识别异常。要让它认业务码得走recordResult回调——一个PredicateObject让你自己定义什么样的返回值算失败。所以这套方案没用注解而是包了一层自己的装饰器。二、类图业务代码只调代理 Bean注解标在代理 Bean 的方法上切面拦截并交给底层熔断器执行。三、调用时序熔断打开时切面反射从方法签名RemoteResultT里拿到 Tnew T()包一个空的RemoteResult.success(...)作为兜底。业务代码看到的永远是同类型的返回值不用判空、不用 catch。四、核心代码注解——空注解参数全靠约定Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceRemoteCircuitBreakerProtected{}代理 Bean——集中管理受保护的调用解决 AOP self-invocation 问题ComponentpublicclassRemoteFacade{ResourceprivateRemoteUtilremoteUtil;RemoteCircuitBreakerProtectedpublicRemoteResultRespAqueryA(ReqAreq){returnremoteUtil.queryA(req);}RemoteCircuitBreakerProtectedpublicRemoteResultRespBqueryB(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){// 从 RemoteResultT 拿到 TParameterizedTypetype(ParameterizedType)method.getGenericReturnType();Class?dataClass(Class?)type.getActualTypeArguments()[0];try{returnRemoteResult.success(dataClass.getDeclaredConstructor().newInstance());}catch(ReflectiveOperationExceptione){returnRemoteResult.success(null);}}熔断器工厂——recordResult 定义什么样的返回值算失败CircuitBreakerConfigconfigCircuitBreakerConfig.custom().failureRateThreshold(60).minimumNumberOfCalls(6).slidingWindowSize(10).waitDurationInOpenState(Duration.ofSeconds(10)).permittedNumberOfCallsInHalfOpenState(3).slowCallRateThreshold(80).slowCallDurationThreshold(Duration.ofSeconds(10))// ↓↓↓ 关键.recordResult(response-{if(responseinstanceofRemoteResult?rr.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。