Як налаштувати обробку помилок JAX-RS / Jersey?


216

Я вивчаю JAX-RS (він же JSR-311) за допомогою Джерсі. Я успішно створив Root Resource і розігруюсь з параметрами:

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

Це чудово працює і обробляє будь-який формат у поточному локалі, який розуміється конструктором Date (String) (наприклад, РРР / мм / dd та mm / dd / YYYY). Але якщо я надаю значення, яке є недійсним або не зрозумілим, я отримую відповідь 404.

Наприклад:

GET /hello?name=Mark&birthDate=X

404 Not Found

Як я можу налаштувати таку поведінку? Може бути інший код відповіді (ймовірно, "400 поганий запит")? Що з реєстрацією помилки? Можливо, додайте опис проблеми ("неправильний формат дати") у спеціальний заголовок, щоб допомогти вирішити неполадки? Або повернути цілу відповідь на помилку з деталями, а також код статусу 5xx?

Відповіді:


271

Існує кілька підходів, щоб налаштувати поведінку поводження з помилками з JAX-RS. Ось три найпростіші способи.

Перший підхід полягає у створенні класу винятку, який розширює WebApplicationException.

Приклад:

public class NotAuthorizedException extends WebApplicationException {
     public NotAuthorizedException(String message) {
         super(Response.status(Response.Status.UNAUTHORIZED)
             .entity(message).type(MediaType.TEXT_PLAIN).build());
     }
}

І щоб кинути цей новостворений виняток, ви просто:

@Path("accounts/{accountId}/")
    public Item getItem(@PathParam("accountId") String accountId) {
       // An unauthorized user tries to enter
       throw new NotAuthorizedException("You Don't Have Permission");
}

Зауважте, вам не потрібно оголошувати виняток у закиданні, оскільки WebApplicationException - це виняток під час виконання. Це поверне клієнту 401 відповідь.

Другий і простіший підхід - просто побудувати екземпляр WebApplicationException безпосередньо у вашому коді. Цей підхід працює до тих пір, поки вам не доведеться впроваджувати власні додатки Винятки.

Приклад:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
   // An unauthorized user tries to enter
   throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

Цей код також повертає клієнту 401.

Звичайно, це лише простий приклад. Ви можете зробити Виняток набагато складнішим, якщо це необхідно, і ви можете генерувати будь-який код коду відповіді, який вам потрібно.

Ще один підхід полягає в обгортанні існуючого винятку, можливо, ObjectNotFoundExceptionз невеликим класом обгортки, який реалізує ExceptionMapperінтерфейс, який позначається з @Providerанотацією. Це говорить про час виконання JAX-RS, що якщо завершений виняток буде піднятий, поверніть код відповіді, визначений у ExceptionMapper.


3
У вашому прикладі виклик до super () повинен бути дещо іншим: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Дякуємо за розуміння.
Джон Онстотт

65
У сценарії, згаданому у запитанні, ви не отримаєте шансу викинути виняток, оскільки Джерсі підніме виняток, оскільки він не зможе створити екземпляр об’єкта Date з вхідного значення. Чи є спосіб перехопити виключення Джерсі? Однак є один інтерфейс ExceptionMapper, який також перехоплює винятки, викинуті методом (отримати в цьому випадку).
Rejeev Divakaran

7
Як уникнути появи виключень у журналах вашого сервера, якщо 404 є дійсним випадком, а не помилкою (тобто кожного разу, коли ви запитуєте ресурс, просто щоб побачити, чи він уже існує, при вашому підході на сервері з'являється стек-трак. колоди).
Гвідо

3
Варто згадати, що Jersey 2.x визначає винятки для деяких найпоширеніших кодів помилок HTTP. Тож замість визначення власних підкласів WebApplication ви можете використовувати вбудовані такі, як BadRequestException та NotAuthorizedException. Подивіться, наприклад, підкласи javax.ws.rs.ClientErrorException. Також зверніть увагу, що ви можете надати рядки деталей конструкторам. Наприклад: киньте нову BadRequestException ("Дата початку повинна передувати кінцевій даті");
Бампфер

1
ви забули згадати ще один підхід: реалізація ExceptionMapperінтерфейсу (що є кращим підходом, ніж розширенням). Детальніше дивіться тут vvirlan.wordpress.com/2015/10/19/…
ACV

70
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

Створити над класом. Це буде обробляти 404 (NotFoundException), і тут, в методі toResponse, ви можете дати свою власну відповідь. Аналогічно є ParamException тощо, які вам потрібно буде скласти на карту, щоб забезпечити спеціальні відповіді.


Ви можете використовувати реалізацію ExceptionMapper <Exception>, а також загальні винятки
Saurabh

1
Це також обробляє WebApplicationExceptions, кинуте клієнтом JAX-RS, приховуючи походження помилки. Краще мати спеціальний виняток (не походить від WebApplicationException) або кидати WebApplications з повним реагуванням. WebApplicationExceptions, кинуті Клієнтом JAX-RS, слід обробляти безпосередньо під час виклику, інакше відповідь іншої послуги передається як відповідь вашої служби, хоча це неподільна внутрішня помилка сервера.
Маркус Кулл

38

Джерсі викидає com.sun.jersey.api.ParamException, коли не вдається скасувати параметри, тому одним рішенням є створення ExceptionMapper, який обробляє такі типи винятків:

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}

де я повинен створити цей картограф спеціально для того, щоб Джерсі зареєстрував його?
Патрісіо

1
Все, що вам потрібно зробити, - це додати анотацію @Provider, докладнішу інформацію див. Тут: stackoverflow.com/questions/15185299/…
Ян Кронкіст

27

Ви також можете написати клас багаторазового використання для змінних, позначених QueryParam

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

