news 2026/8/27 10:25:45

公共包远程调用:完整自定义异常体系与使用示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
公共包远程调用:完整自定义异常体系与使用示例

一、设计原则

公共包只做: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 拿到真实异常,上面代码已经处理。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/27 10:25:13

Kimi LeetCode LCP 16. 游乐园的游览计划 Rust实现

以下是 LeetCode LCP 16. 游乐园的游览计划 的 Rust 实现。题目理解小吴计划上午和下午各走一个三角形路径(A-B-C-A 和 A-B-C-A),两个路径至少共享一个顶点 A。重复游玩同一个项目不重复计分。目标是最大化所有不同顶点的喜爱值之和。等价于&…

作者头像 李华
网站建设 2026/8/27 10:24:39

单片机计算机毕设之基于 STM32 的车载环境监测与 Android 远程交互系统设计 基于 STM32 的车载酒精定位采集与远程阈值控制系统设计(010205)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/8/27 10:22:08

高隔离DC/DC在工业电源中的关键参数与设计实践

1. 高隔离DC/DC到底“高”在哪里1.1 隔离等级不是拍脑袋定的做工业电源设计这些年,高隔离DC/DC我接触了不少。从PLC里的隔离通讯供电,到电机驱动器里的IGBT驱动电源,再到电力仪表里的高压侧采样供电,几乎每一个系统里,…

作者头像 李华
网站建设 2026/8/27 10:21:41

Python暗藏官方彩蛋!输入简单指令就能解锁|零壹教育分享

很多人热衷于学习, 将全部的重心放置于Excel处理、文件批量操作之上, 每日与报表、数据以及各类报错频繁打交道, 一门心绪专心打磨锤炼办公自动化能力, 然而却浑然不知这门编程语言里面暗藏着官方别出心裁设计的小彩蛋。其不需要繁复的操作, 仅仅凭借简单的指令便能够触发, 不少…

作者头像 李华
网站建设 2026/8/27 10:21:31

【信息科学与工程学】【制造工程】第三十六篇 机械工程与自动化111

新增编号 A1~A20 编号 学科(课程) 核心知识点 在机械工程和制造工程和自动化体系中的作用 代表教材/资料/论文 + 数学分析方程式列表 工业界应用 难度等级 A1 机械自动化手工装配线设计​ 手工装配线基本概念(工位、节拍、作业元素、工序);装配线平衡问题(ALBP…

作者头像 李华
网站建设 2026/8/27 10:20:25

回归分析实战指南:从数据清洗到模型评估的完整流程

1. 从“相关性”到“因果性”的桥梁:回归分析到底是什么? 如果你在数据分析、金融风控、市场研究或者任何需要处理数字的领域待过一阵子,大概率会听到“回归分析”这个词。它听起来有点学术,甚至有点枯燥,但说穿了&…

作者头像 李华