错误处理与异常处理
在微服务架构中,服务间的调用可能会因为各种原因失败。本章将深入讲解 OpenFeign 的错误处理机制,帮助你构建健壮的服务调用层。
Feign 异常体系
异常继承结构
OpenFeign 定义了一套完整的异常体系,所有异常都继承自 FeignException:
FeignException (abstract)
├── FeignException.BadRequest (400)
├── FeignException.Unauthorized (401)
├── FeignException.Forbidden (403)
├── FeignException.NotFound (404)
├── FeignException.MethodNotAllowed (405)
├── FeignException.NotAcceptable (406)
├── FeignException.ProxyAuthenticationRequired (407)
├── FeignException.RequestTimeout (408)
├── FeignException.Conflict (409)
├── FeignException.Gone (410)
├── FeignException.UnsupportedMediaType (415)
├── FeignException.TooManyRequests (429)
├── FeignException.InternalServerError (500)
├── FeignException.NotImplemented (501)
├── FeignException.BadGateway (502)
├── FeignException.ServiceUnavailable (503)
├── FeignException.GatewayTimeout (504)
└── FeignException.RetryableException (可重试异常)
异常信息获取
FeignException 提供了丰富的方法来获取错误信息:
try {
User user = userClient.getUserById(1L);
} catch (FeignException e) {
// 获取 HTTP 状态码
int status = e.status();
// 获取请求 URL
String url = e.request().url();
// 获取请求方法
HttpMethod method = e.httpMethod();
// 获取请求头
Map<String, Collection<String>> headers = e.request().headers();
// 获取请求体
byte[] requestBody = e.request().body();
// 获取响应体(如果有)
byte[] responseBody = e.responseBody().orElse(null);
String responseBodyString = e.contentUTF8();
// 获取异常消息
String message = e.getMessage();
log.error("Feign 调用失败: {} {}, 状态码: {}, 响应: {}",
method, url, status, responseBodyString);
}
异常类型判断
try {
User user = userClient.getUserById(userId);
} catch (FeignException e) {
if (e instanceof FeignException.NotFound) {
// 404:资源不存在
throw new ResourceNotFoundException("用户不存在: " + userId);
} else if (e instanceof FeignException.Unauthorized) {
// 401:未授权
throw new AuthenticationException("认证失败,请重新登录");
} else if (e instanceof FeignException.Forbidden) {
// 403:无权限
throw new PermissionDeniedException("无权访问该资源");
} else if (e instanceof FeignException.BadRequest) {
// 400:请求参数错误
String errorDetail = parseErrorDetail(e.contentUTF8());
throw new ValidationException("参数错误: " + errorDetail);
} else if (e instanceof FeignException.TooManyRequests) {
// 429:请求过于频繁
String retryAfter = e.responseHeaders().get("Retry-After")
.stream().findFirst().orElse("60");
throw new RateLimitException("请求过于频繁,请 " + retryAfter + " 秒后重试");
} else if (e instanceof FeignException.ServiceUnavailable) {
// 503:服务不可用
throw new ServiceUnavailableException("服务暂时不可用,请稍后重试");
} else if (e instanceof FeignException.RetryableException) {
// 可重试异常(通常是超时)
throw new ServiceTimeoutException("请求超时,请稍后重试");
} else {
// 其他异常
throw new ServiceException("服务调用失败: " + e.getMessage());
}
}
自定义错误解码器
基本实现
错误解码器允许你将 HTTP 错误响应转换为自定义异常:
public class CustomErrorDecoder implements ErrorDecoder {
private static final Logger log = LoggerFactory.getLogger(CustomErrorDecoder.class);
private final ErrorDecoder defaultDecoder = new ErrorDecoder.Default();
@Override
public Exception decode(String methodKey, Response response) {
int status = response.status();
String body = getResponseBody(response);
log.error("Feign 调用错误: method={}, status={}, body={}",
methodKey, status, body);
// 尝试解析业务错误信息
ApiError apiError = parseApiError(body);
switch (status) {
case 400:
return new BadRequestException(apiError.getMessage());
case 401:
return new UnauthorizedException(apiError.getMessage());
case 403:
return new ForbiddenException(apiError.getMessage());
case 404:
return new NotFoundException(apiError.getMessage());
case 409:
return new ConflictException(apiError.getMessage());
case 429:
return new RateLimitException(apiError.getMessage());
case 500:
case 502:
case 503:
case 504:
// 服务端错误,可能需要重试
return new RetryableException(
status,
apiError.getMessage(),
HttpMethod.valueOf(response.request().httpMethod().name()),
null,
response.request()
);
default:
return defaultDecoder.decode(methodKey, response);
}
}
private String getResponseBody(Response response) {
try {
if (response.body() != null) {
return Util.toString(response.body().asReader(StandardCharsets.UTF_8));
}
} catch (IOException e) {
log.warn("Failed to read response body", e);
}
return "";
}
private ApiError parseApiError(String body) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(body, ApiError.class);
} catch (Exception e) {
return new ApiError("Unknown error", body);
}
}
}
业务错误响应结构
通常服务端会返回结构化的错误信息:
// API 错误响应结构
public class ApiError {
private String code; // 业务错误码
private String message; // 错误消息
private List<FieldError> errors; // 字段级错误
private String traceId; // 追踪 ID
// getter、setter 省略
}
public class FieldError {
private String field;
private String message;
// getter、setter 省略
}
注册错误解码器
public class FeignConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}
或者在 YAML 中配置:
spring:
cloud:
openfeign:
client:
config:
user-service:
errorDecoder: com.example.feign.CustomErrorDecoder
全局异常处理
使用 @ControllerAdvice
在 Spring Boot 应用中,可以使用 @ControllerAdvice 统一处理 Feign 异常:
@RestControllerAdvice
public class FeignExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(FeignExceptionHandler.class);
/**
* 处理 Feign 404 异常
*/
@ExceptionHandler(FeignException.NotFound.class)
public ResponseEntity<ErrorResponse> handleNotFound(FeignException.NotFound e) {
log.warn("资源不存在: {}", e.request().url());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("RESOURCE_NOT_FOUND", "请求的资源不存在"));
}
/**
* 处理 Feign 401 异常
*/
@ExceptionHandler(FeignException.Unauthorized.class)
public ResponseEntity<ErrorResponse> handleUnauthorized(FeignException.Unauthorized e) {
log.warn("认证失败: {}", e.request().url());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("UNAUTHORIZED", "认证失败,请重新登录"));
}
/**
* 处理 Feign 403 异常
*/
@ExceptionHandler(FeignException.Forbidden.class)
public ResponseEntity<ErrorResponse> handleForbidden(FeignException.Forbidden e) {
log.warn("权限不足: {}", e.request().url());
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("FORBIDDEN", "无权访问该资源"));
}
/**
* 处理 Feign 429 异常
*/
@ExceptionHandler(FeignException.TooManyRequests.class)
public ResponseEntity<ErrorResponse> handleTooManyRequests(FeignException.TooManyRequests e) {
log.warn("请求过于频繁: {}", e.request().url());
String retryAfter = e.responseHeaders().getOrDefault("Retry-After", List.of("60"))
.get(0);
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", retryAfter)
.body(new ErrorResponse("RATE_LIMITED", "请求过于频繁,请稍后重试"));
}
/**
* 处理 Feign 5xx 异常
*/
@ExceptionHandler({
FeignException.InternalServerError.class,
FeignException.BadGateway.class,
FeignException.ServiceUnavailable.class,
FeignException.GatewayTimeout.class
})
public ResponseEntity<ErrorResponse> handleServerError(FeignException e) {
log.error("服务端错误: url={}, status={}", e.request().url(), e.status(), e);
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("SERVICE_ERROR", "服务暂时不可用,请稍后重试"));
}
/**
* 处理 Feign 可重试异常(通常是超时)
*/
@ExceptionHandler(FeignException.RetryableException.class)
public ResponseEntity<ErrorResponse> handleRetryableException(FeignException.RetryableException e) {
log.error("请求超时: {}", e.request().url(), e);
return ResponseEntity
.status(HttpStatus.GATEWAY_TIMEOUT)
.body(new ErrorResponse("TIMEOUT", "请求超时,请稍后重试"));
}
/**
* 处理其他 Feign 异常
*/
@ExceptionHandler(FeignException.class)
public ResponseEntity<ErrorResponse> handleFeignException(FeignException e) {
log.error("Feign 调用失败: status={}, url={}", e.status(), e.request().url(), e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("FEIGN_ERROR", "服务调用失败"));
}
}
// 错误响应结构
public record ErrorResponse(String code, String message) {}
针对不同客户端的异常处理
如果需要针对不同的 Feign 客户端使用不同的异常处理策略:
@RestControllerAdvice
public class FeignExceptionHandler {
@ExceptionHandler(FeignException.class)
public ResponseEntity<ErrorResponse> handleFeignException(
FeignException e,
HttpServletRequest request) {
// 根据请求路径判断是哪个服务的调用
String requestUri = request.getRequestURI();
if (requestUri.contains("/orders")) {
return handleOrderServiceError(e);
} else if (requestUri.contains("/users")) {
return handleUserServiceError(e);
} else {
return handleGenericError(e);
}
}
private ResponseEntity<ErrorResponse> handleOrderServiceError(FeignException e) {
// 订单服务特定的错误处理
if (e.status() == 404) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("ORDER_NOT_FOUND", "订单不存在"));
}
// ...
return handleGenericError(e);
}
private ResponseEntity<ErrorResponse> handleUserServiceError(FeignException e) {
// 用户服务特定的错误处理
// ...
return handleGenericError(e);
}
private ResponseEntity<ErrorResponse> handleGenericError(FeignException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SERVICE_ERROR", "服务调用失败"));
}
}
重试策略
理解重试触发条件
OpenFeign 的重试机制由 Retryer 控制。默认情况下,Spring Cloud OpenFeign 使用 Retryer.NEVER_RETRY,即不重试。
以下情况可能触发重试:
- 连接超时:无法建立 TCP 连接
- 读取超时:连接已建立,但等待响应超时
- 服务端返回 5xx:服务端错误
- IOException:网络 IO 异常
自定义重试器
public class SmartRetryer implements Retryer {
private final int maxAttempts;
private final long period;
private final long maxPeriod;
private int attempt;
public SmartRetryer(int maxAttempts, long period, long maxPeriod) {
this.maxAttempts = maxAttempts;
this.period = period;
this.maxPeriod = maxPeriod;
this.attempt = 1;
}
@Override
public void continueOrPropagate(RetryableException e) {
// 超过最大重试次数
if (attempt >= maxAttempts) {
throw e;
}
// 根据异常类型决定是否重试
if (!shouldRetry(e)) {
throw e;
}
// 计算等待时间(指数退避)
long interval = calculateInterval();
try {
Thread.sleep(interval);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw e;
}
attempt++;
}
private boolean shouldRetry(RetryableException e) {
int status = e.status();
// 服务端错误可以重试
if (status >= 500 && status < 600) {
return true;
}
// 连接相关异常可以重试
Throwable cause = e.getCause();
if (cause instanceof java.net.ConnectException) {
return true;
}
if (cause instanceof java.net.SocketTimeoutException) {
// 读取超时是否重试取决于业务场景
// 这里选择不重试,避免重复操作
return false;
}
return false;
}
private long calculateInterval() {
// 指数退避算法
long interval = (long) (period * Math.pow(2, attempt - 1));
return Math.min(interval, maxPeriod);
}
@Override
public Retryer clone() {
return new SmartRetryer(maxAttempts, period, maxPeriod);
}
}
配置重试器
@Configuration
public class FeignConfig {
@Bean
public Retryer retryer() {
// 最多重试 3 次,初始间隔 100ms,最大间隔 1s
return new SmartRetryer(3, 100, 1000);
}
}
重试与幂等性
重试前必须考虑接口的幂等性:
public class IdempotentRetryer implements Retryer {
// 幂等的 HTTP 方法
private static final Set<String> IDEMPOTENT_METHODS =
Set.of("GET", "HEAD", "OPTIONS", "PUT", "DELETE");
@Override
public void continueOrPropagate(RetryableException e) {
String method = e.method().name();
// 非幂等方法不重试
if (!IDEMPOTENT_METHODS.contains(method)) {
throw e;
}
// 幂等方法可以重试
// ... 重试逻辑
}
@Override
public Retryer clone() {
return new IdempotentRetryer();
}
}
对于 POST 等非幂等方法,可以通过添加请求 ID 实现幂等:
// 添加请求 ID 拦截器
@Bean
public RequestInterceptor requestIdInterceptor() {
return template -> {
String requestId = UUID.randomUUID().toString();
template.header("X-Request-Id", requestId);
};
}
// 服务端去重处理
// 收到请求后先检查 X-Request-Id 是否已处理过
// 如果已处理,直接返回之前的结果
超时处理
超时类型
OpenFeign 涉及多种超时:
| 超时类型 | 说明 | 配置属性 |
|---|---|---|
| 连接超时 | 建立 TCP 连接的超时时间 | connectTimeout |
| 读取超时 | 等待响应数据的超时时间 | readTimeout |
| 写入超时 | 发送请求数据的超时时间 | writeTimeout(OkHttp) |
| 连接请求超时 | 从连接池获取连接的超时时间 | connectionRequestTimeout |
超时配置最佳实践
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000 # 连接超时 5 秒
readTimeout: 30000 # 读取超时 30 秒
# 快速响应的服务
auth-service:
connectTimeout: 3000
readTimeout: 5000 # 认证服务 5 秒足够
# 需要长时间处理的服务
report-service:
connectTimeout: 5000
readTimeout: 120000 # 报表服务 2 分钟
# 文件服务
file-service:
connectTimeout: 10000
readTimeout: 300000 # 文件上传/下载 5 分钟
超时异常处理
@Service
public class UserService {
private final UserClient userClient;
public User getUserWithFallback(Long userId) {
try {
return userClient.getUserById(userId);
} catch (FeignException.RetryableException e) {
// 超时异常
log.warn("获取用户超时: userId={}", userId);
// 尝试从缓存获取
User cachedUser = cacheService.getUserFromCache(userId);
if (cachedUser != null) {
return cachedUser;
}
// 返回默认值
return new User(userId, "未知用户", null);
}
}
}
熔断降级
与 Resilience4j 集成
// 配置类
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerConfig customCircuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率 50% 触发熔断
.waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断 10 秒
.slidingWindowSize(10) // 滑动窗口大小
.slidingWindowType(SlidingWindowType.COUNT_BASED)
.minimumNumberOfCalls(5) // 最小调用次数
.build();
}
}
# YAML 配置
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50
waitDurationInOpenState: 10s
slidingWindowSize: 10
instances:
user-service:
baseConfig: default
order-service:
baseConfig: default
failureRateThreshold: 30 # 订单服务更敏感
使用 FallbackFactory 处理异常
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUserById(Long id) {
// 根据异常类型返回不同的降级结果
if (cause instanceof FeignException.NotFound) {
// 404 不是真正的错误,可能是用户确实不存在
return null;
}
if (cause instanceof FeignException.Unauthorized) {
// 认证失败需要抛出异常,不能静默降级
throw new AuthenticationException("认证失败", cause);
}
if (cause instanceof FeignException.TooManyRequests) {
// 限流需要特殊处理
throw new RateLimitException("请求过于频繁", cause);
}
// 其他异常,记录日志并返回默认值
log.error("UserClient.getUserById fallback: id={}, error={}",
id, cause.getMessage());
return new User(id, "默认用户", null);
}
@Override
public List<User> getAllUsers() {
log.error("UserClient.getAllUsers fallback", cause);
return Collections.emptyList();
}
};
}
}
日志记录
结构化错误日志
@Aspect
@Component
public class FeignLoggingAspect {
private static final Logger log = LoggerFactory.getLogger(FeignLoggingAspect.class);
@AfterThrowing(pointcut = "execution(* com.example.client..*(..))", throwing = "ex")
public void logFeignException(JoinPoint joinPoint, FeignException ex) {
Map<String, Object> errorLog = new LinkedHashMap<>();
errorLog.put("timestamp", Instant.now().toString());
errorLog.put("method", joinPoint.getSignature().toShortString());
errorLog.put("httpStatus", ex.status());
errorLog.put("url", ex.request().url());
errorLog.put("httpMethod", ex.httpMethod().name());
errorLog.put("responseBody", ex.contentUTF8());
errorLog.put("exceptionClass", ex.getClass().getSimpleName());
errorLog.put("errorMessage", ex.getMessage());
// 提取 Trace ID(如果有)
ex.request().headers().get("X-Trace-Id")
.stream()
.findFirst()
.ifPresent(traceId -> errorLog.put("traceId", traceId));
log.error("Feign 调用失败: {}", errorLog);
}
}
错误追踪
public class TracingInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 添加追踪信息
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
String spanId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
template.header("X-Span-Id", spanId);
// 记录请求开始时间
template.header("X-Request-Start-Time", String.valueOf(System.currentTimeMillis()));
}
}
// 在错误解码器中添加追踪信息
public class TracingErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
String traceId = response.request().headers().get("X-Trace-Id")
.stream()
.findFirst()
.orElse("unknown");
// 将追踪 ID 添加到异常信息中
FeignException exception = new Default().decode(methodKey, response);
log.error("Feign error [traceId={}]: status={}, method={}, url={}",
traceId, response.status(), methodKey, response.request().url());
return exception;
}
}
错误处理最佳实践
1. 区分可恢复和不可恢复错误
public enum ErrorRecoverability {
RECOVERABLE, // 可恢复:可以重试或降级
NON_RECOVERABLE // 不可恢复:需要用户介入
}
public class ErrorRecoveryStrategy {
public ErrorRecoverability analyze(FeignException e) {
// 4xx 错误通常是客户端问题,不可恢复
if (e.status() >= 400 && e.status() < 500) {
// 401 可能可以通过刷新 Token 恢复
if (e.status() == 401) {
return ErrorRecoverability.RECOVERABLE;
}
return ErrorRecoverability.NON_RECOVERABLE;
}
// 5xx 错误通常是服务端问题,可能可以恢复
if (e.status() >= 500) {
return ErrorRecoverability.RECOVERABLE;
}
// 网络错误可能可以恢复
if (e instanceof FeignException.RetryableException) {
return ErrorRecoverability.RECOVERABLE;
}
return ErrorRecoverability.NON_RECOVERABLE;
}
}
2. 不要吞掉异常
// 错误示例:吞掉异常
@Override
public User getUserById(Long id) {
try {
return userClient.getUserById(id);
} catch (FeignException e) {
return null; // 错误!完全隐藏了错误
}
}
// 正确示例:记录并处理
@Override
public User getUserById(Long id) {
try {
return userClient.getUserById(id);
} catch (FeignException.NotFound e) {
// 404 是预期内的错误,可以返回 null
log.debug("User not found: {}", id);
return null;
} catch (FeignException e) {
// 其他错误必须记录
log.error("Failed to get user: {}", id, e);
throw new ServiceException("获取用户失败", e);
}
}
3. 使用断路器保护
@Service
public class UserService {
private final UserClient userClient;
private final CircuitBreaker circuitBreaker;
public UserService(UserClient userClient, CircuitBreakerRegistry registry) {
this.userClient = userClient;
this.circuitBreaker = registry.circuitBreaker("user-service");
}
public User getUserById(Long id) {
return circuitBreaker.executeSupplier(() -> userClient.getUserById(id));
}
public User getUserByIdWithFallback(Long id) {
return circuitBreaker.executeSupplier(
() -> userClient.getUserById(id),
() -> new User(id, "默认用户", null) // 降级逻辑
);
}
}
4. 合理设置超时时间
// 超时时间应该基于服务的 P99 响应时间
// P99 响应时间 + 安全边际 = 超时时间
// 例如:
// 认证服务 P99 = 500ms -> 超时 = 500ms * 3 = 1500ms
// 普通业务服务 P99 = 2s -> 超时 = 2s * 3 = 6s
// 报表服务 P99 = 30s -> 超时 = 30s * 2 = 60s
小结
本章详细介绍了 OpenFeign 的错误处理机制:
| 主题 | 要点 |
|---|---|
| 异常体系 | 了解 FeignException 的子类及其含义 |
| 错误解码器 | 自定义 HTTP 错误到业务异常的转换 |
| 全局异常处理 | 使用 @ControllerAdvice 统一处理 |
| 重试策略 | 根据异常类型和幂等性决定是否重试 |
| 超时处理 | 合理配置各类超时时间 |
| 熔断降级 | 使用断路器保护服务调用 |
| 日志记录 | 结构化记录错误信息,便于追踪 |
下一章将介绍测试 Feign 客户端的方法。