ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

公共包远程调用:完整自定义异常体系与使用示例 一、设计原则公共包只做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); } }三、公共包 DTOpackage 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({) amp;amp; trimBody.endsWith(})) || (trimBody.startsWith([) amp;amp; 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 CompletableFuturelt;RiskRespDTOgt; callRiskApi(String requestParam) { return CompletableFuture.supplyAsync(() -gt; { log.info([公共包-调用第三方风控] param{}, requestParam); String rawBody null; try { ResponseEntitylt;Stringgt; respEntity restClient.get() .uri(RISK_URL ?param requestParam) .retrieve() .onStatus(status -gt; status.is4xxClientError(), (req, resp) -gt; { rawBody resp.getBody().toString(); log.error([公共包]4xx客户端错误 status{},raw{}, resp.getStatusCode(), rawBody); throw new RemoteClient4xxException(下游4xx客户端错误, rawBody, null); }) .onStatus(status -gt; status.is5xxServerError(), (req, resp) -gt; { 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 拿到真实异常上面代码已经处理。
返回列表