2. 默认异常传播策略
微服务间调用HTTP API时遇到错误是家常便饭。在Spring Boot集成OpenFeign的场景下,默认的错误处理器会将下游服务的错误(比如404 Not Found)统一包装成500 Internal Server Error返回。这种处理方式显然不够优雅,好在Spring和OpenFeign都提供了自定义错误处理的机制。
本文将深入分析默认异常传播机制,并展示如何实现自定义错误处理。
2.1. Feign中的默认异常传播
Feign使用ErrorDecoder.Default
作为默认错误处理器。当收到非2xx状态码时:
- 若响应包含
Retry-After
头,返回RetryableException
- 否则直接返回
FeignException
- 重试失败后最终也会抛出
FeignException
decode
方法会将HTTP方法键和响应信息封装到异常对象中。
2.2. Spring Rest Controller的默认异常传播
当RestController
遇到未处理异常时,会返回500错误并附带结构化响应:
{
"timestamp": "2022-07-08T08:07:51.120+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/myapp1/product/Test123"
}
下面通过实例深入分析。
3. 示例应用
构建一个从外部服务获取产品信息的微服务:
首先定义产品模型:
public class Product {
private String id;
private String productName;
private double price;
}
实现控制器:
@RestController("product_controller")
@RequestMapping(value ="myapp1")
public class ProductController {
private ProductClient productClient;
@Autowired
public ProductController(ProductClient productClient) {
this.productClient = productClient;
}
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
return productClient.getProduct(id);
}
}
配置Feign日志:
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
定义Feign客户端:
@FeignClient(name = "product-client", url="http://localhost:8081/product/", configuration = FeignConfig.class)
public interface ProductClient {
@RequestMapping(value = "{id}", method = RequestMethod.GET)
Product getProduct(@PathVariable(value = "id") String id);
}
4. 默认异常传播分析
4.1. 使用WireMock模拟服务
添加依赖:
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.33.2</version>
<scope>test</scope>
</dependency>
启动模拟服务:
WireMockServer wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
4.2. Feign客户端的默认行为
模拟503服务不可用:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
模拟404资源不存在:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
踩坑提示:无论下游返回什么4xx/5xx错误,Feign默认都抛出FeignException
,虽然异常对象包含状态码,但基于异常类型的处理策略很难区分具体错误。
4.3. 控制器层的异常传播
测试服务不可用场景:
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
mockMvc.perform(get("/myapp1/product/" + productId))
.andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()));
无论下游是404还是503,上游统一返回500,这种粗暴处理显然不够精细。
5. 在Feign中实现自定义异常传播
通过自定义ErrorDecoder
根据状态码返回特定异常:
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()){
case 400:
return new BadRequestException();
case 404:
return new ProductNotFoundException("Product not found");
case 503:
return new ProductServiceNotAvailableException("Product Api is unavailable");
default:
return new Exception("Exception while getting product details");
}
}
}
注册自定义解码器:
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
或直接在客户端配置:
@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/",
configuration = { FeignConfig.class, CustomErrorDecoder.class })
验证503场景:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(ProductServiceNotAvailableException.class,
() -> productClient.getProduct(productId));
验证404场景:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(ProductNotFoundException.class,
() -> productClient.getProduct(productId));
注意:虽然Feign客户端能抛出特定异常,但Spring控制器层仍会将其转换为500错误,需要进一步优化。
6. 在Spring控制器中传播自定义异常
使用@RestControllerAdvice
实现全局异常处理:
6.1. 全局异常处理器实现
定义错误响应结构:
public class ErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private Date timestamp;
@JsonProperty(value = "code")
private int code;
@JsonProperty(value = "status")
private String status;
@JsonProperty(value = "message")
private String message;
@JsonProperty(value = "details")
private String details;
}
实现全局异常处理:
@RestControllerAdvice
public class ProductExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ProductServiceNotAvailableException.class})
public ResponseEntity<ErrorResponse> handleProductServiceNotAvailableException(ProductServiceNotAvailableException exception, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
exception.getMessage(),
request.getDescription(false)),
HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler({ProductNotFoundException.class})
public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException exception, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse(
HttpStatus.NOT_FOUND,
exception.getMessage(),
request.getDescription(false)),
HttpStatus.NOT_FOUND);
}
}
6.2. 控制器异常处理测试
测试服务不可用场景:
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
.andExpect(status().isInternalServerError()).andReturn();
ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(500, errorResponse.getCode());
assertEquals("Product Api is unavailable", errorResponse.getMessage());
测试资源不存在场景:
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
.andExpect(status().isNotFound()).andReturn();
ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(404, errorResponse.getCode());
assertEquals("Product not found", errorResponse.getMessage());
关键点:
- 服务不可用(503)→ 返回500 + 详细错误信息
- 资源不存在(404)→ 返回404 + 明确错误提示
- 若未实现
CustomErrorDecoder
,需在@RestControllerAdvice
中处理默认FeignException
作为兜底方案
7. 总结
本文系统分析了Feign和Spring的默认异常处理机制,并提供了两套优化方案:
- Feign层:通过
CustomErrorDecoder
将HTTP状态码转换为业务异常 - Spring层:使用
@RestControllerAdvice
实现全局异常处理
这种分层处理策略既能保留原始错误信息,又能提供符合业务场景的响应格式,是微服务异常处理的最佳实践。完整代码示例可在GitHub仓库中获取。