то використовуйте його так:

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

Хоча обробка помилок у цьому випадку тривіальна (викидання відповіді 400), використання цього класу дозволяє взагалі обробляти параметри, що може включати реєстрацію тощо.


Я намагаюся додати користувальницький обробник парам-запитів у Джерсі (мігруючи з CXF), це виглядає надзвичайно схоже на те, що я роблю, але я не знаю, як встановити / створити нового провайдера. Ваш клас вище мені цього не показує. Я використовую об’єкти JodaTime DateTime для QueryParam і не маю провайдера для їх розшифровки. Це так само просто, як підкласифікувати його, надаючи йому конструктор String і обробляючи це?
Крістіан Бонгіорно

1
Просто створіть клас на зразок DateParamвище, який org.joda.time.DateTimeзамість нього загортає java.util.Calendar. Ви використовуєте це @QueryParamне для DateTimeсебе.
Чарлі Брукінг

1
Якщо ви використовуєте Joda DateTime, трикотаж поставляється з DateTimeParam, який ви можете безпосередньо використовувати. Не потрібно писати своє. Дивіться github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Шрікант

Я збираюся додати це, тому що це супер корисно, але тільки якщо ви використовуєте Джексона з Джерсі. Джексон 2.x має JodaModuleте, що може бути зареєстровано ObjectMapper registerModulesметодом. Він може обробляти всі перетворення типу joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev

11

Одне очевидне рішення: взяти рядок, перетворити в Date самостійно. Таким чином, ви можете визначити потрібний формат, вловити винятки та повторно кинути або налаштувати помилку, що надсилається. Для розбору SimpleDateFormat повинен добре працювати.

Я впевнений, що є способи підключити обробники для типів даних, але, мабуть, трохи простого коду - все, що вам потрібно в цьому випадку.


7

Мені також подобається, що StaxMan , ймовірно, реалізує цей QueryParam як String, а потім обробить конверсію, повторно скидаючи.

Якщо поведінка, характерна для локальної мови, є бажаною та очікуваною поведінкою, ви використовуєте наступне, щоб повернути помилку 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Дивіться JavaDoc для javax.ws.rs.core.Response.Status для отримання додаткових опцій.


4

У документації @QueryParam йдеться

"Тип T анотованого параметра, поля чи властивості повинен:

1) Будьте примітивним типом
2) Майте конструктор, який приймає один аргумент String
3) Майте статичний метод з ім'ям valueOf або fromString, який приймає один аргумент String (див., Наприклад, Integer.valueOf (String))
4) зареєстрована реалізація javax.ws.rs.ext.ParamConverterProvider JAX-RS розширення SPI, що повертає javax.ws.rs.ext.ParamConverter екземпляр, здатний перетворити "з рядка" для типу.
5) Будьте списком, набором або сортуванням набору, де T задовольняє 2, 3 або 4 вище. Отримана колекція доступна лише для читання. "

Якщо ви хочете контролювати, яка відповідь надходить користувачеві, коли параметр запиту у формі String не може бути перетворений у ваш тип T, ви можете кинути WebApplicationException. Dropwizard постачається з наступними * Класами Param, які ви можете використовувати для своїх потреб.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. Дивіться https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

Якщо вам потрібен Joda DateTime, просто використовуйте Dropwizard DateTimeParam .

Якщо наведений вище список не відповідає вашим потребам, визначте свій власний, розширивши AbstractParam. Метод переоцінки розбору Якщо вам потрібен контроль над тілом відповіді на помилку, замініть метод помилки.

Хороша стаття від Coda Hale про це на http://codahale.com/what-makes-jersey-interesting-parameter-classes/

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

Конструктор дати (String arg) застарілий. Я б використовував класи дат Java 8, якщо ви перебуваєте на Java 8. В іншому випадку рекомендується час дати joda.


1

Це фактично правильна поведінка. Джерсі спробує знайти обробник для вашого вводу і спробує побудувати об'єкт із наданого вводу. У цьому випадку він спробує створити новий об'єкт Date зі значенням X, наданим конструктору. Оскільки це недійсна дата, за конвенцією Джерсі повернеться 404.

Що ви можете зробити, це переписати та вказати дату народження як String, а потім спробуйте розібратися, і якщо ви не отримаєте те, що хочете, ви можете кинути будь-яке виняток за будь-яким із механізмів відображення винятків (їх декілька ).


1

Я стикався з тим же питанням.

Я хотів зловити всі помилки на центральному місці та перетворити їх.

Далі йде код того, як я впорався.

Створіть наступний клас, який реалізує ExceptionMapperта додає @Providerпримітку до цього класу. Це впорається з усіма винятками.

toResponseМетод переопределення та повернення об’єкта Response, заповненого налаштованими даними.

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}

1

Підхід 1: Розширення класу WebApplicationException

Створіть новий виняток, розширивши WebApplicationException

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

Тепер киньте "RestException", коли потрібно.

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

Повну заявку ви можете побачити за цим посиланням .

Підхід 2: впровадити ExceptionMapper

Слідом за картографом обробляється виняток типу "DataNotFoundException"

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

Повну заявку ви можете побачити за цим посиланням .


0

Так само як розширення до відповіді @Steven Lavine у ​​випадку, якщо ви хочете відкрити вікно входу в браузер. Мені було важко правильно повернути Відповідь ( автентифікація HTTP MDN) ) з фільтра на випадок, якщо користувач ще не пройшов автентифікацію

Це допомогло мені створити Відповідь, щоб змусити ввійти в браузер, відзначити додаткові зміни заголовків. Це встановить код статусу на 401 і встановить заголовок, який змушує браузер відкривати діалогове вікно користувача / пароля.

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.