JAX-RS / Jersey วิธีการจัดการข้อผิดพลาดที่กำหนดเอง?


216

ฉันกำลังเรียนรู้ JAX-RS (aka, JSR-311) โดยใช้ Jersey ฉันสร้างทรัพยากรรูทสำเร็จแล้วและกำลังเล่นกับพารามิเตอร์:

@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 คำขอไม่ถูกต้อง")? สิ่งที่เกี่ยวกับการบันทึกข้อผิดพลาด? อาจเพิ่มคำอธิบายปัญหา ("รูปแบบวันที่ไม่ดี") ในส่วนหัวที่กำหนดเองเพื่อช่วยแก้ไขปัญหาหรือไม่ หรือส่งคืนการตอบกลับข้อผิดพลาดทั้งหมดพร้อมรายละเอียดพร้อมกับรหัสสถานะ 5xx

คำตอบ:


271

มีหลายวิธีในการปรับแต่งพฤติกรรมการจัดการข้อผิดพลาดด้วย JAX-RS ต่อไปนี้เป็นวิธีที่ง่ายกว่าสามวิธี

วิธีแรกคือการสร้างคลาสข้อยกเว้นที่ขยาย 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");
}

โปรดสังเกตว่าคุณไม่จำเป็นต้องประกาศข้อยกเว้นในประโยคข้อผิดพลาดเนื่องจาก 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 ไปยังไคลเอนต์

แน่นอนว่านี่เป็นเพียงตัวอย่างง่ายๆ คุณสามารถทำให้ข้อยกเว้นซับซ้อนยิ่งขึ้นได้ถ้าจำเป็นและคุณสามารถสร้างโค้ดการตอบสนอง http ที่คุณต้องการ

อีกวิธีหนึ่งคือการตัด Exception ที่มีอยู่บางทีObjectNotFoundExceptionด้วยคลาส wrapper ขนาดเล็กที่ใช้ExceptionMapperอินเตอร์เฟสที่มีคำอธิบายประกอบพร้อม@Providerหมายเหตุประกอบ นี้บอก JAX-RS ExceptionMapperรันไทม์ว่าถ้าข้อยกเว้นห่อถูกยกกลับรหัสการตอบสนองที่กำหนดไว้ใน


3
ในตัวอย่างของคุณการเรียกไปที่ super () ควรแตกต่างกันเล็กน้อย: super (Response.status (Status.UNAUTHORIZED)) เอนทิตี (ข้อความ) .type ("text / plain"). build ()); ขอบคุณสำหรับความเข้าใจ
Jon Onstott

65
ในสถานการณ์ที่กล่าวถึงในคำถามคุณจะไม่ได้รับโอกาสในการโยนข้อยกเว้นเนื่องจาก Jersey จะยกข้อยกเว้นเนื่องจากจะไม่สามารถสร้างอินสแตนซ์ของวัตถุ Date จากค่าอินพุต มีวิธีที่จะสกัดกั้นข้อยกเว้นของ Jersey หรือไม่? มีอินเทอร์เฟซ ExceptionMapper หนึ่งอินเทอร์เฟซอย่างไรก็ตามที่ยัง intercepts ข้อยกเว้นที่ส่งออกมาโดยวิธีการ (รับในกรณีนี้)
Rejeev Divakaran

7
คุณจะหลีกเลี่ยงข้อยกเว้นที่จะปรากฏในบันทึกเซิร์ฟเวอร์ของคุณได้อย่างไรถ้า 404 เป็นกรณีที่ถูกต้องและไม่ใช่ข้อผิดพลาด (เช่นทุกครั้งที่คุณค้นหาทรัพยากรเพียงเพื่อดูว่ามันมีอยู่แล้วด้วยวิธีการของคุณ บันทึก)
กุยโด้

3
เป็นมูลค่าการกล่าวขวัญว่า Jersey 2.x กำหนดข้อยกเว้นสำหรับรหัสข้อผิดพลาด HTTP ที่พบบ่อยที่สุด ดังนั้นแทนที่จะกำหนดคลาสย่อยของคุณเองของ WebApplication คุณสามารถใช้ตัวที่มีอยู่แล้วภายในเช่น BadRequestException และ NotAuthorizedException ดูคลาสย่อยของ javax.ws.rs.ClientErrorException เป็นตัวอย่าง นอกจากนี้โปรดทราบว่าคุณสามารถระบุสตริงรายละเอียดให้กับผู้สร้างได้ ตัวอย่างเช่น: โยน BadRequestException ใหม่ ("วันที่เริ่มต้นต้องอยู่ก่อนวันที่สิ้นสุด");
Bampfer

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 <Exception> ได้เช่นกันสำหรับข้อยกเว้นทั่วไป
Saurabh

