JAX-RS / Jersey如何自定义错误处理?


216

我正在使用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状态代码?

Answers:


271

有几种方法可以使用JAX-RS定制错误处理行为。这是三种更简单的方法。

第一种方法是创建一个扩展WebApplicationException的Exception类。

例:

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响应代码。

另一种方法是包装现有的Exception,也许是ObjectNotFoundException使用小型包装器类来包装,该类实现ExceptionMapper带有@Provider注释的接口。这告诉JAX-RS运行时,如果引发了包装的Exception,请返回中定义的响应代码ExceptionMapper


3
在您的示例中,对super()的调用应略有不同:super(Response.status(Status.UNAUTHORIZED)。entity(message).type(“ text / plain”)。build()); 感谢您的见解。
乔恩·昂斯托特

65
在问题中提到的情况下,您将没有机会引发异常,因为Jersey将引发异常,因为它无法根据输入值创建Date对象的实例。有没有办法拦截球衣异常?有一个ExceptionMapper接口,但是它也拦截方法抛出的异常(在本例中为get)。
Rejeev Divakaran

7
如果404是有效的情况而不是错误,如何避免出现在服务器日志中的异常(即,每次查询资源时,只要查看它是否已存在,就可以用一种方法在服务器中显示一个堆栈跟踪记录日志)。
Guido

3
值得一提的是,Jersey 2.x为一些最常见的HTTP错误代码定义了异常。因此,您可以使用内置的类(例如BadRequestException和NotAuthorizedException)来代替定义自己的WebApplication子类。例如,查看javax.ws.rs.ClientErrorException的子类。还要注意,您可以向构造函数提供一个细节字符串。例如:throw new BadRequestException(“开始日期必须早于结束日期”);
2015年

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 <异常>,以及用于一般例外
SAURABH

1
这也将处理JAX-RS Client抛出的WebApplicationExceptions,隐藏错误源。最好有一个自定义的Exception(不是从WebApplicationException派生的)或抛出具有完整Response的WebApplications。JAX-RS客户端抛出的WebApplicationExceptions应该在调用时直接处理,否则,另一个服务的响应将作为您服务的响应传递,尽管这是未处理的内部服务器错误。
马库斯·库尔

38

当无法解组参数时,Jersey抛出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();
    }
}

我应该在哪里为泽西岛专门创建此映射器以进行注册?
Patricio

1
您所需要做的就是添加@Provider批注,有关更多详细信息,请参见此处:stackoverflow.com/questions/15185299/…–
Jan

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响应),但是使用此类可以使您排除一般的参数处理,其中可能包括日志记录等。


我正在尝试在Jersey(从CXF迁移)中添加自定义查询参数处理程序,这看起来与我正在执行的操作非常相似,但是我不知道如何安装/创建新的提供程序。您的上等课程没有告诉我这一点。我正在使用QueryParam的JodaTime DateTime对象,但是没有提供程序来解码它们。它像子类化一样简单,给它一个String构造函数并进行处理吗?
克里斯蒂安·邦吉诺

1
只需创建一个类似DateParam上述的类(org.joda.time.DateTime而不是)即可java.util.Calendar。您使用它@QueryParam而不是DateTime它本身。
查理·布鲁金

1
如果您使用的是Joda DateTime,则球衣附带DateTimeParam,供您直接使用。无需自己编写。参见github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…–
Srikanth

我将添加它,因为它超级有用,但仅当您将Jackson与Jersey一起使用时才如此。Jackson 2.x具有JodaModule可以使用该ObjectMapper registerModules方法注册的。它可以处理所有的joda类型转换。com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev

11

一个明显的解决方案:接受一个String,自己转换为Date。这样,您可以定义所需的格式,捕获异常并重新抛出或自定义发送的错误。对于解析,SimpleDateFormat应该可以正常工作。

我敢肯定,也有一些方法可以挂钩数据类型的处理程序,但是在这种情况下,可能只需要一点简单的代码即可。


7

我也很喜欢StaxMan可能将QueryParam实现为String,然后处理转换,并根据需要重新抛出。

如果特定于语言环境的行为是期望的行为,则可以使用以下命令返回400错误请求错误:

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。结果集合是只读的。”

如果要控制在无法将字符串形式的查询参数转换为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日期时间。


1

实际上,这是正确的行为。Jersey将尝试为您的输入查找一个处理程序,并尝试从提供的输入中构造一个对象。在这种情况下,它将尝试使用提供给构造函数的X值创建一个新的Date对象。由于这是一个无效的日期,按惯例,泽西岛将返回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答案的扩展名,如果您想打开浏览器登录窗口。我发现很难从过滤器正确返回响应(MDN HTTP身份验证),以防用户尚未通过身份验证

这有助于我构建响应以强制浏览器登录,请注意标题的其他修改。这会将状态代码设置为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"); }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.