JAX-RS / Jersey como personalizar o tratamento de erros?

216

Estou aprendendo JAX-RS (também conhecido como JSR-311) usando Jersey. Criei com êxito um Recurso Raiz e estou brincando com os parâmetros:

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

Isso funciona muito bem e manipula qualquer formato no código de idioma atual que seja entendido pelo construtor Date (String) (como AAAA / mm / dd e mm / dd / AAAA). Mas se eu fornecer um valor inválido ou não compreendido, recebo uma resposta 404.

Por exemplo:

GET /hello?name=Mark&birthDate=X

404 Not Found

Como posso personalizar esse comportamento? Talvez um código de resposta diferente (provavelmente "400 Request Bad")? Que tal registrar um erro? Talvez adicione uma descrição do problema ("formato de data inválido") em um cabeçalho personalizado para ajudar na solução de problemas? Ou retornar uma resposta de erro inteira com detalhes, junto com um código de status 5xx?

Mark Renouf
fonte

Respostas:

271

Existem várias abordagens para customizar o comportamento de manipulação de erros com o JAX-RS. Aqui estão três das maneiras mais fáceis.

A primeira abordagem é criar uma classe Exception que estenda o WebApplicationException.

Exemplo:

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

E para lançar essa exceção recém-criada, você simplesmente:

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

Observe que você não precisa declarar a exceção em uma cláusula throws, porque WebApplicationException é uma exceção de tempo de execução. Isso retornará uma resposta 401 para o cliente.

A segunda e mais fácil abordagem é simplesmente construir uma instância do WebApplicationException diretamente no seu código. Essa abordagem funciona desde que você não precise implementar suas próprias exceções de aplicativo.

Exemplo:

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

Esse código também retorna um 401 para o cliente.

Obviamente, este é apenas um exemplo simples. Você pode tornar a exceção muito mais complexa, se necessário, e pode gerar o código de resposta http sempre que precisar.

Uma outra abordagem é agrupar uma exceção existente, talvez uma ObjectNotFoundExceptionclasse de wrapper pequena que implemente a ExceptionMapperinterface anotada com uma @Provideranotação. Isso informa ao tempo de execução do JAX-RS que, se a Exceção agrupada for gerada, retorne o código de resposta definido em ExceptionMapper.

Steven Levine
fonte
3
No seu exemplo, a chamada para super () deve ser um pouco diferente: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Obrigado pela compreensão embora.
precisa saber é o seguinte
65
No cenário mencionado na pergunta, você não terá a chance de lançar uma exceção, pois Jersey criará uma exceção, pois não poderá criar a instância do objeto Date a partir do valor de entrada. Existe uma maneira de interceptar a exceção de Jersey? Há uma interface ExceptionMapper, no entanto, que também intercepta as exceções geradas pelo método (neste caso).
Rejeev Divakaran
7
Como você evita que a exceção apareça nos logs do servidor, se o 404 é um caso válido e não um erro (ou seja, toda vez que você consulta um recurso, apenas para ver se ele já existe, com a sua abordagem, um stacktrace aparece no servidor Histórico).
Guido
3
Vale ressaltar que o Jersey 2.x define exceções para alguns dos códigos de erro HTTP mais comuns. Portanto, em vez de definir suas próprias subclasses de WebApplication, você pode usar as internas, como BadRequestException e NotAuthorizedException. Veja as subclasses de javax.ws.rs.ClientErrorException, por exemplo. Observe também que você pode fornecer uma sequência de detalhes para os construtores. Por exemplo: throw new BadRequestException ("A data de início deve preceder a data de término");
Bampfer 31/07/2015
1
você esqueceu de mencionar outra abordagem: implementar a ExceptionMapperinterface (que é uma abordagem melhor do que a extensão). Veja mais aqui 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();
}
}

Crie a classe acima. Isso manipulará 404 (NotFoundException) e aqui no método toResponse você poderá fornecer sua resposta personalizada. Da mesma forma, existem ParamException etc. que você precisaria mapear para fornecer respostas personalizadas.

Arnav
fonte
Você pode usar implementos ExceptionMapper <exceção>, bem como para exceções genéricas
Saurabh
1
Isso também lidaria com o WebApplicationExceptions lançado pelo cliente JAX-RS, ocultando a origem do erro. É melhor ter uma exceção personalizada (não derivada de WebApplicationException) ou lançar WebApplications com resposta completa. As WebApplicationExceptions lançadas pelo cliente JAX-RS devem ser tratadas diretamente na chamada, caso contrário, a resposta de outro serviço é transmitida como resposta do seu serviço, embora seja um erro interno não tratado do servidor.
Markus Kull
38

Jersey lança um com.sun.jersey.api.ParamException quando falha ao desmarcar os parâmetros, portanto, uma solução é criar um ExceptionMapper que lida com esses tipos de exceções:

@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
fonte
onde devo criar esse mapeador especificamente para Jersey registrá-lo?
Patricio
1
Tudo que você tem a fazer é adicionar a anotação @Provider, veja aqui para mais detalhes: stackoverflow.com/questions/15185299/...
Jan Kronquist
27

Você também pode escrever uma classe reutilizável para variáveis ​​anotadas por 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());
  }
}

use-o assim:

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

Embora o tratamento de erros seja trivial nesse caso (lançando uma resposta de 400), o uso dessa classe permite fatorar o tratamento de parâmetros em geral, que pode incluir o registro etc.

