JAX-RS / Jersey как настроить обработку ошибок?

Я изучаю 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?

Ответов (11)

Решение

Существует несколько подходов к настройке поведения обработки ошибок с помощью 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 клиенту.

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

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

Джерси выдает исключение 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();
    }
}

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

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

Документация @QueryParam говорит

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

1) Быть примитивным типом
2) Иметь конструктор, принимающий единственный аргумент String
3) Иметь статический метод с именем valueOf или fromString, который принимает единственный аргумент String (см., Например, Integer.valueOf (String))
4) Иметь зарегистрированная реализация javax.ws.rs.ext.ParamConverterProvider JAX-RS extension SPI, которая возвращает экземпляр 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-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();
    }
}

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

Я столкнулся с той же проблемой.

Я хотел собрать все ошибки в одном месте и преобразовать их.

Ниже приведен код того, как я с этим справился.

Создайте следующий класс, который реализует 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. Путем расширения класса 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();
    }

}

Вы можете увидеть полную заявку по этой ссылке .

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

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

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Дополнительные параметры см. В JavaDoc для javax.ws.rs.core.Response.Status .

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

Это помогло мне создать 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"); }

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

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

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

@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 и т. Д., Которые вам нужно сопоставить, чтобы предоставить настраиваемые ответы.