JAX-RS / Джерси, как настроить обработку ошибок?

216

Я изучаю JAX-RS (он же JSR-311), используя Джерси. Я успешно создал корневой ресурс и играю с параметрами:

@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) (например, YYYY / mm / dd и mm / dd / YYYY). Но если я предоставлю недопустимое или непонятное значение, я получу ответ 404.

Например:

GET /hello?name=Mark&birthDate=X

404 Not Found

Как я могу настроить это поведение? Может быть, другой код ответа (вероятно, «400 Bad Request»)? Как насчет регистрации ошибки? Может быть, добавить описание проблемы («неверный формат даты») в пользовательский заголовок, чтобы помочь в устранении неполадок? Или вернуть полный ответ об ошибке с деталями вместе с кодом статуса 5xx?

Марк Ренуф
источник

Ответы:

271

Существует несколько подходов для настройки поведения обработки ошибок с помощью JAX-RS. Вот три из простых способов.

Первый подход заключается в создании класса Exception, который расширяет 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");
}

Обратите внимание, вам не нужно объявлять исключение в предложении throws, потому что 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 клиенту.

Конечно, это всего лишь простой пример. Вы можете сделать Исключение намного более сложным, если это необходимо, и вы можете сгенерировать любой код ответа http, который вам необходим.

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

Стивен Левин
источник
3
В вашем примере вызов super () должен немного отличаться: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Спасибо за понимание, хотя.
Джон Онстотт
65
В сценарии, упомянутом в вопросе, у вас не будет возможности вызвать исключение, поскольку Джерси вызовет исключение, так как он не сможет создать экземпляр объекта Date из входного значения. Есть ли способ перехватить исключение Джерси? Существует один интерфейс ExceptionMapper, однако он также перехватывает исключения, сгенерированные методом (в этом случае get).
Реджеев Дивакаран
7
Как избежать появления исключения в журналах сервера, если 404 является допустимым регистром, а не ошибкой (т. Е. Каждый раз, когда вы запрашиваете ресурс, просто чтобы увидеть, существует ли он уже, при вашем подходе на сервере появляется трассировка стека) журналы).
Гвидо
3
Стоит отметить, что Jersey 2.x определяет исключения для некоторых из наиболее распространенных кодов ошибок HTTP. Поэтому вместо того, чтобы определять свои собственные подклассы WebApplication, вы можете использовать встроенные, такие как BadRequestException и NotAuthorizedException. Посмотрите на подклассы javax.ws.rs.ClientErrorException, например. Также обратите внимание, что вы можете предоставить конструктору строку подробностей. Например: выбросить новое BadRequestException («Дата начала должна предшествовать дате окончания»);
Bampfer
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 и т. Д., Которые вам необходимо сопоставить, чтобы предоставить настраиваемые ответы.

Arnav
источник
Вы также можете использовать инструмент ExceptionMapper <Exception> для общих исключений
Saurabh
1
Это будет обрабатывать исключения WebApplicationException, выдаваемые также клиентом JAX-RS, скрывая источник ошибки. Лучше иметь пользовательское исключение (не производное от WebApplicationException) или генерировать веб-приложения с полным ответом. 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.х имеетJodaModuleObjectMapper registerModules метод, который можно зарегистрировать методом. Он может обрабатывать все преобразования типа Joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

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

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

StaxMan
источник
7

Мне тоже нравится StaxMan , вероятно, реализовал бы этот QueryParam в виде строки, а затем обработал бы преобразование, перебрасывая при необходимости.

Если специфичным для локали является желаемое и ожидаемое поведение, вы должны использовать следующее для возврата ошибки 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Посмотрите JavaDoc для javax.ws.rs.core.Response.Status для большего количества вариантов.

dshaw
источник
4

Документация @QueryParam гласит

«Тип T аннотированного параметра, поля или свойства должен:

1) быть примитивным типом
2) иметь конструктор, который принимает один аргумент String
3) иметь статический метод с именем valueOf или fromString, который принимает один аргумент String (см., Например, Integer.valueOf (String))
4) иметь зарегистрированная реализация javax.ws.rs.ext.ParamConverterProvider SPI расширения JAX-RS, которая возвращает экземпляр javax.ws.rs.ext.ParamConverter, способный к преобразованию «из строки» для типа.
5) Be List, Set или SortedSet, где T удовлетворяет 2, 3 или 4 выше. Полученная коллекция доступна только для чтения. "

Если вы хотите контролировать, какой ответ отправляется пользователю, когда параметр запроса в строковой форме не может быть преобразован в тип 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-интереing-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();
    }
}

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

Srikanth
источник
1

Это правильное поведение на самом деле. Джерси попытается найти обработчик для вашего ввода и попытается построить объект из предоставленного ввода. В этом случае он попытается создать новый объект Date со значением X, предоставленным конструктору. Поскольку это недопустимая дата, по соглашению Джерси вернет 404.

Что вы можете сделать, это переписать и поставить дату рождения в виде строки, затем попытаться проанализировать, и если вы не получите то, что вы хотите, вы можете свободно генерировать любое исключение, которое вы хотите, с помощью любого из механизмов отображения исключений (есть несколько ).

ACV
источник
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
}
suraj.tripathi
источник
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, если вы хотите открыть окно входа в браузер. Мне было трудно правильно вернуть Response ( MDN HTTP Authentication ) из фильтра, если пользователь еще не прошел аутентификацию

Это помогло мне построить Response для принудительного входа в браузер, обратите внимание на дополнительную модификацию заголовков. Это установит код состояния на 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"); }
Omnibyte
источник