JAX-RS / Jersey bagaimana cara menyesuaikan penanganan kesalahan?

216

Saya belajar JAX-RS (alias, JSR-311) menggunakan Jersey. Saya telah berhasil membuat Sumber Daya Root dan bermain-main dengan parameter:

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

Ini berfungsi dengan baik, dan menangani format apa pun di lokal saat ini yang dipahami oleh konstruktor Date (String) (seperti YYYY / mm / dd dan mm / dd / YYYY). Tetapi jika saya memberikan nilai yang tidak valid atau tidak dipahami, saya mendapatkan respons 404.

Sebagai contoh:

GET /hello?name=Mark&birthDate=X

404 Not Found

Bagaimana saya bisa menyesuaikan perilaku ini? Mungkin kode respons yang berbeda (mungkin "400 Permintaan Buruk")? Bagaimana dengan mencatat kesalahan? Mungkin menambahkan deskripsi masalah ("format tanggal buruk") di header khusus untuk membantu pemecahan masalah? Atau kembalikan respons Kesalahan lengkap dengan detail, bersama dengan kode status 5xx?

Mark Renouf
sumber

Jawaban:

271

Ada beberapa pendekatan untuk menyesuaikan perilaku penanganan kesalahan dengan JAX-RS. Berikut adalah tiga cara termudah.

Pendekatan pertama adalah membuat kelas Exception yang memperluas WebApplicationException.

Contoh:

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

Dan untuk membuang Exception yang baru dibuat ini, Anda cukup:

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

Perhatikan, Anda tidak perlu mendeklarasikan pengecualian dalam klausa throws karena WebApplicationException adalah pengecualian runtime. Ini akan mengembalikan respons 401 kepada klien.

Pendekatan kedua dan lebih mudah adalah dengan membuat instance WebApplicationExceptionlangsung dalam kode Anda. Pendekatan ini berfungsi selama Anda tidak harus menerapkan Pengecualian aplikasi Anda sendiri.

Contoh:

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

Kode ini juga mengembalikan 401 ke klien.

Tentu saja, ini hanyalah contoh sederhana. Anda dapat membuat Exception jauh lebih rumit jika perlu, dan Anda dapat menghasilkan kode respons http apa pun yang Anda perlukan.

Satu pendekatan lain adalah dengan membungkus Pengecualian yang ada, mungkin ObjectNotFoundExceptiondengan kelas pembungkus kecil yang mengimplementasikan ExceptionMapperantarmuka yang dijelaskan dengan @Provideranotasi. Ini memberitahu runtime JAX-RS, bahwa jika Exception yang dibungkus dinaikkan, kembalikan kode respons yang ditentukan dalam ExceptionMapper.

Steven Levine
sumber
3
Dalam contoh Anda, panggilan ke super () harus sedikit berbeda: super (Response.status (Status.UNAUTHORIZED). Entitas (pesan) .type ("text / plain"). Build ()); Terima kasih atas wawasannya.
Jon Onstott
65
Dalam skenario yang disebutkan dalam pertanyaan, Anda tidak akan mendapatkan kesempatan untuk melemparkan pengecualian, karena Jersey akan meningkatkan pengecualian karena tidak akan dapat membuat instance objek Date dari nilai input. Apakah ada cara untuk mencegat pengecualian Jersey? Ada satu antarmuka ExceptionMapper, namun itu juga mencegat pengecualian yang dilemparkan oleh metode (dapatkan dalam kasus ini).
Rejeev Divakaran
7
Bagaimana Anda menghindari pengecualian untuk muncul di log server Anda jika 404 adalah kasus yang valid dan bukan kesalahan (yaitu setiap kali Anda meminta sumber daya, hanya untuk melihat apakah sudah ada, dengan pendekatan Anda stacktrace muncul di server log).
Guido
3
Patut disebutkan bahwa Jersey 2.x mendefinisikan pengecualian untuk beberapa kode kesalahan HTTP paling umum. Jadi, alih-alih mendefinisikan subkelas Anda sendiri dari WebApplication, Anda bisa menggunakan yang built-in seperti BadRequestException dan NotAuthorizedException. Lihatlah subclass dari javax.ws.rs.ClientErrorException misalnya. Perhatikan juga Anda dapat memberikan string detail ke konstruktor. Misalnya: melempar BadRequestException baru ("Tanggal mulai harus mendahului tanggal akhir");
Bampfer
1
Anda lupa menyebutkan pendekatan lain: mengimplementasikan ExceptionMapperantarmuka (yang merupakan pendekatan yang lebih baik kemudian memperluas). Lihat lebih lanjut di sini 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();
}
}

