Перейти к основному содержимому

Обработка исключений с помощью Джерси

· 7 мин. чтения

1. Введение

В этом руководстве мы рассмотрим различные способы обработки исключений с помощью Jersey , который является реализацией JAX-RS .

JAX-RS предоставляет нам множество механизмов для обработки исключений, которые мы можем выбирать и комбинировать. Обработка исключений REST — важный шаг для создания лучшего API. В нашем случае мы создадим API для покупки акций и посмотрим, как каждый шаг влияет на другой.

2. Настройка сценария

Наша минимальная настройка включает в себя создание репозитория , пары bean-компонентов и некоторых конечных точек. Все начинается с конфигурации нашего ресурса. Там мы определим наш начальный URL-адрес с помощью @ApplicationPath и нашего пакета конечных точек:

@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
public ExceptionHandlingConfig() {
packages("com.foreach.jersey.exceptionhandling.rest");
}
}

2.1. Бобы

Нам понадобятся только два bean-компонента: Stock и Wallet , чтобы мы могли сохранять Stocks и покупать их. Для нашего Stock нам просто нужно свойство цены , чтобы помочь с проверками. Что еще более важно, наш класс 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. Конечные точки

Точно так же наш API будет иметь две конечные точки. Они будут определять стандартные методы для сохранения и извлечения наших компонентов:

@Path("/stocks")
public class StocksResource {
// POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
// POST and GET methods
}

Например, давайте посмотрим на наш метод GET в StocksResource :

@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. Что происходит, когда мы выбрасываем исключение?

Когда возникает необработанное исключение, мы можем раскрыть конфиденциальную информацию о внутреннем устройстве нашего приложения. Если мы попробуем этот метод GET из StocksResource с несуществующим Stock , мы получим страницу, подобную этой:

./28893504f8fd4075bd1317a345ee2647.png

На этой странице показаны сервер и версия приложения, которые могут помочь потенциальным злоумышленникам использовать уязвимости. Кроме того, есть информация об именах наших классов и номерах строк, которая также может помочь злоумышленникам. Самое главное, что большая часть этой информации бесполезна для пользователей API и производит плохое впечатление.

Чтобы помочь контролировать исключительные ответы, JAX-RS предоставляет классы ExceptionMapper и WebApplicationException . Давайте посмотрим, как они работают.

4. Пользовательские исключения с WebApplicationException

С помощью WebApplicationException мы можем создавать собственные исключения. Этот специальный тип RuntimeException позволяет нам определить статус ответа и сущность. Мы начнем с создания InvalidTradeException , которое устанавливает сообщение и статус:

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 и включить их в наш ответ . В нашем примере у нас есть свойство subject , которое мы будем использовать для переноса контекстных данных:

public class RestErrorResponse {
private Object subject;
private String message;

// getters and setters
}

Поскольку это исключение не предназначено для манипулирования, мы не будем беспокоиться о типе субъекта .

4.2. Использование всего

Чтобы увидеть, как мы можем использовать пользовательские исключения, давайте определим метод покупки акций :

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

Затем кошелек , чтобы купить его:

$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
"id": "WALLET",
"balance": 100.0
}'

{"balance": 100.0, "id": "WALLET"}

После этого мы купим акции с помощью нашего кошелька :

$ 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. Необработанные исключения с помощью ExceptionMapper

Чтобы уточнить, генерирования исключения WebApplicationException будет недостаточно, чтобы избавиться от страницы ошибки по умолчанию. Мы должны указать объект для нашего ответа , чего нельзя сказать о 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();
}
}

Например, мы просто повторно передаем информацию об исключении в наш ответ , который будет отображать именно то, что мы возвращаем. Впоследствии мы можем пойти немного дальше, проверив код состояния перед построением нашего ответа :

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. Обработка определенных исключений

Если есть конкретное исключение , которое выдается часто, мы также можем создать для него 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. Конфигурация

Чтобы активировать наши преобразователи исключений, мы должны вернуться к нашей конфигурации ресурсов Джерси и зарегистрировать их:

public ExceptionHandlingConfig() {
// packages ...
register(IllegalArgumentExceptionMapper.class);
register(ServerExceptionMapper.class);
}

Этого достаточно, чтобы избавиться от страницы ошибки по умолчанию. Затем, в зависимости от того, что выбрасывается, Джерси будет использовать один из наших преобразователей исключений при возникновении необработанного исключения. Например, при попытке получить несуществующий 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. Заключение

В этой статье мы увидели множество способов обработки исключений с помощью Джерси. Более того, почему это важно, и как это настроить. После этого мы построили простой сценарий, в котором мы могли их применить. В результате у нас теперь есть более дружественный и безопасный API.

И, как всегда, исходный код доступен на GitHub .