1. Overview
1.概述
We expect HTTP API calls between microservices to encounter occasional errors.
我们预计微服务之间的HTTP API调用会偶尔遇到错误。
In Spring Boot with OpenFeign, the default error handler propagates downstream errors, such as Not Found, as Internal Server Error. This is seldom the best way to convey the error. However, both Spring and OpenFeign allow us to provide our own error handling.
在使用OpenFeign,默认错误处理器将下游错误,如Not Found,作为Internal Server Error传播。这很少是最好的方式来传达错误。然而,Spring和OpenFeign都允许我们提供我们自己的错误处理。
In this article, we’ll see how default exception propagation works. We’ll also learn how to supply our own errors.
在这篇文章中,我们将看到默认的异常传播是如何进行的。我们还将学习如何提供我们自己的错误。
2. Default Exception Propagation Strategy
2.默认的异常传播策略
The Feign client makes interactions between microservices straightforward and highly configurable, using annotations and configuration properties. However, API calls might fail due to any random technical reason, bad user requests, or coding errors.
Feign客户端使用注解和配置属性,使微服务之间的交互变得简单明了且高度可配置。然而,API调用可能由于任何随机的技术原因、糟糕的用户请求或编码错误而失败。
Fortunately, Feign and Spring have a sensible default implementation for error handling.
幸运的是,Feign和Spring有一个明智的默认实现来处理错误。
2.1. Default Exception Propagation in Feign
2.1.Feign中默认的异常传播
Feign uses the ErrorDecoder.Default class for its error handling. With this, whenever Feign receives any non-2xx status code, it passes that to the ErrorDecoder’s decode method. The decode method either returns a RetryableException if the HTTP response had a Retry-After header or it returns a FeignException otherwise. When retrying, if the request fails after the default number of retries, then the FeignException will be returned.
Feign使用ErrorDecoder.Default类来处理其错误。有了它,每当Feign收到任何非2xx状态代码时,它就会将其传递给ErrorDecoder的解码方法。decode 方法要么返回一个RetryableException如果HTTP响应有一个重试-。后头,否则它将返回一个FeignException。当重试时,如果请求在默认的重试次数后失败,那么将返回FeignException。
The decode method stores the HTTP method key and response in the FeignException.
decode方法在FeignException中存储了HTTP方法密钥和响应。
2.2. Default Exception Propagation in Spring Rest Controller
2.2.Spring Rest控制器中的默认异常传播
Whenever the RestController receives any unhandled exception, it returns a 500 Internal Server Error response to the client.
每当RestController收到任何未处理的异常,就会向客户端返回一个500 Internal Server Error响应。
Also, Spring provides a well-structured error response with details such as timestamp, HTTP status code, error, and the path:
此外,Spring还提供结构良好的错误响应,包括时间戳、HTTP状态码、错误和路径等细节。
{
"timestamp": "2022-07-08T08:07:51.120+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/myapp1/product/Test123"
}
Let’s take a deep dive into this with an example.
让我们通过一个例子来深入了解这个问题。
3. Example Application
3.应用实例
Let’s imagine we need to build a simple microservice that returns product information from another external service.
让我们想象一下,我们需要建立一个简单的微服务,从另一个外部服务返回产品信息。
First, let’s model the Product class with a few properties:
首先,让我们用一些属性对Product类进行建模。
public class Product {
private String id;
private String productName;
private double price;
}
Then, let’s implement the ProductController with the Get Product endpoint:
然后,让我们用Get Product端点实现ProductController。
@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);
}
}
Next, let’s see how to register the Feign Logger as a Bean:
接下来,让我们看看如何注册Feign Logger作为一个Bean:。
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
Finally, let’s implement the ProductClient to interface with the external API:
最后,让我们实现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);
}
Let’s now explore default error propagation using the above example.
现在让我们用上面的例子来探讨默认的错误传播。
4. Default Exception Propagation
4.默认的异常传播
4.1. Using WireMock Server
4.1.使用WireMock服务器
To experiment, we’ll need to use a mocking framework to simulate the service we’re calling.
为了进行实验,我们需要使用一个模拟框架来模拟我们正在调用的服务。
First, let’s include the WireMockServer Maven dependency:
首先,让我们加入WireMockServer Maven依赖。
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.33.2</version>
<scope>test</scope>
</dependency>
Then, let’s configure and start the WireMockServer:
然后,让我们配置并启动WireMockServer。
WireMockServer wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
The WireMockServer is started at the same host and port that the Feign client is configured to use.
WireMockServer是在同一个host和port,Feign客户端被配置为使用。
4.2. Default Exception Propagation in Feign Client
4.2.在Feign客户端中默认的异常传播
Feign’s default error handler, ErrorDecoder.Default, always throws a FeignException.
Feign的默认错误处理程序,ErrorDecoder.Default,总是抛出一个FeignException。
Let’s mock the getProduct method with the WireMock.stubFor to make it appear to be unavailable:
让我们用WireMock.stubFor来模拟getProduct方法,使其看起来不可用。
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
In the above test case, the ProductClient throws the FeignException when it encounters the 503 error from the downstream service.
在上述测试案例中,当ProductClient遇到下游服务的503错误时,抛出了FeignException。
Next, let’s try the same experiment but with a 404 Not Found response:
接下来,让我们尝试同样的实验,但用404未找到的响应。
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
Again, we’re getting a general FeignException. In this situation, perhaps the user requested something that was wrong and our Spring application needs to know that it’s a bad user request so that it can handle things differently.
同样,我们得到了一个一般的FeignException。在这种情况下,也许用户请求的东西是错误的,我们的Spring应用程序需要知道这是一个糟糕的用户请求,这样它就能以不同的方式处理事情。
We should note that FeignException does have a status property containing the HTTP status code, but a try/catch strategy routes exceptions based on their type, rather than their properties.
我们应该注意到,FeignException确实有一个status属性,包含HTTP状态代码,但是try/catch策略是根据异常的类型,而不是根据其属性来路由。
4.3. Default Exception Propagation in Spring Rest Controller
4.3.Spring Rest控制器中的默认异常传播
Let’s now see how the FeignException propagates back to the requester.
现在让我们看看FeignException如何传播回请求者。
When the ProductController gets the FeignException from the ProductClient, it passes that to its default error handling implementation provided by the framework.
当ProductController获得FeignException 从ProductClient。它将其传递给框架所提供的默认错误处理实现。
Let’s assert when the product service is unavailable:
让我们断言,当产品服务不可用时:。
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()));
Here, we can see that we get the Spring INTERNAL_SERVER_ERROR. This default behavior is not always the best, as different service errors may require different outcomes.
在这里,我们可以看到,我们得到了Spring的INTERNAL_SERVER_ERROR。这种默认行为并不总是最好的,因为不同的服务错误可能需要不同的结果。
5. Propagating Custom Exceptions in Feign With the ErrorDecoder
5.用ErrorDecoder传播Feign中的自定义异常
Instead of always returning the default FeignException, we should return some application-specific exceptions based on the HTTP status code.
不要总是返回默认的FeignException,我们应该根据HTTP状态代码返回一些特定的应用程序异常。
Let’s override the decode method in a custom ErrorDecoder implementation:
让我们在一个自定义的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");
}
}
}
In our custom decode method, we’re returning different exceptions with a few application-specific ones to provide more context for the actual problem. We can also include more details in the application-specific exception messages.
在我们的自定义decode方法中,我们正在返回不同的异常,其中有一些特定于应用程序的异常,以便为实际问题提供更多的背景。我们还可以在特定应用的异常消息中包含更多的细节。
We should note that the decode method returns the FeignException rather than throwing it.
我们应该注意到,t他的decode方法返回FeignException而不是抛出它。
Now, let’s configure the CustomErrorDecoder in the FeignConfig as a Spring Bean:
现在。让我们在CustomErrorDecoder中配置FeignConfig作为Spring 。spaces=”true”>FeignConfig中作为一个Spring Bean。
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
Alternatively, the CustomErrorDecoder can be configured directly in the ProductClient:
另外,CustomErrorDecoder可以直接在ProductClient中进行配置。
@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/",
configuration = { FeignConfig.class, CustomErrorDecoder.class })
Then, let’s check whether the CustomErrorDecoder returns ProductServiceNotAvailableException:
然后,让我们检查一下CustomErrorDecoder是否返回ProductServiceNotAvailableException。
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(ProductServiceNotAvailableException.class,
() -> productClient.getProduct(productId));
Again, let’s write a test case to assert the ProductNotFoundException when the product is not present:
再次,让我们写一个测试案例,在产品不存在时断言ProductNotFoundException:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(ProductNotFoundException.class,
() -> productClient.getProduct(productId));
While we’re now providing a variety of exceptions from the Feign client, Spring will still produce a generic internal server error when it catches them all. Since this is not what we desire, let’s see how we can improve on that.
虽然我们现在从Feign客户端提供了各种异常,但Spring在抓到这些异常时仍然会产生一个通用的内部服务器错误。由于这不是我们所希望的,让我们看看如何改进。
6. Propagating Custom Exceptions in Spring Rest Controller
6.在Spring Rest Controller中传播自定义异常
As we’ve seen, the default Spring Boot error handler provides a generic error response. API Consumers might need detailed information with relevant error responses. Ideally, the error response should be able to explain the problem and help in debugging.
正如我们所见,默认的Spring Boot错误处理程序提供了一个通用的错误响应。API消费者可能需要相关错误响应的详细信息。理想情况下,错误响应应该能够解释问题并帮助调试。
We could override the default exception handler in the RestController in many ways.
我们可以用很多方式覆盖RestController中的默认异常处理程序。
We’ll look into one such approach to handle errors with the RestControllerAdvice annotation.
我们将研究一个这样的方法,用RestControllerAdvice注释来处理错误。
6.1. Using @RestControllerAdvice
6.1.使用@RestControllerAdvice
The @RestControllerAdvice annotation allows us to consolidate multiple exceptions into a single, global error handling component.
@RestControllerAdvice注解允许我们将多个异常整合到一个全局错误处理组件中。
Let’s imagine a scenario where the ProductController needs to return a different custom error response based on the downstream error.
让我们设想这样一个场景:ProductController 需要根据下游的错误返回一个不同的自定义错误响应。
First, let’s create the ErrorResponse class to customize the error response:
首先,让我们创建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;
}
Now, let’s subclass the ResponseEntityExceptionHandler and include the @ExceptionHandler annotation with the error handlers:
现在,让我们对 ResponseEntityExceptionHandler进行子类化,并将@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);
}
}
In the above code, the ProductServiceNotAvailableException returns as an INTERNAL_SERVER_ERROR response to the client. In contrast, a user-specific error like ProductNotFoundException is handled differently and returns as a NOT_FOUND response.
在上述代码中。ProductServiceNotAvailableException作为一个INTERNAL_SERVER_ERROR响应返回给客户端。相反,像ProductNotFoundException这样的用户特定错误会得到不同的处理,并作为一个NOT_FOUND响应返回。
6.2. Testing the Spring Rest Controller
6.2.测试Spring Rest控制器
Let’s test the ProductController when product service is unavailable:
让我们测试一下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());
Again, let’s test the same ProductController but with a product not found error:
再一次,让我们测试同一个ProductController,但有一个产品未找到的错误。
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());
The above tests show how the ProductController returns different error responses based on the downstream error.
上述测试显示了ProductController如何根据下游错误返回不同的错误响应。
If we hadn’t implemented our CustomErrorDecoder, then the RestControllerAdvice is required to handle the default FeignException as a fallback to have a generic error response.
如果我们没有实现我们的CustomErrorDecoder。那么RestControllerAdvice就需要处理默认的FeignException作为回退,以获得一个通用的错误响应。
7. Conclusion
7.结语
In this article, we’ve explored how the default error handling is implemented in Feign and Spring.
在这篇文章中,我们已经探讨了Feign和Spring中如何实现默认的错误处理。
Also, we’ve seen how we can customize that in Feign client with CustomErrorDecoder and in the Rest Controller with RestControllerAdvice.
此外,我们已经看到我们如何在Feign客户端中用CustomErrorDecoder和在Rest Controller中用RestControllerAdvice进行定制。。
As always, all these code examples can be found over on GitHub.
一如既往,所有这些代码实例都可以在GitHub上找到。