Buat kelas di atas. Ini akan menangani 404 (NotFoundException) dan di sini di metode toResponse Anda dapat memberikan respons khusus. Demikian pula ada ParamException dll. Yang perlu Anda petakan untuk memberikan tanggapan yang disesuaikan.

Arnav
sumber
Anda dapat menggunakan implement ExceptionMapper <Exception> juga untuk pengecualian umum
Saurabh
1
Ini akan menangani WebApplicationExceptions yang dilemparkan oleh JAX-RS Client juga, menyembunyikan asal kesalahan. Lebih baik memiliki Pengecualian khusus (tidak berasal dari WebApplicationException) atau membuang Aplikasi Web dengan Respon lengkap. WebApplicationExceptions yang dilemparkan oleh JAX-RS Client harus ditangani langsung saat panggilan berlangsung, jika tidak, respons layanan lain dilewatkan sebagai respons layanan Anda meskipun itu merupakan kesalahan server internal yang tidak tertangani.
Markus Kull
38

Jersey melempar com.sun.jersey.api.ParamException ketika gagal menghapus parameter, jadi salah satu solusinya adalah membuat ExceptionMapper yang menangani jenis pengecualian ini:

@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
sumber
di mana saya harus membuat mapper ini khusus untuk Jersey untuk mendaftarkannya?
Patricio
1
Yang harus Anda lakukan adalah menambahkan anotasi @Provider, lihat di sini untuk detail lebih lanjut: stackoverflow.com/questions/15185299/…
Jan Kronquist
27

Anda juga bisa menulis kelas yang dapat digunakan kembali untuk variabel beranotasi 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());
  }
}

lalu gunakan seperti ini:

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

Meskipun penanganan kesalahannya sepele dalam kasus ini (melemparkan respons 400), menggunakan kelas ini memungkinkan Anda untuk memperhitungkan faktor penanganan parameter secara umum yang mungkin termasuk pencatatan, dll.

Charlie Brooking
sumber
Saya mencoba untuk menambahkan param handler kueri khusus di Jersey (bermigrasi dari CXF) ini terlihat sangat mirip dengan apa yang saya lakukan tetapi saya tidak tahu cara menginstal / membuat penyedia baru. Kelas Anda di atas tidak menunjukkan ini pada saya. Saya menggunakan objek JodaTime DateTime untuk QueryParam dan tidak memiliki penyedia untuk memecahkan kode mereka. Apakah semudah mensubclassingnya, memberinya String constructor dan menanganinya?
Christian Bongiorno
1
Cukup buat kelas seperti di DateParamatas yang membungkus org.joda.time.DateTimebukan java.util.Calendar. Anda menggunakannya dengan @QueryParambukan DateTimedirinya sendiri.
Charlie Brooking
1
Jika Anda menggunakan Joda DateTime, jersey dilengkapi dengan DateTimeParam untuk Anda gunakan secara langsung. Tidak perlu menulis sendiri. Lihat github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Saya akan menambahkan ini karena sangat berguna, tetapi hanya jika Anda menggunakan Jackson dengan Jersey. Jackson 2.x memiliki JodaModuleyang dapat didaftarkan dengan ObjectMapper registerModulesmetode ini. Itu dapat menangani semua konversi jenis joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

Satu solusi yang jelas: ambil dalam sebuah String, konversikan ke Date yourself. Dengan begitu Anda dapat menentukan format yang Anda inginkan, menangkap pengecualian dan melempar ulang atau menyesuaikan kesalahan yang sedang dikirim. Untuk parsing, SimpleDateFormat seharusnya berfungsi dengan baik.

Saya yakin ada cara untuk mengaitkan penangan untuk tipe data juga, tetapi mungkin sedikit kode sederhana adalah semua yang Anda butuhkan dalam kasus ini.

StaxMan
sumber
7

Saya juga suka StaxMan mungkin akan mengimplementasikan QueryParam itu sebagai sebuah String, kemudian menangani konversi, rethrowing seperlunya.