Charlie Brooking
fonte
Estou tentando adicionar um manipulador de parâmetros de consulta personalizado em Jersey (migrando do CXF) que parece muito semelhante ao que estou fazendo, mas não sei como instalar / criar um novo provedor. Sua classe acima não me mostra isso. Estou usando objetos JodaTime DateTime para QueryParam e não tenho um provedor para decodificá-los. É tão fácil quanto subclassificá-lo, fornecendo a ele um construtor String e lidando com isso?
Christian Bongiorno
1
Basta criar uma classe como a DateParamacima, que envolve um em org.joda.time.DateTimevez de java.util.Calendar. Você usa isso @QueryParammais do que DateTimeele próprio.
Charlie Brooking
1
Se você estiver usando o Joda DateTime, a camisa vem com o DateTimeParam para você usar diretamente. Não há necessidade de escrever o seu próprio. Veja github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Vou adicionar isso porque é super útil, mas apenas se você estiver usando Jackson com Jersey. O Jackson 2.x tem um JodaModuleque pode ser registrado com o ObjectMapper registerModulesmétodo Ele pode lidar com todas as conversões do tipo joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

Uma solução óbvia: pegue uma String, converta você mesmo para o Date. Dessa forma, você pode definir o formato desejado, capturar exceções e repetir ou personalizar o erro que está sendo enviado. Para análise, SimpleDateFormat deve funcionar bem.

Estou certo de que existem maneiras de conectar manipuladores para tipos de dados também, mas talvez um pouco de código simples seja tudo o que você precisa nesse caso.

StaxMan
fonte
7

Eu também gosto de StaxMan provavelmente implementaria esse QueryParam como uma String e depois lidaria com a conversão, reconvertendo conforme necessário.

Se o comportamento específico da localidade for o comportamento desejado e esperado, você usaria o seguinte para retornar o erro 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Consulte o JavaDoc para javax.ws.rs.core.Response.Status para obter mais opções.

dshaw
fonte
4

A documentação do @QueryParam diz

"O tipo T do parâmetro, campo ou propriedade anotado deve:

1) Seja um tipo primitivo
2) Tenha um construtor que aceite um único argumento String
3) Tenha um método estático chamado valueOf ou fromString que aceite um único argumento String (consulte, por exemplo, Integer.valueOf (String))
4) implementação registrada da javax.ws.rs.ext.ParamConverterProvider SPI de extensão JAX-RS que retorna uma instância javax.ws.rs.ext.ParamConverter capaz de uma conversão "da cadeia de caracteres" para o tipo.
5) Seja List, Set ou SortedSet, onde T satisfaz 2, 3 ou 4 acima. A coleção resultante é somente leitura. "

Se você deseja controlar qual resposta vai para o usuário quando o parâmetro de consulta no formulário String não pode ser convertido para o seu tipo T, você pode lançar WebApplicationException. O Dropwizard vem com as seguintes classes * Param que você pode usar para suas necessidades.

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

Se você precisar do Joda DateTime, use o Dropwizard DateTimeParam .

Se a lista acima não atender às suas necessidades, defina sua própria extensão estendendo AbstractParam. Substituir método de análise. Se você precisar de controle sobre o corpo da resposta a erros, substitua o método de erro.

Bom artigo de Coda Hale sobre isso está em 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();
    }
}

O construtor Date (String arg) foi descontinuado. Eu usaria as classes de data do Java 8 se você estivesse no Java 8. Caso contrário, o joda date time é recomendado.

Srikanth
fonte
1

Este é o comportamento correto, na verdade. Jersey tentará encontrar um manipulador para sua entrada e tentará construir um objeto a partir da entrada fornecida. Nesse caso, ele tentará criar um novo objeto Date com o valor X fornecido ao construtor. Como essa é uma data inválida, por convenção, Jersey retornará 404.

O que você pode fazer é reescrever e colocar a data de nascimento como uma String e, em seguida, tentar analisar e, se você não conseguir o que deseja, poderá liberar qualquer exceção que desejar por qualquer mecanismo de mapeamento de exceção (existem vários )

ACV
fonte
1

Eu estava enfrentando o mesmo problema.

Eu queria pegar todos os erros em um local central e transformá-los.

A seguir, está o código de como eu lidei com isso.

Crie a seguinte classe que implementa ExceptionMappere adicione @Provideranotações nessa classe. Isso irá lidar com todas as exceções.

Substitua o toResponsemétodo e retorne o objeto Response preenchido com dados customizados.

//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
fonte
1

Abordagem 1: estendendo a classe WebApplicationException

Crie uma nova exceção estendendo 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());
         }
}

Agora lance 'RestException' sempre que necessário.

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

Você pode ver a inscrição completa neste link .

Abordagem 2: Implementar ExceptionMapper

O mapeador a seguir lida com exceção do tipo '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();
    }

}

Você pode ver a inscrição completa neste link .

Hari Krishna
fonte
0

Apenas como uma extensão para a resposta de Steven Lavine, caso você queira abrir a janela de login do navegador. Achei difícil retornar corretamente a resposta ( autenticação HTTP MDN ) do filtro, caso o usuário ainda não estivesse autenticado

Isso me ajudou a criar a resposta para forçar o login do navegador, observe as modificações adicionais dos cabeçalhos. Isso definirá o código de status como 401 e definirá o cabeçalho que faz com que o navegador abra a caixa de diálogo nome de usuário / senha.

// 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
fonte