1. 概述
我們預計微服務之間的 HTTP API 調用可能會遇到偶爾的錯誤。
在 Spring Boot 中使用 OpenFeign 時,默認錯誤處理程序會將下游錯誤(如 未找到)傳播為 內部服務器錯誤。 這種方式通常不是最佳方式來表達錯誤。 但是,Spring 和 OpenFeign 都允許我們提供自己的錯誤處理。
在本文中,我們將瞭解默認異常傳播的工作原理。 我們還將學習如何提供自定義錯誤處理。
2. 默認異常傳播策略
Feign 客户端使微服務之間的交互變得簡單且高度可配置,通過使用註解和配置屬性。然而,API 調用可能會由於任何隨機技術原因、不良的用户請求或編碼錯誤而失敗。
幸運的是,
Feign 和 Spring 具有合理的默認實現,用於錯誤處理。
2.1. Feign 中默認異常傳播
Feign 使用 <em >ErrorDecoder</em> 類進行其錯誤處理。通過此機制,當 Feign 接收到任何非 2xx 狀態碼時,它會將該狀態碼傳遞到 <em >ErrorDecoder</em> 的 <em >decode</em > 方法。<em >decode</em > 方法如果返回一個 <em >RetryableException</em >,則 HTTP 響應包含 <em >Retry-After</em > 頭部;否則,返回一個 <em >FeignException</em >。在重試時,如果請求在默認重試次數後仍然失敗,則返回 <em >FeignException</em >。
<em >decode</em > 方法存儲 HTTP 方法鍵和響應信息到 <em >FeignException</em > 中。
2.2. Spring Rest Controller 中的默認異常傳播
當 RestController 接收到任何未處理的異常時,它會將一個 500 Internal Server Error 響應返回給客户端。
此外,Spring 提供了結構良好的錯誤響應,其中包含時間戳、HTTP 狀態碼、錯誤信息和路徑等信息:
{
"timestamp": "2022-07-08T08:07:51.120+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/myapp1/product/Test123"
}讓我們用一個例子來深入探討一下這個問題。
3. 示例應用程序
假設我們需要構建一個簡單的微服務,該微服務從另一個外部服務中返回產品信息。
首先,讓我們使用以下屬性對 Product 類進行建模:
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. 默認異常傳播
This section describes how exceptions are propagated when they are not explicitly handled within a method. Understanding default exception propagation is crucial for debugging and troubleshooting applications.
When an exception is thrown in a method and is not caught by a try-catch block within that method, it propagates up the call stack to the calling method. This continues until either an exception is caught or the program terminates.
Key Concepts:
- Call Stack: The sequence of function calls that leads to the current point of execution.
- Propagation: The process of an exception being passed from one method to another.
- Uncaught Exception: An exception that is not caught by any
try-catchblock in the call stack.
Example:
public class Example {
public void methodA() {
throw new IllegalArgumentException("Invalid input");
}
public void methodB() {
methodA();
System.out.println("This line will not be executed if an exception is thrown in methodA.");
}
public static void main(String[] args) {
Example example = new Example();
example.methodB();
}
}
In this example, if methodA() throws an IllegalArgumentException, it will propagate to methodB(). However, since methodB() does not have a try-catch block to handle this exception, the program will terminate, and the exception will be treated as an uncaught exception.
4.1. 使用 WireMock Server
為了進行實驗,我們需要使用一個 Mocking 框架來模擬我們所調用的服務。
首先,讓我們包含 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 在相同的 host 和 port 上啓動,與 Feign 客户端配置使用的相同。
4.2. Feign 客户端的默認異常傳播
Feign 的默認錯誤處理程序,<em>ErrorDecoder.Default</em>,始終會拋出 <em>FeignException</em>。
讓我們使用 <em>WireMock.stubFor</em> 模擬 <em>getProduct</em> 方法,使其看起來不可用:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));在上述測試用例中,<em >ProductClient</em> 在遇到下游服務返回的 503 錯誤時會拋出 <em >FeignException</em>。
接下來,我們嘗試相同的實驗,但使用 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 異常。在這種情況下,用户可能請求了不正確的內容,我們的 Spring 應用需要知道這是一個無效的用户請求,以便採取不同的處理方式。
需要注意的是,FeignException 確實具有 status 屬性,其中包含 HTTP 狀態碼,但 try / catch 策略根據異常的類型進行路由,而不是根據其屬性。
4.3. Spring Rest Controller 中默認異常傳播
現在,讓我們看看 FeignException 如何向請求方傳播。
當 ProductController 從 ProductClient 接收到 FeignException 時,它會將該異常傳遞到由框架提供的默認錯誤處理實現。
讓我們斷言當產品服務不可用時:
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 傳播自定義異常
與其始終返回默認的 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();
}當然,以下是翻譯後的內容:
或者,CustomErrorDecoder 可以直接配置在 ProductClient 中:
@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));再次編寫一個測試用例,以斷言當產品不存在時,拋出 ProductNotFoundException 異常:
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(ProductNotFoundException.class,
() -> productClient.getProduct(productId));雖然我們現在提供了多種 Feign 客户端的異常處理方式,但 Spring 在捕獲所有異常時仍然會產生通用的內部服務器錯誤。由於這不是我們想要的結果,讓我們看看如何改進它。
6. 在 Spring Rest Controller 中傳播自定義異常
正如您所見,默認的 Spring Boot 錯誤處理程序提供了一個通用的錯誤響應。API 消費者可能需要包含詳細信息和相關錯誤響應。理想情況下,錯誤響應應該能夠解釋問題並幫助調試。
我們可以通過在 RestController 中覆蓋默認的異常處理程序來實現。
我們將探討使用 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,幷包含帶有錯誤處理程序的 @ExceptionHandler</em/> 註解:
@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
讓我們測試在 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,但這次模擬產品未找到錯誤:
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 就需要作為回退機制來處理默認的 FeignException,以便提供通用的錯誤響應。
7. 結論
本文介紹了 Feign 和 Spring 中默認錯誤處理的實現方式。
此外,我們還了解到可以通過 Feign 客户端使用 <em >CustomErrorDecoder</em >,以及在 Rest Controller 中使用 `RestControllerAdvice> 來自定義錯誤處理。