Jika perilaku spesifik lokal adalah perilaku yang diinginkan dan diharapkan, Anda akan menggunakan yang berikut ini untuk mengembalikan kesalahan 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Lihat JavaDoc untuk javax.ws.rs.core.Response.Status untuk opsi lebih lanjut.

dshaw
sumber
4

Dokumentasi @QueryParam berkata

"Tipe T dari parameter, bidang, atau properti yang dianotasi harus:

1) Menjadi tipe primitif
2) Memiliki konstruktor yang menerima argumen String tunggal
3) Memiliki metode statis bernama valueOf atau fromString yang menerima argumen String tunggal (lihat, misalnya, Integer.valueOf (String))
4) Memiliki implementasi terdaftar dari javax.ws.rs.ext.ParamConverterProvider JAX-RS ekstensi SPI yang mengembalikan instance javax.ws.rs.ext.ParamConverter yang mampu melakukan konversi "dari string" untuk jenis tersebut.
5) Jadilah Daftar, Atur atau Urutkan, di mana T memenuhi 2, 3 atau 4 di atas. Koleksi yang dihasilkan hanya baca. "

Jika Anda ingin mengontrol respons apa yang masuk ke pengguna ketika parameter kueri dalam bentuk String tidak dapat dikonversi ke tipe T Anda, Anda bisa melempar WebApplicationException. Dropwizard hadir dengan kelas * Param berikut yang dapat Anda gunakan untuk kebutuhan Anda.

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

Jika Anda membutuhkan Joda DateTime, cukup gunakan Dropwizard DateTimeParam .

Jika daftar di atas tidak sesuai dengan kebutuhan Anda, tentukan sendiri dengan memperluas AbstractParam. Ganti metode parse. Jika Anda perlu mengontrol badan respons kesalahan, ganti metode kesalahan.

Artikel bagus dari Coda Hale tentang ini ada di 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();
    }
}

Tanggal (String arg) konstruktor sudah usang. Saya akan menggunakan kelas tanggal Java 8 jika Anda berada di Jawa 8. Jika tidak, tanggal joda disarankan.

Srikanth
sumber
1

Ini sebenarnya perilaku yang benar. Jersey akan mencoba menemukan penangan untuk input Anda dan akan mencoba untuk membangun objek dari input yang disediakan. Dalam hal ini akan mencoba untuk membuat objek Date baru dengan nilai X yang diberikan kepada konstruktor. Karena ini adalah tanggal yang tidak valid, dengan konvensi Jersey akan mengembalikan 404.

Apa yang dapat Anda lakukan adalah menulis ulang dan menempatkan tanggal lahir sebagai sebuah String, kemudian mencoba untuk menguraikan dan jika Anda tidak mendapatkan apa yang Anda inginkan, Anda bebas untuk membuang pengecualian yang Anda inginkan dengan salah satu mekanisme pemetaan pengecualian (ada beberapa ).

ACV
sumber
1

Saya menghadapi masalah yang sama.

Saya ingin menangkap semua kesalahan di tempat sentral dan mengubahnya.

Berikut ini adalah kode untuk bagaimana saya menanganinya.

Buat kelas berikut yang mengimplementasikan ExceptionMapperdan menambahkan @Provideranotasi pada kelas ini. Ini akan menangani semua pengecualian.

Ganti toResponsemetode dan kembalikan objek Respons yang diisi dengan data khusus.

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

Pendekatan 1: Dengan memperluas kelas WebApplicationException

Buat pengecualian baru dengan memperluas 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());
         }
}

Sekarang lempar 'RestException' kapan pun diperlukan.

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

Anda dapat melihat aplikasi lengkap di tautan ini .

Pendekatan 2: Terapkan ExceptionMapper

Mapper berikut menangani pengecualian tipe '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();
    }

}

Anda dapat melihat aplikasi lengkap di tautan ini .

Hari Krishna
sumber
0

Sama seperti ekstensi ke @Steven Lavine jika Anda ingin membuka jendela login browser. Saya merasa sulit untuk mengembalikan Respons ( MDN HTTP Authentication ) dengan benar dari Filter jika pengguna belum diautentikasi

Ini membantu saya untuk membangun Respons untuk memaksa masuk browser, perhatikan modifikasi tambahan dari header. Ini akan mengatur kode status ke 401 dan mengatur header yang menyebabkan browser membuka dialog nama pengguna / kata sandi.

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