1
สิ่งนี้จะจัดการ WebApplicationExceptions ที่โยนโดยไคลเอนต์ JAX-RS ด้วยเช่นกันซ่อนแหล่งกำเนิดข้อผิดพลาด มีข้อยกเว้นแบบกำหนดเองที่ดีกว่า (ไม่ได้มาจาก WebApplicationException) หรือส่ง WebApplications พร้อมการตอบกลับที่สมบูรณ์ WebApplicationExceptions ที่โยนโดยไคลเอนต์ JAX-RS ควรได้รับการจัดการโดยตรงที่การโทรมิฉะนั้นการตอบสนองของบริการอื่นจะถูกส่งผ่านเป็นการตอบสนองของบริการของคุณแม้ว่ามันจะเป็นข้อผิดพลาดเซิร์ฟเวอร์ภายในที่ไม่สามารถจัดการได้
Markus Kull

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

ฉันควรสร้างผู้ทำแผนที่นี้โดยเฉพาะสำหรับ Jersey เพื่อลงทะเบียนได้อย่างไร
Patricio

1
สิ่งที่คุณต้องทำคือการเพิ่มคำอธิบายประกอบ @Provider ดูที่นี่สำหรับรายละเอียดเพิ่มเติม: stackoverflow.com/questions/15185299//
Jan Kronquist

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) สิ่งนี้ดูคล้ายกับสิ่งที่ฉันทำ แต่ฉันไม่รู้วิธีติดตั้ง / สร้างผู้ให้บริการใหม่ ชั้นเรียนของคุณไม่แสดงให้ฉันเห็น ฉันใช้วัตถุ JodaTime DateTime สำหรับ QueryParam และไม่มีผู้ให้บริการที่จะถอดรหัสพวกเขา มันง่ายเหมือน subclassing หรือไม่ให้มันเป็นตัวสร้างสตริงและจัดการมันได้หรือไม่
Christian Bongiorno

1
เพียงแค่สร้างชั้นเรียนเช่นDateParamหนึ่งดังกล่าวข้างต้นที่ล้อมแทนorg.joda.time.DateTime java.util.Calendarคุณใช้มันด้วยตัวเอง@QueryParamมากกว่า DateTime
Charlie Brooking

1
หากคุณใช้ Joda DateTime jersey มาพร้อมกับ 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 BAD REQUEST ข้อผิดพลาด:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

ดู JavaDoc สำหรับjavax.ws.rs.core.Response.Statusสำหรับตัวเลือกเพิ่มเติม


4

เอกสาร @QueryParam พูดว่า

"ประเภท T ของพารามิเตอร์ฟิลด์หรือคุณสมบัติที่ทำหมายเหตุประกอบไว้ต้อง:

1) เป็นประเภทดั้งเดิม
2) มี Constructor ที่ยอมรับอาร์กิวเมนต์ String เดี่ยว
3) มีเมธอดสแตติกชื่อ valueOf หรือ fromString ที่ยอมรับอาร์กิวเมนต์ String เดี่ยว (ดูตัวอย่างเช่น Integer.valueOf (String))
4) การใช้งานที่ลงทะเบียนของ javax.ws.rs.ext.ParamConverterProvider SPI ส่วนขยาย JAX-RS ที่ส่งคืนอินสแตนซ์ javax.ws.rs.ext.ParamConverter ที่สามารถแปลงเป็น "จากสตริง" สำหรับประเภท
5) Be List, Set หรือ SortedSet โดยที่ T ตรงกับ 2, 3 หรือ 4 ด้านบน การรวบรวมผลลัพธ์เป็นแบบอ่านอย่างเดียว "

หากคุณต้องการควบคุมการตอบสนองที่ผู้ใช้ต้องการเมื่อไม่สามารถแปลงพารามิเตอร์การสืบค้นในรูปแบบ String เป็นชนิด 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 จะพยายามค้นหา handler สำหรับอินพุตของคุณและจะพยายามสร้างวัตถุจากอินพุตที่ให้ไว้ ในกรณีนี้มันจะพยายามสร้างวัตถุ Date ใหม่ด้วยค่า X ที่ให้ไว้กับ Constructor เนื่องจากนี่เป็นวันที่ไม่ถูกต้องตามระเบียบของ Jersey จะส่งคืน 404

สิ่งที่คุณสามารถทำได้คือเขียนใหม่และใส่วันเกิดเป็นสตริงจากนั้นลองแยกวิเคราะห์และถ้าคุณไม่ได้สิ่งที่คุณต้องการคุณมีอิสระที่จะโยนข้อยกเว้นใด ๆ ที่คุณต้องการโดยกลไกการทำแผนที่ข้อยกเว้นใด ๆ )


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 Authentication ) อย่างถูกต้องจากตัวกรองในกรณีที่ผู้ใช้ยังไม่ได้รับการรับรองความถูกต้อง

สิ่งนี้ช่วยให้ฉันสร้างการตอบสนองเพื่อบังคับให้เข้าสู่ระบบเบราว์เซอร์บันทึกการแก้ไขเพิ่มเติมของส่วนหัว สิ่งนี้จะตั้งรหัสสถานะเป็น 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"); }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.