JAX-RS / Jersey làm thế nào để tùy chỉnh xử lý lỗi?


216

Tôi đang học JAX-RS (hay còn gọi là JSR-311) bằng cách sử dụng Jersey. Tôi đã thành công tạo ra một Tài nguyên gốc và đang chơi xung quanh với các tham số:

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

Điều này hoạt động rất tốt và xử lý bất kỳ định dạng nào trong ngôn ngữ hiện tại được hiểu bởi hàm tạo Ngày (Chuỗi) (như YYYY / mm / dd và mm / dd / YYYY). Nhưng nếu tôi cung cấp một giá trị không hợp lệ hoặc không hiểu, tôi nhận được phản hồi 404.

Ví dụ:

GET /hello?name=Mark&birthDate=X

404 Not Found

Làm thế nào tôi có thể tùy chỉnh hành vi này? Có lẽ một mã phản hồi khác nhau (có thể là "400 Yêu cầu Không hợp lệ")? Điều gì về việc đăng nhập một lỗi? Có thể thêm mô tả về sự cố ("định dạng ngày xấu") trong tiêu đề tùy chỉnh để hỗ trợ khắc phục sự cố? Hoặc trả lại toàn bộ phản hồi Lỗi với các chi tiết, cùng với mã trạng thái 5xx?

Câu trả lời:


271

Có một số cách tiếp cận để tùy chỉnh hành vi xử lý lỗi với JAX-RS. Đây là ba trong số những cách dễ dàng hơn.

Cách tiếp cận đầu tiên là tạo một lớp Exception mở rộng WebApplicationException.

Thí dụ:

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

Và để ném Ngoại lệ mới được tạo này, bạn chỉ cần:

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

Lưu ý, bạn không cần phải khai báo ngoại lệ trong mệnh đề ném vì WebApplicationException là Ngoại lệ thời gian chạy. Điều này sẽ trả về một phản hồi 401 cho khách hàng.

Cách tiếp cận thứ hai và dễ dàng hơn là chỉ cần xây dựng một thể hiện WebApplicationExceptiontrực tiếp trong mã của bạn. Cách tiếp cận này hoạt động miễn là bạn không phải thực hiện Ngoại lệ ứng dụng của riêng mình.

Thí dụ:

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

Mã này cũng trả về một 401 cho khách hàng.

Tất nhiên, đây chỉ là một ví dụ đơn giản. Bạn có thể làm cho Ngoại lệ phức tạp hơn nhiều nếu cần thiết và bạn có thể tạo mã phản hồi http mà bạn cần.

Một cách tiếp cận khác là bọc một Ngoại lệ hiện có, có lẽ là ObjectNotFoundExceptionvới một lớp bao bọc nhỏ thực hiện ExceptionMappergiao diện được chú thích bằng một @Providerchú thích. Điều này cho biết thời gian chạy JAX-RS, nếu Ngoại lệ được bao bọc được nâng lên, trả về mã phản hồi được xác định trong ExceptionMapper.


3
Trong ví dụ của bạn, lệnh gọi super () nên khác một chút: super (Feedback.status (Status.UNAUTHORIZED). Thực thể (tin nhắn) .type ("text / plain"). Build ()); Cảm ơn cho cái nhìn sâu sắc mặc dù.
Jon Onstott

65
Trong kịch bản được đề cập trong câu hỏi, bạn sẽ không có cơ hội ném ngoại lệ, vì Jersey sẽ đưa ra ngoại lệ vì nó sẽ không thể tạo đối tượng Date từ giá trị đầu vào. Có cách nào để chặn ngoại lệ Jersey không? Có một giao diện ExceptionMapper, tuy nhiên, nó cũng chặn các ngoại lệ được ném bởi phương thức (có trong trường hợp này).
Rejeev Divakaran

7
Làm thế nào để bạn tránh ngoại lệ xuất hiện trong nhật ký máy chủ của mình nếu 404 là trường hợp hợp lệ và không phải là lỗi (tức là mỗi khi bạn truy vấn tài nguyên, chỉ để xem liệu nó đã tồn tại chưa, với cách tiếp cận của bạn, một stacktrace xuất hiện trong máy chủ nhật ký).
Guido

3
Đáng nói là Jersey 2.x định nghĩa các ngoại lệ cho một số mã lỗi HTTP phổ biến nhất. Vì vậy, thay vì xác định các lớp con WebApplication của riêng bạn, bạn có thể sử dụng các lớp tích hợp sẵn như BadRequestException và NotAuthorizedException. Nhìn vào các lớp con của javax.ws.rs.ClientErrorException chẳng hạn. Cũng lưu ý rằng bạn có thể cung cấp một chuỗi chi tiết cho các nhà xây dựng. Ví dụ: ném BadRequestException mới ("Ngày bắt đầu phải trước ngày kết thúc");
Bampfer

