JAX-RS / Jersey jak dostosować obsługę błędów?

216

Uczę się JAX-RS (alias JSR-311) przy użyciu Jersey. Z powodzeniem utworzyłem Zasób root i bawię się parametrami:

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

Działa to świetnie i obsługuje dowolny format w bieżących ustawieniach regionalnych rozumiany przez konstruktor Date (String) (np. RRRR / mm / dd i mm / dd / RRRR). Ale jeśli podam wartość, która jest niepoprawna lub niezrozumiała, otrzymuję odpowiedź 404.

Na przykład:

GET /hello?name=Mark&birthDate=X

404 Not Found

Jak mogę dostosować to zachowanie? Może inny kod odpowiedzi (prawdopodobnie „400 złych żądań”)? Co z logowaniem błędu? Może dodać opis problemu („zły format daty”) w niestandardowym nagłówku, aby ułatwić rozwiązywanie problemów? Lub zwróć całą odpowiedź na błąd wraz ze szczegółami, wraz z kodem statusu 5xx?

Mark Renouf
źródło

Odpowiedzi:

271

Istnieje kilka podejść do dostosowania zachowania w zakresie obsługi błędów w JAX-RS. Oto trzy najprostsze sposoby.

Pierwsze podejście polega na utworzeniu klasy wyjątku, która rozszerza wyjątek WebApplicationException.

Przykład:

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

Aby rzucić ten nowo utworzony wyjątek, po prostu:

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

Zauważ, że nie musisz deklarować wyjątku w klauzuli throws, ponieważ WebApplicationException to wyjątek czasu wykonywania. To zwróci klientowi odpowiedź 401.

Drugim i łatwiejszym podejściem jest po prostu skonstruowanie wystąpienia WebApplicationExceptionbezpośrednio w kodzie. Takie podejście działa, o ile nie musisz implementować własnych wyjątków aplikacji.

Przykład:

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

Ten kod również zwraca klientowi wartość 401.

Oczywiście to tylko prosty przykład. Możesz uczynić wyjątek znacznie bardziej złożonym, jeśli to konieczne, i możesz wygenerować kod HTTP, którego potrzebujesz.

Innym podejściem jest zawinięcie istniejącego wyjątku, być może ObjectNotFoundExceptionz małą klasą opakowania, która implementuje ExceptionMapperinterfejs opatrzony @Provideradnotacją. Mówi to środowisku wykonawczemu JAX-RS, że jeśli zgłoszony wyjątek zostanie zgłoszony, zwróć kod odpowiedzi zdefiniowany w pliku ExceptionMapper.

Steven Levine
źródło
3
W twoim przykładzie wywołanie funkcji super () powinno się nieco różnić: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Dzięki za wgląd.
Jon Onstott
65
W scenariuszu wymienionym w pytaniu nie będziesz mieć szansy na zgłoszenie wyjątku, ponieważ Jersey zgłosi wyjątek, ponieważ nie będzie w stanie utworzyć wystąpienia obiektu Date na podstawie wartości wejściowej. Czy istnieje sposób na przechwycenie wyjątku Jersey? Istnieje jeden interfejs ExceptionMapper, który jednak przechwytuje wyjątki zgłaszane przez metodę (get w tym przypadku).
Rejeev Divakaran
7
Jak uniknąć wyjątku, który pojawia się w logach serwera, jeśli 404 jest poprawną sprawą, a nie błędem (tj. Za każdym razem, gdy pytasz o zasób, aby zobaczyć, czy już istnieje, po podejściu na serwerze pojawia się ślad stosu logi).
Guido
3
Warto wspomnieć, że Jersey 2.x definiuje wyjątki dla niektórych z najczęstszych kodów błędów HTTP. Dlatego zamiast definiować własne podklasy aplikacji sieciowej, możesz użyć wbudowanych, takich jak BadRequestException i NotAuthorizedException. Spójrz na przykład na podklasy javax.ws.rs.ClientErrorException. Pamiętaj również, że możesz podać ciąg szczegółów do konstruktorów. Na przykład: wyrzuć nowy wyjątek BadRequestException („Data początkowa musi poprzedzać datę końcową”);
Bampfer
1
zapomniałeś wspomnieć o jeszcze innym podejściu: implementacji ExceptionMapperinterfejsu (co jest lepszym podejściem niż rozszerzanie). Zobacz więcej tutaj 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();
}
}

