Micronaut 中的錯誤處理
1. 概述
錯誤處理是開發系統時的主要關注點之一。在程式碼級別,錯誤處理處理我們編寫的程式碼拋出的異常。在服務等級上,錯誤是**指我們回傳的所有不成功的回應**。
在大型系統中,以一致的方式處理類似的錯誤是一個很好的做法。例如,在具有兩個控制器的服務中,我們希望身份驗證錯誤回應相似,以便我們可以更輕鬆地偵錯問題。退一步來說,為了簡單起見,我們可能希望系統的所有服務都有相同的錯誤回應。我們可以透過使用全域異常處理程序來實現這種方法。
在本教程中,我們將重點放在 Micronaut 中的錯誤處理。與大多數 Java 框架類似,Micronaut 提供了一個常見的錯誤處理機制。我們將討論此機制並在範例中進行演示。
2. Micronaut 中的錯誤處理
在編碼中,我們唯一可以認為理所當然的事情就是錯誤會發生。無論我們編寫多好的程式碼、定義良好的測試和測試覆蓋率,我們都無法避免錯誤。因此,我們如何在系統中處理它們應該是我們主要關心的問題之一。透過使用狀態處理程序和異常處理程序等一些框架功能,Micronaut 中的錯誤處理變得更加容易。
如果我們熟悉 Spring 中的錯誤處理,那麼就很容易熟悉 Micronaut 的方式。 Micronaut 提供處理程序來處理拋出的異常,也提供處理特定回應狀態的處理程序。在錯誤狀態處理中,我們可以設定局部範圍或全域範圍。異常處理僅在全域範圍內。
值得一提的是,如果我們利用 Micronaut 環境功能,我們可以為不同的活動環境設定不同的全域錯誤處理程序。例如,如果我們有一個發布事件訊息的錯誤處理程序,我們可以利用活動環境並跳過本地環境上的訊息發布功能。
3. Micronaut 中使用@Error註解進行錯誤處理
在 Micronaut 中,我們可以使用@Error**註解**來定義錯誤處理程序。此註解是在方法層級定義的,它應該位於@Controller註解的類別內部。它具有一些與其他控制器方法類似的功能,例如它可以在參數上使用請求綁定註釋來存取請求標頭、請求正文等。
透過在 Micronaut 中使用@Error註解進行錯誤處理,我們可以處理異常或回應狀態碼。這與其他流行的 Java 框架不同,後者僅提供每個異常的處理程序。
錯誤處理程序的一個特點是我們可以為它們設定一個範圍。透過將範圍設為global ,我們可以擁有一個處理程序來處理整個服務的 404 回應。如果我們不設定範圍,則處理程序僅處理同一控制器中拋出的指定錯誤。
3.1.使用@Error註解處理回應錯誤碼
@Error註釋為我們提供了一種根據錯誤回應狀態處理錯誤的方法。這樣,我們就可以定義一個通用的方法來處理所有HttpStatus.NOT_FOUND回應,例如。我們可以處理的錯誤狀態應該是io.micronaut.http.HttpStatus列舉中定義的狀態:
@Controller("/notfound")
public class NotFoundController {
@Error(status = HttpStatus.NOT_FOUND, global = true)
public HttpResponse<JsonError> notFound(HttpRequest<?> request) {
JsonError error = new JsonError("Page Not Found")
.link(Link.SELF, Link.of(request.getUri()));
return HttpResponse.<JsonError> notFound().body(error);
}
}
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
在此控制器中,我們定義一個以@Error註解的方法來處理HttpStatus.NOT_FOUND回應。範圍設定為global ,因此所有 404 錯誤都應通過此方法。處理後,所有此類錯誤都應返回狀態代碼 404,並修改正文,其中包含錯誤訊息「找不到頁面」和連結。
請注意,即使我們使用@Controller註釋,該控制器也沒有指定任何HttpMethod ,因此它並不完全像傳統控制器一樣工作,但正如我們前面提到的,它具有一些實現相似之處。
現在假設我們有一個端點給出NOT_FOUND錯誤回應:
@Get("/not-found-error")
public HttpResponse<String> endpoint1() {
return HttpResponse.notFound();
}
「/not-found- NOT_FOUND 」端點應該始終傳回 404。
@Test
public void whenRequestThatThrows404_thenResponseIsHandled(
RequestSpecification spec
) {
spec.given()
.basePath(ERRONEOUS_ENDPOINTS_PATH)
.when()
.get("/not-found-error")
.then()
.statusCode(404)
.body(Matchers.containsString("\"message\":\"Page Not Found\",\"_links\":"));
}
此 Micronaut 測試向「/not-found-error」端點發出 GET 請求,並傳回預期的 404 狀態碼。但是,透過斷言回應正文,我們可以驗證回應是否透過處理程序傳遞,因為錯誤訊息是我們新增到處理程序的訊息。
需要澄清的一件事是,如果我們更改基本路徑和路徑以指向NotFoundController ,因為該控制器中沒有定義 GET,只有錯誤,那麼伺服器就是拋出 404 的伺服器,並且處理程序仍然處理它。
3.2.使用@Error註解處理異常
在 Web 服務中,如果未在任何地方擷取和處理異常,則控制器預設會傳回內部伺服器錯誤。 Micronaut 中的錯誤處理為此類情況提供了@Error註釋。
讓我們建立一個拋出異常的端點和一個處理這些特定異常的處理程序:
@Error(exception = UnsupportedOperationException.class)
public HttpResponse<JsonError> unsupportedOperationExceptions(HttpRequest<?> request) {
log.info("Unsupported Operation Exception handled");
JsonError error = new JsonError("Unsupported Operation")
.link(Link.SELF, Link.of(request.getUri()));
return HttpResponse.<JsonError> notFound().body(error);
}
@Get("/unsupported-operation")
public HttpResponse<String> endpoint5() {
throw new UnsupportedOperationException();
}
“/unsupported-operation”端點僅拋出UnsupportedOperationException異常。 unsupportedOperationExceptions方法使用@Error註解來處理這些異常。由於不支援該資源,因此它會傳回 404 個錯誤代碼,並傳回帶有訊息「不支援的動作」的回應正文。請注意,此範例中的範圍是本地的,因為我們沒有將其設定為global 。
如果我們到達此端點,我們應該會看到處理程序處理它並傳回unsupportedOperationExceptions方法中定義的回應:
@Test
public void whenRequestThatThrowsLocalUnsupportedOperationException_thenResponseIsHandled(
RequestSpecification spec
) {
spec.given()
.basePath(ERRONEOUS_ENDPOINTS_PATH)
.when()
.get("/unsupported-operation")
.then()
.statusCode(404)
.body(containsString("\"message\":\"Unsupported Operation\""));
}
@Test
public void whenRequestThatThrowsExceptionInOtherController_thenResponseIsNotHandled(
RequestSpecification spec
) {
spec.given()
.basePath(PROBES_ENDPOINTS_PATH)
.when()
.get("/readiness")
.then()
.statusCode(500)
.body(containsString("\"message\":\"Internal Server Error\""));
}
在第一個範例中,我們請求「/unsupported-operation」端點,該端點會拋出UnsupportedOperationException例外。由於本地處理程序位於同一控制器中,因此我們會從處理程序獲得我們期望的回應,並帶有修改後的回應錯誤訊息「不支援的操作」。
在第二個範例中,我們從不同的控制器要求「/readiness」端點,這也會引發UnsupportedOperationException異常。由於此端點是在不同的控制器上定義的,因此本機處理程序不會處理異常,因此我們得到的回應是預設的錯誤代碼 500。
4. Micronaut 中使用ExceptionHandler介面進行錯誤處理
Micronaut 也提供了實作ExceptionHandler介面的選項,以在全域範圍內處理特定例外狀況。這種方法需要每個異常一個類,這意味著預設情況下它們必須位於全域範圍內。
Micronaut 提供了一些預設的異常處理程序,例如:
-
jakarta.validation.ConstraintViolationException -
com.fasterxml.jackson.core.JsonProcessingException -
UnsupportedMediaException - 還有更多
如果需要,當然可以在我們的服務上覆蓋這些處理程序。
需要考慮的一件事是異常層次結構。當我們為特定異常 A 建立處理程序時,擴展 A 的異常 B 也將屬於同一處理程序,除非我們為該特定異常 B 實現另一個處理程序。
4.1.處理例外
如前所述,我們可以使用ExceptionHandler介面來全域處理特定類型的例外:
@Slf4j
@Produces
@Singleton
@Requires(classes = { CustomException.class, ExceptionHandler.class })
public class CustomExceptionHandler implements ExceptionHandler<CustomException, HttpResponse<String>> {
@Override
public HttpResponse<String> handle(HttpRequest request, CustomException exception) {
log.info("handling CustomException: [{}]", exception.getMessage());
return HttpResponse.ok("Custom Exception was handled");
}
}
在此類中,我們實作接口,該接口使用泛型來定義我們將處理的異常。在本例中,它是我們先前定義的CustomException 。此類需要使用@Requires註解並包含異常類,也包含介面。 handle方法將觸發異常的請求和異常物件作為參數。然後,我們只需在回應正文中新增自訂訊息,返回 200 個回應狀態代碼。
現在假設我們有一個拋出CustomException的端點:
@Get("/custom-error")
public HttpResponse<String> endpoint3(@Nullable @Header("skip-error") String isErrorSkipped) {
if (isErrorSkipped == null) {
throw new CustomException("something else went wrong");
}
return HttpResponse.ok("Endpoint 3");
}
“/custom-error”端點接受isErrorSkipped標頭,以啟用/停用拋出的例外。如果我們不包含標頭,則會引發異常:
@Test
public void whenRequestThatThrowsCustomException_thenResponseIsHandled(
RequestSpecification spec
) {
spec.given()
.basePath(ERRONEOUS_ENDPOINTS_PATH)
.when()
.get("/custom-error")
.then()
.statusCode(200)
.body(is("Custom Exception was handled"));
}
在此測試中,我們請求“/custom-error”端點,不包含標頭。因此,會拋出CustomException異常。然後,我們透過斷言處理程序期望的回應代碼和回應正文來驗證處理程序是否已處理此異常。
4.2.基於層次結構處理異常
對於未明確處理的異常,如果它們擴展具有處理程序的異常,則它們將由同一處理程序隱式處理。假設我們有一個擴展CustomException的CustomeChildException :
public class CustomChildException extends CustomException {
public CustomChildException(String message) {
super(message);
}
}
並且有一個端點拋出此異常:
@Get("/custom-child-error")
public HttpResponse<String> endpoint4(@Nullable @Header("skip-error") String isErrorSkipped) {
log.info("endpoint4");
if (isErrorSkipped == null) {
throw new CustomChildException("something else went wrong");
}
return HttpResponse.ok("Endpoint 4");
}
「 /custom-child-error 」端點接受isErrorSkipped標頭,以啟用/停用拋出的例外。如果我們不包含標頭,則會引發異常:
@Test
public void whenRequestThatThrowsCustomChildException_thenResponseIsHandled(
RequestSpecification spec
) {
spec.given()
.basePath(ERRONEOUS_ENDPOINTS_PATH)
.when()
.get("/custom-child-error")
.then()
.statusCode(200)
.body(is("Custom Exception was handled"));
}
此測試命中“/custom-child-error”端點並觸發CustomChildException異常。從回應中,我們可以透過對我們期望從處理程序獲得的回應代碼和回應正文進行斷言來驗證處理程序是否也處理了此子異常。
5. 結論
在本文中,我們詳細介紹了 Micronaut 中的錯誤處理。處理錯誤的方法有多種,包括處理異常或處理錯誤回應狀態代碼。我們也了解如何將處理程序應用於不同的範圍(本地和全域)。最後,我們透過一些程式碼範例示範了討論的所有選項,並使用 Micronaut 測試來驗證結果。
與往常一樣,所有原始程式碼都可以在 GitHub 上取得。