添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
低调的海豚  ·  MySQL数据库ORDER ...·  1 年前    · 
欢快的骆驼  ·  WPF ...·  1 年前    · 

微服务间的 HTTP API 调用可能会出现异常。在 Spring Boot 中使用 OpenFeign 时,默认会把下游服务的 “Not Found” 等异常全部当做 “Internal Server Error” 响应给客户端。这并不是异常的最佳处理方式,幸而,Spring 和 OpenFeign 都提供了一些机制,允许我们自定义异常处理。

本文将带你了解,Spring Boot 和 OpenFeign 默认的异常传播、处理机制,以及如何实现自定义的异常处理。

2、默认的异常传播策略

2.1、Feign 中默认的异常传播

Feign 使用 ErrorDecoder.Default 内部实现类进行异常处理。每当 Feign 收到任何非 2xx 状态码时,都会将其传递给 ErrorDecoder decode 方法。

如果 HTTP 响应有 Retry-After 头信息, decode 方法就会返回 RetryableException ,否则就会返回 FeignException

重试时,如果请求在默认重试次数之后仍然失败,则会返回 FeignException

decode 方法将 HTTP 方法 key 和响应存储在 FeignException 中。

2.2、Spring Rest Controller 中的默认异常传播

只要 RestController 收到任何未处理的异常,它就会向客户端返回 500 Internal Server Error (内部服务器错误)响应。

该异常响应包含时间戳、HTTP 状态码、异常信息和路径等信息:

"timestamp" : "2022-07-08T08:07:51.120+00:00" , "status" : 500 , "error" : "Internal Server Error" , "path" : "/myapp1/product/Test123"

下面,我们通过一个例子来深入了解一下。

3、示例应用

构建一个简单的微服务,调用另一个外部服务返回 product 信息。

首先,创建 Product Model 类。

public class Product {
    private String id;
    private String productName;
    private double price;

然后,在 ProductController 中实现 Get Product 端点:

@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 Logger 注册为 Bean:

public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;

最后,实现 ProductClient ,以调用外部 API 接口:

@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 Server

使用 Wiremock 框架来模拟被调用的服务,以进行测试。

首先,添加 WireMockServer Maven 依赖:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.33.2</version>
    <scope>test</scope>
</dependency>

然后,配置并启动 WireMockServer

WireMockServer wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();

WireMockServer 在与配置的 Feign 客户端相同的 host port 上启动。

4.2、Feign Client 默认的异常传播

Feign 默认的 Error handler, ErrorDecoder.Default 总是抛出 FeignException

使用 WireMock.stubFor 来模拟 getProduct 方法,返回 SERVICE_UNAVAILABLE 状态(服务不可用)。

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));

如上,当 ProductClient 遇到下游服务的 503 (SERVICE_UNAVAILABLE)异常时,会抛出 FeignException

接着,使用 404 Not Found 响应进行同样的测试:

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));

同样地,客户端再次收到 FeignException 。这并不合理,因为 404 NOT_FOUND 异常可能是用户提交的查询有问题。我们需要对不同的异常进行区分,以进行不同的处理。

注意, FeignException 确实具有一个包含 HTTP 状态码的 status 属性,但是 try/catch 策略是根据异常的类型而不是属性来路由异常。

4.3、Spring Rest Controller 的异常传播

接着,看一下 FeignException 是如何传播回客户端的。

ProductController ProductClient 捕获 FeignException 时,它会将其传递给 Spring Boot 默认的异常处理器。

当 product service 不可用时,进行断言:

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()));

如上,客户端最终得到得异常状态是 Spring 的 INTERNAL_SERVER_ERROR 状态。

5、使用 ErrorDecoder 在 Feign 中传播自定义异常

为了避免永远返回默认的 FeignException ,我们可以根据 HTTP 状态码返回一些特定的异常。

自定义 ErrorDecoder 实现,覆写 decode 方法:

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");

在自定义的 decode 方法中,为不同的状态码返回了不同的异常,还在异常中提供了更多的细节信息。

注意, decode 方法是返回 FeignException ,而不是抛出异常。

现在,在 FeignConfig 中将 CustomErrorDecoder 配置为 Spring Bean:

@Bean
public ErrorDecoder errorDecoder() {
   return new CustomErrorDecoder();

或者,也可以直接在 ProductClient 中配置 CustomErrorDecoder

@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/", 
   configuration = { FeignConfig.class, CustomErrorDecoder.class })

然后,测试 CustomErrorDecoder 是否会返回 ProductServiceNotAvailableException

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(ProductServiceNotAvailableException.class, 
  () -> productClient.getProduct(productId));

同样,再写一个测试用例,在 product 不存在时断言 ProductNotFoundException

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(ProductNotFoundException.class, 
  () -> productClient.getProduct(productId));

现在,Feign Client 会根据不同的状态码返回不同的异常,但是在 Spring 捕获该异常后,还是会统一对客户端响应 “internal server error” 状态。

6、在 Spring Rest Controller 中传播自定义异常

Spring Boot 默认的 Error handler 提供了通用的异常响应。客户端可能需要更为详细的异常信息。

有多种方式可以自定义 RestController 的 Exception Handler。在这里我们使用 RestControllerAdvice 注解来处理异常。

6.1、使用 @RestControllerAdvice

@RestControllerAdvice 注解允许我们将多个异常合并到一个全局异常处理组件中。

假如: ProductController 需要根据下游服务的异常返回不同的自定义异常响应。

首先,创建 ErrorResponse 类,表示异常响应:

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;

现在,创建 ResponseEntityExceptionHandler 的子类实现 ProductExceptionHandler ,并在异常处理方法上添加 @ExceptionHandler 注解:

@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);

如上, ProductServiceNotAvailableException 异常会响应 INTERNAL_SERVER_ERROR 状态给客户端。而,用户特定的异常(如 ProductNotFoundException )会以不同的方式处理,并返回一个 NOT_FOUND 响应。

6.2、测试 Spring Rest Controller

在 product service 不可用时测试 ProductController

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());

接着,再次测试同一个 ProductController ,但这次会返回 “Product not found” 异常消息:

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());

上述测试显示了 ProductController 如何根据下游服务的异常返回不同状态的异常响应。

如果我们没有实现自定义的 CustomErrorDecoder ,那么需要使用 RestControllerAdvice 来直接处理 Feign Client 默认的 FeignException

在本文中,我们学习了如何在 Feign Client 中使用 ErrorDecoder 以及在 Rest Controller 中使用 RestControllerAdvice 进行自定义异常处理。

参考: https://www.baeldung.com/category/spring

  • Kafka 中的 InstanceAlreadyExistsException 异常
  • 使用 MongoDB 和 Spring AI 构建 RAG 应用
  • Spring Boot v3.3.4 发布
  • 使用 OpenFeign 和 CompletableFuture 并行处理多个 HTTP 请求
  • 使用 Stream API 处理 JDBC ResultSet
  •