1
bạn đã quên đề cập đến một cách tiếp cận khác: thực hiện ExceptionMappergiao diện (đó là một cách tiếp cận tốt hơn sau đó mở rộng). Xem thêm tại đây vvirlan.wordpress.com/2015/10/19/ Cách
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();
}
}

Tạo lớp trên. Điều này sẽ xử lý 404 (NotFoundException) và ở đây trong phương thức toResponse, bạn có thể đưa ra phản hồi tùy chỉnh của mình. Tương tự như vậy, có ParamException, vv mà bạn sẽ cần ánh xạ để cung cấp các phản hồi tùy chỉnh.


Bạn có thể sử dụng triển khai ExceptionMapper <Exception> cho các trường hợp ngoại lệ chung
Saurabh

1
Điều này cũng sẽ xử lý WebApplicationExceptions do JAX-RS Client ném, ẩn nguồn gốc lỗi. Tốt hơn nên có một Ngoại lệ tùy chỉnh (không bắt nguồn từ WebApplicationException) hoặc ném WebApplecting với Phản hồi hoàn chỉnh. WebApplicationExceptions được ném bởi JAX-RS Client phải được xử lý trực tiếp tại cuộc gọi, nếu không, phản hồi của dịch vụ khác được chuyển qua dưới dạng phản hồi của dịch vụ của bạn mặc dù đó là lỗi máy chủ nội bộ chưa được xử lý.
Markus Kull

38

Jersey ném com.sun.jersey.api.ParamException khi không thể sắp xếp các tham số, vì vậy một giải pháp là tạo ExceptionMapper xử lý các loại ngoại lệ này:

@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();
    }
}

Tôi nên tạo mapper này cụ thể cho Jersey để đăng ký nó ở đâu?
Patricio

1
Tất cả những gì bạn phải làm là thêm chú thích @Provider, xem tại đây để biết thêm chi tiết: stackoverflow.com/questions/15185299/
Kẻ

27

Bạn cũng có thể viết một lớp có thể sử dụng lại cho các biến có chú thích 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());
  }
}

sau đó sử dụng nó như thế này:

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

Mặc dù việc xử lý lỗi là không đáng kể trong trường hợp này (ném 400 phản hồi), nhưng việc sử dụng lớp này cho phép bạn xử lý tham số yếu tố nói chung có thể bao gồm ghi nhật ký, v.v.


Tôi đang cố gắng thêm trình xử lý truy vấn tùy chỉnh ở Jersey (di chuyển từ CXF), điều này trông khá giống với những gì tôi đang làm nhưng tôi không biết cách cài đặt / tạo nhà cung cấp mới. Lớp học trên của bạn không cho tôi thấy điều này. Tôi đang sử dụng các đối tượng DateTime Joda cho QueryParam và không có nhà cung cấp để giải mã chúng. Có phải nó dễ dàng như phân lớp nó, tạo cho nó một hàm tạo String và xử lý nó không?
Christian Bongiorno

1
Chỉ cần tạo một lớp giống như DateParamở trên mà kết thúc tốt đẹp org.joda.time.DateTimethay vì java.util.Calendar. Bạn sử dụng nó với @QueryParamhơn là DateTimechính nó.
Charlie Brooking

1
Nếu bạn đang sử dụng Joda DateTime, áo đi kèm với DateTimeParam để bạn sử dụng trực tiếp. Không cần phải viết của riêng bạn. Xem github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/iêu
Srikanth

Tôi sẽ thêm cái này vì nó siêu hữu ích, nhưng chỉ khi bạn đang sử dụng Jackson với Jersey. Jackson 2.x có một JodaModulecái có thể được đăng ký với ObjectMapper registerModulesphương thức. Nó có thể xử lý tất cả các chuyển đổi loại joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev

11

Một giải pháp rõ ràng: lấy một Chuỗi, tự chuyển đổi thành Ngày. Bằng cách đó, bạn có thể xác định định dạng bạn muốn, bắt ngoại lệ và ném lại hoặc tùy chỉnh lỗi được gửi. Để phân tích cú pháp, SimpleDateFormat sẽ hoạt động tốt.

Tôi chắc chắn cũng có nhiều cách để xử lý các trình xử lý cho các loại dữ liệu, nhưng có lẽ một chút mã đơn giản là tất cả những gì bạn cần trong trường hợp này.


7

Tôi cũng giống như StaxMan có thể sẽ triển khai QueryParam đó dưới dạng Chuỗi, sau đó xử lý chuyển đổi, chia sẻ lại khi cần thiết.

