You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot微服务中如何实现跨服务异常传递?

Spring Boot微服务间异常传递实现方案

一、基础异常传递:让调用方捕获服务端自定义异常

服务端(提供API的微服务)处理

服务端需要将自定义异常(如ResourceNotFoundException)转换为标准结构化响应,确保异常信息能通过HTTP响应完整传递给调用方:

  1. 基于你已有的ApiResponse和CareConnectException结构,用全局异常处理器统一拦截异常:
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse> handleResourceNotFound(ResourceNotFoundException e) {
        CareConnectException error = new CareConnectException(
                "1002", 
                e.getMessage(), 
                "请求的资源不存在,请检查参数"
        );
        ApiResponse response = new ApiResponse();
        response.setError(error);
        // 资源不存在对应HTTP 404状态码更符合REST规范
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }
}

当服务端抛出ResourceNotFoundException时,会返回包含错误码、消息、提示的结构化响应,而非未处理的原始异常。

调用方(Feign客户端)处理

Feign默认会将非2xx响应包装为FeignException,需要解析响应体还原服务端的异常信息:

  1. 自定义Feign异常解码器:
@Component
public class CustomFeignErrorDecoder implements ErrorDecoder {

    private final ObjectMapper objectMapper;

    public CustomFeignErrorDecoder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            // 解析响应体为统一错误格式
            ApiResponse apiResponse = objectMapper.readValue(response.body().asInputStream(), ApiResponse.class);
            CareConnectException error = apiResponse.getError();
            
            // 根据服务端返回的错误码,还原为对应自定义异常
            if ("1002".equals(error.getCode())) {
                return new ResourceNotFoundException("Patient", "Id", error.getMessage());
            }
            return new RuntimeException("服务调用异常:" + error.getMessage());
        } catch (IOException e) {
            // 解析失败时返回默认Feign异常
            return FeignException.errorStatus(methodKey, response);
        }
    }
}
  1. 给Feign客户端配置该解码器:
@FeignClient(name = "patient-service", configuration = CustomFeignConfig.class)
public interface PatientFeignClient {
    @GetMapping("/patients/{patientId}")
    ApiResponse getPatientById(@PathVariable Long patientId);
}

class CustomFeignConfig {
    @Bean
    public ErrorDecoder errorDecoder(ObjectMapper objectMapper) {
        return new CustomFeignErrorDecoder(objectMapper);
    }
}
  1. 调用方业务代码中直接捕获目标异常:
public PatientEntity getPatient(Long patientId) {
    try {
        ApiResponse response = patientFeignClient.getPatientById(patientId);
        return (PatientEntity) response.getData();
    } catch (ResourceNotFoundException e) {
        // 处理资源不存在逻辑
        log.error("获取患者失败:{}", e.getMessage());
        throw e; // 或转换为业务异常向上传递
    }
}

二、多异常场景的优雅处理

如果服务端会抛出多种异常,可通过以下方式优化,避免硬编码大量catch块:

服务端:统一异常分类与编码

定义基础业务异常类,所有自定义异常继承它,全局处理器统一处理:

// 基础业务异常类
public class BaseBusinessException extends RuntimeException {
    private String errorCode;
    private String tip;

    // 构造方法、getter/setter
}

// 具体异常类
public class ResourceNotFoundException extends BaseBusinessException {
    public ResourceNotFoundException(String resourceType, String field, String value) {
        super(String.format("%s with %s=%s not found", resourceType, field, value));
        setErrorCode("1002");
        setTip("请求的资源不存在,请检查参数");
    }
}

// 全局异常处理器优化
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseBusinessException.class)
    public ResponseEntity<ApiResponse> handleBaseBusinessException(BaseBusinessException e) {
        CareConnectException error = new CareConnectException(
                e.getErrorCode(), 
                e.getMessage(), 
                e.getTip()
        );
        ApiResponse response = new ApiResponse();
        response.setError(error);
        
        // 根据异常类型匹配HTTP状态码
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        if (e instanceof ResourceNotFoundException) {
            status = HttpStatus.NOT_FOUND;
        } else if (e instanceof IllegalArgumentException) {
            status = HttpStatus.BAD_REQUEST;
        }
        return new ResponseEntity<>(response, status);
    }

    // 处理系统异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse> handleSystemException(Exception e) {
        CareConnectException error = new CareConnectException(
                "9999", 
                e.getMessage(), 
                "系统异常,请稍后重试"
        );
        ApiResponse response = new ApiResponse();
        response.setError(error);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

调用方:通用异常解析与转换

优化Feign解码器,通过错误码映射异常类,避免硬编码:

@Component
public class CustomFeignErrorDecoder implements ErrorDecoder {

    private final ObjectMapper objectMapper;
    // 错误码-异常类映射,可配置化
    private static final Map<String, Class<? extends BaseBusinessException>> ERROR_CODE_MAP = new HashMap<>();

    static {
        ERROR_CODE_MAP.put("1002", ResourceNotFoundException.class);
        ERROR_CODE_MAP.put("1003", IllegalArgumentException.class);
        // 新增其他异常映射
    }

    public CustomFeignErrorDecoder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            ApiResponse apiResponse = objectMapper.readValue(response.body().asInputStream(), ApiResponse.class);
            CareConnectException error = apiResponse.getError();
            
            Class<? extends BaseBusinessException> exceptionClass = ERROR_CODE_MAP.get(error.getCode());
            if (exceptionClass != null) {
                // 反射实例化异常(需保证异常构造方法统一)
                return exceptionClass.getConstructor(String.class, String.class, String.class)
                        .newInstance(error.getMessage(), error.getCode(), error.getTip());
            } else {
                return new BaseBusinessException(error.getMessage(), error.getCode(), error.getTip());
            }
        } catch (Exception e) {
            return FeignException.errorStatus(methodKey, response);
        }
    }
}

调用方只需捕获基础异常类,即可处理所有服务端传递的异常:

public PatientEntity getPatient(Long patientId) {
    try {
        ApiResponse response = patientFeignClient.getPatientById(patientId);
        return (PatientEntity) response.getData();
    } catch (BaseBusinessException e) {
        // 根据异常类型做差异化处理
        if (e instanceof ResourceNotFoundException) {
            // 资源不存在逻辑
        } else if (e instanceof IllegalArgumentException) {
            // 参数错误逻辑
        }
        log.error("服务调用异常,错误码:{},消息:{}", e.getErrorCode(), e.getMessage());
        throw e;
    }
}

三、注意事项

  • 服务端必须返回结构化错误响应,避免调用方解析混乱。
  • 不要在调用方捕获FeignException后直接抛出新异常,会丢失服务端原始异常信息。
  • HTTP状态码需与异常类型匹配,符合REST规范(如资源不存在用404,参数错误用400)。

内容的提问来源于stack exchange,提问作者Himanshu Sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 05:40:39