1. 簡介
在本教程中,我們將探討使用 Jersey 處理異常的不同方法,Jersey 是一個 JAX-RS 實現。
JAX-RS 提供了許多機制來處理異常,我們可以選擇和組合它們。 處理 REST 異常是構建更好 API 的重要步驟。 在我們的用例中,我們將構建一個股票購買 API,並查看每個步驟如何影響彼此。
2. 場景設置
我們的最小配置包括創建倉庫、幾個 Bean 以及一些端點。它從我們的資源配置開始。在那裏,我們將使用 @ApplicationPath 定義我們的起始 URL 以及我們的端點包:
@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
public ExceptionHandlingConfig() {
packages("com.baeldung.jersey.exceptionhandling.rest");
}
}
2.1. Bean
我們需要兩個 Bean:Stock 和 Wallet,以便保存 Stock 併購買它們。對於我們的 Stock,我們只需要一個 price 屬性來幫助驗證。更重要的是,我們的 Wallet 類將具有驗證方法,以幫助構建我們的場景:
public class Wallet {
private String id;
private Double balance = 0.0;
// getters and setters
public Double addBalance(Double amount) {
return balance += amount;
}
public boolean hasFunds(Double amount) {
return (balance - amount) >= 0;
}
}
2.2. Endpoints
類似於地,我們的 API 將有兩個端點。這些將定義標準方法來保存和檢索我們的 Bean:
@Path("/stocks")
public class StocksResource {
// POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
// POST and GET methods
}
例如,讓我們看看 StocksResource 中的 GET 方法:
@GET
@Path("/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response get(@PathParam("ticker") String id) {
Optional<Stock> stock = stocksRepository.findById(id);
stock.orElseThrow(() -> new IllegalArgumentException("ticker"));
return Response.ok(stock.get())
.build();
}
在我們的 GET 方法中,我們正在拋出我們的第一個異常。我們稍後將處理它,以便我們可以看到它的影響。
3. 異常處理時發生的情況?
當未處理的異常發生時,我們可能會暴露有關我們應用程序內部的敏感信息。如果我們嘗試從 StocksResource 中使用不存在的 Stock 進行 GET 請求,我們會得到類似如下的頁面:
該頁面顯示應用程序服務器和版本,這可能會幫助潛在攻擊者利用漏洞。此外,還有關於我們的類名和行號的信息,這也可能會幫助攻擊者。 最重要的是,大部分信息對 API 用户來説毫無用處,給用户留下了糟糕的印象。
為了幫助控制異常響應,JAX-RS 提供了 ExceptionMapper 和 WebApplicationException 類。讓我們看看它們是如何工作的。
4. 4. 定製異常與 WebApplicationException
使用 WebApplicationException,我們可以創建自定義異常。此特殊類型的RuntimeException 允許我們定義響應狀態和實體。
public class InvalidTradeException extends WebApplicationException {
public InvalidTradeException() {
super("invalid trade operation", Response.Status.NOT_ACCEPTABLE);
}
}
此外,JAX-RS 定義了 WebApplicationException 的子類,用於常見的 HTTP 狀態碼。這些包括有用的異常,如 NotAllowedException、BadRequestException 等。但是,當我們想要更復雜的錯誤消息時,我們可以返回 JSON 響應。
4.1. JSON 異常
我們可以創建簡單的 Java 類並將其包含在我們的 Response 中。在我們的示例中,我們有一個 subject 屬性,我們將使用它來封裝上下文數據:
public class RestErrorResponse {
private Object subject;
private String message;
// getters and setters
}
由於此異常不應該被操縱,因此我們不必擔心 subject 的類型。
4.2. 充分利用一切
要了解如何使用自定義異常,讓我們定義一個用於購買 Stock 的方法:
@POST
@Path("/{wallet}/buy/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response postBuyStock(
@PathParam("wallet") String walletId, @PathParam("ticker") String id) {
Optional<Stock> stock = stocksRepository.findById(id);
stock.orElseThrow(InvalidTradeException::new);
Optional<Wallet> w = walletsRepository.findById(walletId);
w.orElseThrow(InvalidTradeException::new);
Wallet wallet = w.get();
Double price = stock.get()
.getPrice();
if (!wallet.hasFunds(price)) {
RestErrorResponse response = new RestErrorResponse();
response.setSubject(wallet);
response.setMessage("insufficient balance");
throw new WebApplicationException(Response.status(Status.NOT_ACCEPTABLE)
.entity(response)
.build());
}
wallet.addBalance(-price);
walletsRepository.save(wallet);
return Response.ok(wallet)
.build();
}
在這個方法中,我們使用了我們創建的一切。我們拋出一個InvalidTradeException 用於不存在的股票或錢包。 並且,如果資金不足,我們將構建一個 RestErrorResponse,其中包含我們的 Wallet,並將其作為 WebApplicationException 拋出。
4.3. 用例示例
首先,讓我們創建一個 Stock:
$ curl 'http://localhost:8080/jersey/exception-handling/stocks' -H 'Content-Type: application/json' -d '{
"id": "STOCK",
"price": 51.57
}'
{"id": "STOCK", "price": 51.57}
然後,一個 Wallet 來購買它:
$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
"id": "WALLET",
"balance": 100.0
}'
{"balance": 100.0, "id": "WALLET"}
然後,我們將使用我們的 Wallet 購買 Stock:
$ curl -X POST 'http://localhost:8080/jersey/exception-handling/wallets/WALLET/buy/STOCK'
{"balance": 48.43, "id": "WALLET"}
然後,我們將在響應中獲得更新後的餘額。 此外,如果我們嘗試再次購買,我們將獲得詳細的 RestErrorResponse:
{
"message": "insufficient balance",
"subject": {
"balance": 48.43,
"id": "WALLET"
}
}
5. Unhandled Exceptions With ExceptionMapper
為了明確一點,拋出 WebApplicationException 無法消除默認錯誤頁。我們必須為我們的 Response 指定一個實體,而 InvalidTradeException 不適用這個要求。 經常情況下,無論我們嘗試處理多少個場景,一個未處理的異常仍然可能發生。 因此,最好先處理這些異常。 通過 ExceptionMapper,我們定義了針對特定類型的異常的捕獲點,並在提交 Response 之前修改它。
public class ServerExceptionMapper implements ExceptionMapper<WebApplicationException> {
@Override
public Response toResponse(WebApplicationException exception) {
String message = exception.getMessage();
Response response = exception.getResponse();
Status status = response.getStatusInfo().toEnum();
return Response.status(status)
.entity(status + ": " + message)
.type(MediaType.TEXT_PLAIN)
.build();
}
}
例如,我們只是簡單地將異常信息傳遞到我們的 Response 中,這將顯示我們返回的內容。 隨後,我們可以進一步檢查狀態碼,然後再構建我們的 Response:
switch (status) {
case METHOD_NOT_ALLOWED:
message = "HTTP METHOD NOT ALLOWED";
break;
case INTERNAL_SERVER_ERROR:
message = "internal validation - " + exception;
break;
default:
message = "[unhandled response code] " + exception;
}
5.1. Handling Specific Exceptions
如果某個特定的 Exception 經常被拋出,我們可以創建一個針對它的 ExceptionMapper。 在我們的端點中,我們為了簡單的驗證拋出 IllegalArgumentException,所以我們先創建一個針對它的映射器。 這個時候,我們使用 JSON 響應:
public class IllegalArgumentExceptionMapper
implements ExceptionMapper<IllegalArgumentException> {
@Override
public Response toResponse(IllegalArgumentException exception) {
return Response.status(Response.Status.EXPECTATION_FAILED)
.entity(build(exception.getMessage()))
.type(MediaType.APPLICATION_JSON)
.build();
}
private RestErrorResponse build(String message) {
RestErrorResponse response = new RestErrorResponse();
response.setMessage("an illegal argument was provided: " + message);
return response;
}
}
現在,每次未處理的 IllegalArgumentException 在我們的應用程序中發生,我們的 IllegalArgumentExceptionMapper 將處理它。
5.2. Configuration
為了激活我們的異常映射器,我們必須返回到我們的 Jersey 資源配置並註冊它們:
public ExceptionHandlingConfig() {
// packages ...
register(IllegalArgumentExceptionMapper.class);
register(ServerExceptionMapper.class);
}
這足以消除默認錯誤頁。 然後,根據被拋出的內容,Jersey 將使用我們的一個異常映射器。 例如,當嘗試獲取一個不存在的 Stock 時,IllegalArgumentExceptionMapper 將被使用:
$ curl 'http://localhost:8080/jersey/exception-handling/stocks/NONEXISTENT'
{"message": "an illegal argument was provided: ticker"}
同樣,對於其他未處理的異常,更廣泛的 ServerExceptionMapper 將被使用。 例如,當我們使用錯誤的 HTTP 方法時:
$ curl -X POST 'http://localhost:8080/jersey/exception-handling/stocks/STOCK'
Method Not Allowed: HTTP 405 Method Not Allowed
6. 結論
在本文中,我們看到了使用 Jersey 處理異常的多種方法。 此外,我們還了解了其重要性以及如何進行配置。 隨後,我們構建了一個簡單的場景,以便應用這些方法。 最終,我們擁有一個更友好、更安全的 API。