一、设计原则
公共包只做:http 请求、超时、舱壁 / 熔断 / 重试、原始响应日志、异常包装;不做业务降级 fallback。
- 下游业务逻辑错误 → 返回 DTO(带业务 code)。
- 网络、4xx、5xx、解析异常、舱壁满、熔断打开、重试耗尽 → 抛出异常向上透传,由上层业务处理降级、告警、补偿。
- Resilience4j 原生异常直接透传,不吞掉,上层可以区分是限流 / 熔断 / 重试耗尽。
二、公共包异常定义
2.1 顶层父异常
package com.common.remote.exception; /** 远程调用顶层父异常 */ public abstract class RemoteCallException extends RuntimeException { /** 下游原始响应体,出现异常时尽量带回,方便排查,可能为null */ private final String rawResponse; public RemoteCallException(String message, String rawResponse, Throwable cause) { super(message, cause); this.rawResponse = rawResponse; } public String getRawResponse() { return rawResponse; } }2.2 网络异常
package com.common.remote.exception; /** 网络异常:连接超时、读取超时、socket断开、连接失败 */ public class RemoteNetworkException extends RemoteCallException { public RemoteNetworkException(String message, String rawResponse, Throwable cause) { super(message, rawResponse, cause); } }2.3 下游 4xx 客户端错误
package com.common.remote.exception; /** 下游返回4xx 客户端错误:参数错误、鉴权失败 */ public class RemoteClient4xxException extends RemoteCallException { public RemoteClient4xxException(String message, String rawResponse, Throwable cause) { super(message, rawResponse, cause); } }2.4 下游 5xx 服务端故障
package com.common.remote.exception; /** 下游返回5xx 服务端故障 */ public class RemoteServer5xxException extends RemoteCallException { public RemoteServer5xxException(String message, String rawResponse, Throwable cause) { super(message, rawResponse, cause); } }2.5 返回体解析异常
package com.common.remote.exception; /** 返回体解析异常:空body、非JSON、JSON结构不匹配 */ public class RemoteResponseParseException extends RemoteCallException { public RemoteResponseParseException(String message, String rawResponse, Throwable cause) { super(message, rawResponse, cause); } }三、公共包 DTO
package com.common.remote.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public class RiskRespDTO { private Integer code; private String msg; private Object data; public boolean isBizSuccess() { return Integer.valueOf(200).equals(code); } // getter setter public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }四、公共包响应解析工具
package com.common.remote.parser; import com.common.remote.dto.RiskRespDTO; import com.common.remote.exception.RemoteResponseParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class RemoteResponseParser { private final ObjectMapper objectMapper; public RemoteResponseParser(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } public RiskRespDTO parse(String rawBody) { if (rawBody == null || rawBody.isBlank()) { throw new RemoteResponseParseException("下游返回body为空", rawBody, null); } String trimBody = rawBody.trim(); if (!((trimBody.startsWith("{") && trimBody.endsWith("}")) || (trimBody.startsWith("[") && trimBody.endsWith("]")))) { throw new RemoteResponseParseException("下游返回非标准JSON", rawBody, null); } try { return objectMapper.readValue(rawBody, RiskRespDTO.class); } catch (JsonProcessingException e) { throw new RemoteResponseParseException("JSON结构与预期不匹配", rawBody, e); } } }五、公共包远程调用 Service(核心,无 fallback)
注解只作用在这个纯远程调用方法;没有任何上层业务逻辑,删除 fallbackMethod。
Resilience4j 原生异常:BulkheadFullException、CircuitBreakerOpenException、RetryExhaustedException直接向上抛出。
package com.common.remote.service; import com.common.remote.dto.RiskRespDTO; import com.common.remote.exception.RemoteClient4xxException; import com.common.remote.exception.RemoteNetworkException; import com.common.remote.exception.RemoteResponseParseException; import com.common.remote.exception.RemoteServer5xxException; import com.common.remote.parser.RemoteResponseParser; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.threadpool.bulkhead.annotation.Bulkhead; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; import java.util.concurrent.CompletableFuture; @Slf4j @Service public class ThirdPartyRiskRemoteService { @Resource private RestClient restClient; @Resource private RemoteResponseParser remoteResponseParser; private static final String RISK_URL = "http://127.0.0.1:8090/risk/check"; /** 纯远程调用,内部无业务逻辑 舱壁、熔断、重试只保护网络请求 不配置fallback,异常全部向上抛出,由业务方处理降级 */ @Bulkhead(name = "thirdPartyRiskBulkhead", mode = Bulkhead.Mode.THREADPOOL) @Retry(name = "thirdPartyRiskRetry") @CircuitBreaker(name = "thirdPartyRiskCb") public CompletableFuture<RiskRespDTO> callRiskApi(String requestParam) { return CompletableFuture.supplyAsync(() -> { log.info("[公共包-调用第三方风控] param={}", requestParam); String rawBody = null; try { ResponseEntity<String> respEntity = restClient.get() .uri(RISK_URL + "?param=" + requestParam) .retrieve() .onStatus(status -> status.is4xxClientError(), (req, resp) -> { rawBody = resp.getBody().toString(); log.error("[公共包]4xx客户端错误 status={},raw={}", resp.getStatusCode(), rawBody); throw new RemoteClient4xxException("下游4xx客户端错误", rawBody, null); }) .onStatus(status -> status.is5xxServerError(), (req, resp) -> { rawBody = resp.getBody().toString(); log.error("[公共包]5xx下游服务异常 status={},raw={}", resp.getStatusCode(), rawBody); throw new RemoteServer5xxException("下游5xx服务异常", rawBody, null); }) .toEntity(String.class); rawBody = respEntity.getBody(); log.info("[公共包-下游原始响应] rawBody={}", rawBody); //解析,解析失败抛RemoteResponseParseException return remoteResponseParser.parse(rawBody); } catch (RestClientException e) { log.error("[公共包]网络IO异常", e); throw new RemoteNetworkException("调用下游网络异常", rawBody, e); } }); } }application.yml 配置和之前保持不变,不要写 fallbackMethod。
六、上层业务调用方(业务服务,引入公共包)
业务层有自己的业务逻辑,捕获全部异常,做业务自己的降级、告警、补偿。
package com.biz.service; import com.common.remote.dto.RiskRespDTO; import com.common.remote.exception.*; import com.common.remote.service.ThirdPartyRiskRemoteService; import io.github.resilience4j.bulkhead.BulkheadFullException; import io.github.resilience4j.circuitbreaker.CircuitBreakerOpenException; import io.github.resilience4j.retry.RetryExhaustedException; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.concurrent.ExecutionException; @Slf4j @Service public class OrderBizService { @Resource private ThirdPartyRiskRemoteService thirdPartyRiskRemoteService; /** 下单业务,包含本地业务逻辑 + 调用公共包远程接口 */ public void createOrder(String userId) { // ========== 本地业务逻辑(运行在Tomcat/业务线程,不受舱壁限制) ========== log.info("下单本地前置业务逻辑 userId={}", userId); RiskRespDTO riskResp; try { riskResp = thirdPartyRiskRemoteService.callRiskApi(userId).get(); } catch (ExecutionException e) { // CompletableFuture包装,拿到真实内部异常 Throwable cause = e.getCause(); handleRemoteException(cause); return; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("调用远程线程被中断", e); // 业务自行决定失败策略 return; } // http调用成功返回,判断下游业务码 if (!riskResp.isBizSuccess()) { // 下游业务逻辑失败,例:风控拦截用户 log.warn("下游风控业务拒绝 code={},msg={}", riskResp.getCode(), riskResp.getMsg()); // 业务:抛业务异常 / 返回结果 / 走其他分支 return; } // ========== 下单后置本地业务逻辑 ========== log.info("下单后置业务逻辑"); } /** 统一处理所有远程调用异常,业务层自己实现降级、告警 */ private void handleRemoteException(Throwable cause) { if (cause instanceof BulkheadFullException) { //舱壁满,限流 log.warn("远程调用舱壁限流,隔离线程池已满"); // 业务动作:告警、返回系统繁忙、拒绝请求 } else if (cause instanceof CircuitBreakerOpenException) { //熔断打开 log.warn("远程调用熔断打开"); } else if (cause instanceof RetryExhaustedException) { //重试全部耗尽仍然失败 log.warn("远程调用重试全部耗尽"); } else if (cause instanceof RemoteNetworkException ex) { log.error("网络异常 raw={}", ex.getRawResponse(), ex); // 可选:写本地数据库,定时任务补偿 } else if (cause instanceof RemoteClient4xxException ex) { log.error("下游4xx参数错误 raw={}", ex.getRawResponse(), ex); } else if (cause instanceof RemoteServer5xxException ex) { log.error("下游5xx服务故障 raw={}", ex.getRawResponse(), ex); } else if (cause instanceof RemoteResponseParseException ex) { log.error("下游返回格式异常 raw={}", ex.getRawResponse(), ex); //监控埋点:统计格式异常,告警下游接口变更 } else if (cause instanceof RemoteCallException ex) { log.error("通用远程调用异常 raw={}", ex.getRawResponse(), ex); } else { log.error("未知异常", cause); } } }七、关键区分总结
公共包
- @Bulkhead @CircuitBreaker @Retry 只加在纯 http 调用方法;
- 本地业务逻辑一定放在上层业务,不要进被注解的方法;
- 下游业务成功:返回 RiskRespDTO,业务 code 放在 DTO;
- 网络、限流、熔断、解析错误:抛出异常,不返回业务码 DTO。
上层业务
- 执行自己全部本地业务逻辑;
- 捕获 Resilience4j 原生异常 + 公共包自定义异常;
- 根据不同异常类型做:告警、降级、拒绝、补偿落库;
- 下游业务逻辑失败,判断 DTO 中的 code。
八、生产额外建议
- 上层可以增加 micrometer 埋点,统计每种异常的计数器,对接 prometheus 告警。
- 敏感返回体打印日志时做脱敏。
- 写接口场景直接移除 @Retry 注解,避免非幂等重复调用。
- CompletableFuture.get() 会抛出 ExecutionException,需要 getCause 拿到真实异常,上面代码已经处理。