Nếu hành vi cụ thể của miền địa phương là hành vi mong muốn và mong đợi, bạn sẽ sử dụng cách sau để trả về lỗi 400 BAD YÊU CẦU:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Xem JavaDoc cho javax.ws.rs.core.Response.Status để có thêm tùy chọn.


4

Tài liệu @QueryParam nói

"Loại T của tham số, trường hoặc thuộc tính được chú thích phải:

1) Là kiểu nguyên thủy
2) Có hàm tạo chấp nhận một đối số Chuỗi đơn
3) Có một phương thức tĩnh có tên valueOf hoặc fromString chấp nhận một đối số Chuỗi đơn (ví dụ: Integer.valueOf (Chuỗi))
4) Có một đã đăng ký triển khai javax.ws.rs.ext.ParamConverterProvider SPI tiện ích mở rộng JAX-RS trả về một phiên bản javax.ws.rs.ext.ParamConverter có khả năng chuyển đổi "từ chuỗi" cho loại.
5) Be List, Set hoặc Sắp xếp, trong đó T thỏa mãn 2, 3 hoặc 4 ở trên. Bộ sưu tập kết quả là chỉ đọc. "

Nếu bạn muốn kiểm soát phản hồi nào cho người dùng khi tham số truy vấn ở dạng Chuỗi không thể được chuyển đổi thành loại T của bạn, bạn có thể ném WebApplicationException. Dropwizard đi kèm với các lớp * Param bạn có thể sử dụng cho nhu cầu của mình.

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

Nếu bạn cần Joda DateTime, chỉ cần sử dụng Dropwizard DateTimeParam .

Nếu danh sách trên không phù hợp với nhu cầu của bạn, hãy xác định danh sách của riêng bạn bằng cách mở rộng Tóm tắt. Ghi đè phương thức phân tích cú pháp. Nếu bạn cần kiểm soát cơ thể phản hồi lỗi, ghi đè phương thức lỗi.

Bài viết hay từ Coda Hale về điều này là tại 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();
    }
}

Hàm tạo ngày (String arg) không được dùng nữa. Tôi sẽ sử dụng các lớp ngày Java 8 nếu bạn ở trên Java 8. Nếu không thì thời gian ngày joda được khuyến nghị.


1

Đây là hành vi chính xác thực sự. Jersey sẽ cố gắng tìm một trình xử lý cho đầu vào của bạn và sẽ cố gắng xây dựng một đối tượng từ đầu vào được cung cấp. Trong trường hợp này, nó sẽ cố gắng tạo một đối tượng Date mới với giá trị X được cung cấp cho hàm tạo. Vì đây là một ngày không hợp lệ, theo quy ước Jersey sẽ trả về 404.

Những gì bạn có thể làm là viết lại và đặt ngày sinh dưới dạng Chuỗi, sau đó thử phân tích cú pháp và nếu bạn không có được những gì bạn muốn, bạn có thể ném bất kỳ ngoại lệ nào bạn muốn bằng bất kỳ cơ chế ánh xạ ngoại lệ nào (có một số ).


1

Tôi đã phải đối mặt với cùng một vấn đề.

Tôi muốn bắt tất cả các lỗi ở một vị trí trung tâm và biến đổi chúng.

Sau đây là mã cho cách tôi xử lý nó.

Tạo lớp sau đây thực hiện ExceptionMappervà thêm @Providerchú thích vào lớp này. Điều này sẽ xử lý tất cả các ngoại lệ.

toResponsePhương thức ghi đè và trả về đối tượng Phản hồi được điền với dữ liệu tùy chỉnh.

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

Cách tiếp cận 1: Bằng cách mở rộng lớp WebApplicationException

Tạo ngoại lệ mới bằng cách mở rộng 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());
         }
}

Bây giờ ném 'RestException' bất cứ khi nào cần thiết.

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

Bạn có thể thấy ứng dụng hoàn chỉnh tại liên kết này .

Cách tiếp cận 2: Thực hiện ExceptionMapper

Trình theo dõi mapper xử lý ngoại lệ của loại '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();
    }

}

Bạn có thể thấy ứng dụng hoàn chỉnh tại liên kết này .


0

Cũng giống như một phần mở rộng cho câu trả lời @Steven Lavine trong trường hợp bạn muốn mở cửa sổ đăng nhập trình duyệt. Tôi thấy thật khó để trả lại đúng Phản hồi ( Xác thực HTTP MDN ) từ Bộ lọc trong trường hợp người dùng chưa được xác thực

Điều này giúp tôi xây dựng Phản hồi để buộc đăng nhập trình duyệt, lưu ý sửa đổi bổ sung của các tiêu đề. Điều này sẽ đặt mã trạng thái thành 401 và đặt tiêu đề khiến trình duyệt mở hộp thoại tên người dùng / mật khẩu.

// 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"); }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.