Stwórz ponad klasę. To obsłuży 404 (NotFoundException) i tutaj w metodzie toResponse możesz podać swoją niestandardową odpowiedź. Podobnie istnieją ParamException itp., Które należy zmapować, aby uzyskać dostosowane odpowiedzi.

Arnav
źródło
Możesz używać implementacji ExceptionMapper <Exception> również w przypadku ogólnych wyjątków
Saurabh
1
To obsługiwałoby wyjątki WebApplicationException zgłaszane przez klienta JAX-RS, ukrywając źródło błędu. Lepiej mieć niestandardowy wyjątek (niepochodzący z WebApplicationException) lub rzucić WebApplications z pełną odpowiedzią. WebApplicationExceptions zgłaszane przez klienta JAX-RS powinny być obsługiwane bezpośrednio podczas połączenia, w przeciwnym razie odpowiedź innej usługi jest przekazywana jako odpowiedź Twojej usługi, chociaż jest to nieobsługiwany wewnętrzny błąd serwera.
Markus Kull,
38

Jersey zgłasza wyjątek com.sun.jersey.api.ParamException, gdy nie zdoła usunąć parametrów, więc jednym z rozwiązań jest utworzenie wyjątku, który obsługuje te typy wyjątków:

@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();
    }
}
Jan Kronquist
źródło
gdzie mam utworzyć tego mapera specjalnie dla Jersey, aby go zarejestrować?
Patricio
1
Wystarczy dodać adnotację @Provider, więcej informacji można znaleźć tutaj: stackoverflow.com/questions/15185299/…
Jan Kronquist
27

Możesz także napisać klasę wielokrotnego użytku dla zmiennych z komentarzami 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());
  }
}

użyj tego w następujący sposób:

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

Mimo że obsługa błędów jest w tym przypadku trywialna (generowanie odpowiedzi 400), użycie tej klasy pozwala ogólnie oddzielić obsługę parametrów, która może obejmować rejestrowanie itp.

Charlie Brooking
źródło
Próbuję dodać niestandardowy moduł obsługi parametrów zapytania w Jersey (migracja z CXF), który wygląda niezwykle podobnie do tego, co robię, ale nie wiem, jak zainstalować / utworzyć nowego dostawcę. Twoja wyższa klasa mi tego nie pokazuje. Korzystam z obiektów JodaTime DateTime dla QueryParam i nie mam dostawcy, który mógłby je zdekodować. Czy jest to tak proste, jak podklasowanie, nadawanie konstruktorowi String i obsługiwanie go?
Christian Bongiorno,
1
Po prostu stwórz klasę podobną do DateParampowyższej, która otacza org.joda.time.DateTimezamiast java.util.Calendar. Używasz tego @QueryParamraczej niż DateTimesamego siebie.
Charlie Brooking,
1
Jeśli używasz Joda DateTime, koszulka jest dostarczana z DateTimeParam do bezpośredniego użycia. Nie musisz pisać własnego. Zobacz github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Dodam to, ponieważ jest to bardzo przydatne, ale tylko jeśli używasz Jacksona z Jersey. Jackson 2.x ma plik, JodaModulektóry można zarejestrować za pomocą tej ObjectMapper registerModulesmetody. Może obsługiwać wszystkie konwersje typu Joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev,
11

Jedno oczywiste rozwiązanie: weź ciąg, przekonwertuj na datę sam. W ten sposób możesz zdefiniować żądany format, wychwytywać wyjątki i ponownie przesłać lub dostosować wysyłany błąd. Do parsowania SimpleDateFormat powinien działać poprawnie.

Jestem pewien, że istnieją sposoby na przechwycenie procedur obsługi typów danych, ale być może w tym przypadku wystarczy trochę prostego kodu.

StaxMan
źródło
7

Ja też lubię, że StaxMan prawdopodobnie zaimplementuje ten QueryParam jako String, a następnie obsłuży konwersję, w razie potrzeby ponownie przesyłając.

Jeśli zachowanie specyficzne dla ustawień narodowych jest pożądanym i oczekiwanym zachowaniem, skorzystaj z następujących opcji, aby zwrócić błąd 400 BŁĘDNE ŻĄDANIE:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Więcej informacji znajdziesz w JavaDoc dla javax.ws.rs.core.Response.Status .

dshaw
źródło
4

Dokumentacja @QueryParam mówi

„Typ T opisanego parametru, pola lub właściwości musi:

1) Bądź prymitywnym typem
2)
Posiadaj konstruktor, który akceptuje pojedynczy argument String 3) Posiadaj metodę statyczną o nazwie valueOf lub fromString, która akceptuje pojedynczy argument String (patrz na przykład Integer.valueOf (String))
4) zarejestrowana implementacja rozszerzenia SPI javax.ws.rs.ext.ParamConverterProvider JAX-RS, która zwraca instancję javax.ws.rs.ext.ParamConverter zdolną do konwersji typu „z ciągu” dla tego typu.
5) Bądź na liście, ustaw lub sortuj, gdzie T spełnia 2, 3 lub 4 powyżej. Powstała kolekcja jest tylko do odczytu. „

Jeśli chcesz kontrolować, jaka odpowiedź trafia do użytkownika, gdy parametru zapytania w postaci ciągu nie można przekonwertować na typ T, możesz zgłosić wyjątek WebApplicationException. Dropwizard zawiera następujące * klasy Param, których możesz użyć do swoich potrzeb.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. Zobacz https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

Jeśli potrzebujesz Joda DateTime, po prostu użyj Dropwizard DateTimeParam .

Jeśli powyższa lista nie odpowiada Twoim potrzebom, zdefiniuj własną, rozszerzając AbstractParam. Zastąp metodę analizy. Jeśli potrzebujesz kontroli nad treścią odpowiedzi na błąd, zastąp metodę błędu.

Dobry artykuł Cody Hale na ten temat znajduje się na stronie 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();
    }
}

Konstruktor Date (String arg) jest przestarzały. Używałbym klas daty Java 8, jeśli korzystasz z Java 8. W przeciwnym razie zalecana jest data i godzina Joda.

Srikanth
źródło
1

To jest właściwie właściwe zachowanie. Jersey spróbuje znaleźć moduł obsługi dla danych wejściowych i spróbuje skonstruować obiekt na podstawie danych wejściowych. W takim przypadku spróbuje utworzyć nowy obiekt Date o wartości X podanej konstruktorowi. Ponieważ jest to nieprawidłowa data, zgodnie z umową Jersey zwróci 404.

Możesz przepisać i ustawić datę urodzenia jako ciąg znaków, a następnie spróbować przeanalizować, a jeśli nie dostaniesz tego, czego chcesz, możesz zgłosić dowolny wyjątek za pomocą dowolnego mechanizmu odwzorowywania wyjątków (istnieje kilka ).

ACV
źródło
1

Miałem do czynienia z tym samym problemem.

Chciałem złapać wszystkie błędy w centralnym miejscu i je przekształcić.

Poniżej znajduje się kod tego, jak sobie z tym poradziłem.

Utwórz następującą klasę, która implementuje ExceptionMapperi dodaje@Provider adnotację do tej klasy. Pozwoli to obsłużyć wszystkie wyjątki.

Przesłoń toResponsemetodę i zwróć obiekt odpowiedzi wypełniony dostosowanymi danymi.

//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
źródło
1

Podejście 1: przez rozszerzenie klasy WebApplicationException

Utwórz nowy wyjątek, rozszerzając wyjątek 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());
         }
}

Teraz rzucaj „RestException”, gdy jest to wymagane.

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

Możesz zobaczyć pełną aplikację pod tym linkiem .

Podejście 2: Implementuj ExceptionMapper

Poniższy program odwzorowujący obsługuje wyjątek typu „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();
    }

}

Możesz zobaczyć pełną aplikację pod tym linkiem .

Hari Krishna
źródło
0

Tylko jako rozszerzenie odpowiedzi @Steven Lavine na wypadek, gdybyś chciał otworzyć okno logowania przeglądarki. Trudno mi było poprawnie zwrócić odpowiedź ( uwierzytelnianie MDN HTTP ) z filtra, na wypadek gdyby użytkownik nie został jeszcze uwierzytelniony

Pomogło mi to zbudować Odpowiedź, aby wymusić logowanie do przeglądarki, zwróć uwagę na dodatkową modyfikację nagłówków. Spowoduje to ustawienie kodu stanu na 401 i ustawienie nagłówka, który spowoduje, że przeglądarka otworzy okno dialogowe nazwy użytkownika / hasła.

// 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
źródło