JAX-RS / Jersey nasıl hata işleme özelleştirmek için?


216

Jersey kullanarak JAX-RS (aka JSR-311) öğreniyorum. Başarıyla bir Kök Kaynak oluşturdum ve parametrelerle oynuyorum:

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

Bu harika çalışır ve geçerli yerel ayarda Tarih (Dize) yapıcısı tarafından anlaşılan (YYYY / aa / gg ve aa / gg / YYYY gibi) herhangi bir biçimi işler. Ancak geçersiz veya anlaşılmayan bir değer sağlarsam 404 yanıtı alırım.

Örneğin:

GET /hello?name=Mark&birthDate=X

404 Not Found

Bu davranışı nasıl özelleştirebilirim? Belki farklı bir yanıt kodu (muhtemelen "400 Hatalı İstek")? Bir hatayı günlüğe kaydetmeye ne dersiniz? Sorun gidermeye yardımcı olması için özel bir başlığa sorunun açıklamasını ("bozuk tarih biçimi") ekleyebilirsiniz? Veya 5xx durum koduyla birlikte ayrıntılarla birlikte bir Hata yanıtı döndürmek mi istiyorsunuz?

Yanıtlar:


271

JAX-RS ile hata işleme davranışını özelleştirmek için çeşitli yaklaşımlar vardır. İşte daha kolay üç yol.

İlk yaklaşım, WebApplicationException öğesini genişleten bir Exception sınıfı oluşturmaktır.

Misal:

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

Ve bu yeni oluşturulan İstisna atmak için basitçe:

@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 bir çalışma zamanı özel durumu olduğundan, özel durumu bir throws deyiminde bildirmeniz gerekmez. Bu, istemciye 401 yanıtı döndürür.

İkinci ve daha kolay yaklaşım, WebApplicationExceptiondoğrudan kodunuzda doğrudan bir örnek oluşturmaktır . Bu yaklaşım, kendi uygulama İstisnalarınızı uygulamak zorunda olmadığınız sürece çalışır.

Misal:

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

Bu kod da istemciye bir 401 döndürür.

Tabii ki, bu sadece basit bir örnek. İsterseniz İstisna'yı çok daha karmaşık hale getirebilir ve ihtiyacınız olan http yanıt kodunu oluşturabilirsiniz.

Diğer bir yaklaşım, mevcut bir İstisna'yı, belki de bir açıklama ile ek açıklamalı arabirimi ObjectNotFoundExceptionuygulayan küçük bir sarmalayıcı sınıfıyla sarmaktır. Bu, JAX-RS çalışma zamanına, sarılmış İstisna yükseltilirse ,. ExceptionMapper@ProviderExceptionMapper


3
Örneğinizde, super () çağrısı biraz farklı olmalıdır: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); İçgörü için teşekkürler.
Jon Onstott

65
Soruda belirtilen senaryoda, Jersey, giriş değerinden Date nesnesi örneği oluşturamayacağından, istisna oluşturacağından bir istisna atma şansınız olmayacaktır. Jersey istisnasını engellemenin bir yolu var mı? Bir ExceptionMapper arabirimi vardır, ancak bu yöntem tarafından atılan istisnaları da engeller (bu durumda olsun).
Rejeev Divakaran

7
404 geçerli bir durum ve bir hata değilse, sunucu günlüğünüzde özel durumun görünmesini nasıl önlersiniz (yani bir kaynağı her sorguladığınızda, zaten var olup olmadığını görmek için, yaklaşımınızla birlikte sunucuda bir yığın izlemesi görüntülenir kütükler).
Guido

3
Jersey 2.x'in en yaygın bazı HTTP hata kodları için istisnaları tanımladığını belirtmek gerekir. Dolayısıyla, kendi WebApplication alt sınıflarınızı tanımlamak yerine, BadRequestException ve NotAuthorizedException gibi yerleşik olanları kullanabilirsiniz. Örneğin, javax.ws.rs.ClientErrorException alt sınıflarına bakın. Ayrıca yapıcılara bir ayrıntı dizesi sağlayabileceğinizi unutmayın. Örneğin: yeni BadRequestException ("Başlangıç ​​tarihi bitiş tarihinden önce olmalıdır");
Bampfer

1
başka bir yaklaşımdan bahsetmeyi unutmuştunuz: ExceptionMapperarayüzü uygulamak (daha sonra genişletmek daha iyi bir yaklaşımdır). Daha fazlasını burada görebilirsiniz 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();
}
}

Sınıfın üzerinde oluşturun. Bu işlem 404'ü (NotFoundException) işleyecek ve burada toResponse yönteminde özel yanıtınızı verebilirsiniz. Benzer şekilde, özelleştirilmiş yanıtlar sağlamak için eşlemeniz gereken ParamException vb. Vardır.


Genel istisnalar için de ExceptionMapper <Exception> uygulamalarını kullanabilirsiniz
Saurabh

1
Bu, JAX-RS İstemcisi tarafından atılan WebApplicationExceptions'ı da işleyerek hata kaynağını gizler. Özel bir İstisna (WebApplicationException türetilmemiş) olması veya WebApplications'ı tam Yanıtla atmanız daha iyi olur. JAX-RS İstemcisi tarafından atılan WebApplicationExceptions doğrudan çağrıda ele alınmalıdır, aksi takdirde işlenmemiş bir dahili sunucu hatası olmasına rağmen başka bir hizmetin yanıtı hizmetinizin yanıtı olarak aktarılır.
Markus Kull

38

Jersey, parametreleri çözmeyi başaramadığında bir com.sun.jersey.api.ParamException kurar, böylece bir çözüm bu tür özel durumları işleyen bir ExceptionMapper oluşturmaktır:

@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 için özel olarak bu haritayı nerede oluşturmalıyım?
Patricio

1
Tek yapmanız gereken @Provider ek açıklamasını eklemek, daha fazla ayrıntı için buraya bakın: stackoverflow.com/questions/15185299/…
Jan Kronquist

27

Ayrıca QueryParam açıklamalı değişkenleri için yeniden kullanılabilir bir sınıf yazabilirsiniz

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

sonra şu şekilde kullanın:

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

Bu durumda hata işleme önemsiz olsa da (400 yanıt atarak), bu sınıfı kullanmak genel olarak günlüğe kaydetme vb. Gibi parametre işlemeyi faktör dışı bırakmanıza olanak tanır.


Jersey (CXF göç) özel bir sorgu param işleyicisi eklemek için çalışıyorum bu ne yaptığım oldukça benzer görünüyor ama yeni bir sağlayıcı yükleme / oluşturma bilmiyorum. Yukarıdaki sınıfın bana bunu göstermiyor. QueryParam için JodaTime DateTime nesneleri kullanıyorum ve bunları çözmek için bir sağlayıcı yok. Bir String yapıcısı vermek ve bunu idare etmek alt sınıflamak kadar kolay mıdır?
Christian Bongiorno

1
Bunun gibi bir sınıf oluşturun DateParamorg.joda.time.DateTime yerine yukarıdakijava.util.Calendar . Bunu kendinden @QueryParamziyade kullanıyorsunuz DateTime.
Charlie Brooking

1
Joda DateTime kullanıyorsanız, jersey doğrudan kullanmanız için DateTimeParam ile birlikte gelir. Kendi yazmanıza gerek yok. Bkz. Github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth

Bunu ekleyeceğim çünkü süper kullanışlı, ancak Jackson'ı Jersey ile kullanıyorsanız. Jackson 2.x birJodaModule bu ObjectMapper registerModulesyöntemle kaydedilebilecek özelliği . Tüm joda tipi dönüşümleri işleyebilir. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev

11

Bir bariz çözüm: bir String alın, Date to kendinizi dönüştürün. Bu şekilde istediğiniz biçimi tanımlayabilir, istisnaları yakalayabilir ve gönderilen hatayı yeniden atabilir veya özelleştirebilirsiniz. Ayrıştırma için SimpleDateFormat düzgün çalışmalıdır.

Ben de veri türleri için işleyicileri kanca yolları eminim, ama belki de bu durumda ihtiyacınız olan basit kod biraz.


7

Ben de staxman seviyorum muhtemelen bu QueryParam bir dize olarak uygulamak, daha sonra gerektiği gibi geri dönüşüm dönüşüm işlemek istiyorum.

Yerel ayara özgü davranış istenen ve beklenen davranışsa, 400 BAD REQUEST hatasını döndürmek için aşağıdakileri kullanırsınız:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Daha fazla seçenek için javax.ws.rs.core.Response.Status için JavaDoc'a bakın .


4

@QueryParam belgeleri diyor

"Ek açıklamalı parametre, alan veya özelliğin T türü şunlardan biri olmalıdır:

1) İlkel tür olun
2) Tek bir String bağımsız değişkenini
kabul eden bir yapıcıya sahip olun 3) Tek bir String bağımsız değişkenini kabul eden valueOf veya fromString adlı statik bir yönteme sahip olun (bkz. Örneğin Integer.valueOf (String))
4) javax.ws.rs.ext.ParamConverterProvider türünde "dize" dönüşümü yapabilen bir javax.ws.rs.ext.ParamConverter örneği döndüren JAX-RS uzantısı SPI'nın kayıtlı uygulaması.
5) T'nin yukarıdaki 2, 3 veya 4'ü karşıladığı Liste, Ayarla veya Sırala. Sonuçta elde edilen koleksiyon salt okunurdur. "

Dize formundaki sorgu parametresi T türünüze dönüştürülemediğinde kullanıcıya hangi yanıtın gideceğini denetlemek isterseniz, WebApplicationException özel durumunu atayabilirsiniz. Dropwizard aşağıdaki * İhtiyaçlarınız için kullanabileceğiniz Param sınıflarıyla birlikte gelir.

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

Joda DateTime'a ihtiyacınız varsa, Dropwizard DateTimeParam'ı kullanmanız yeterlidir .

Yukarıdaki liste ihtiyaçlarınızı karşılamıyorsa, AbstractParam'ı genişleterek kendinizinkini tanımlayın. Ayrıştırma yöntemini geçersiz kıl. Hata yanıt gövdesi üzerinde denetime ihtiyacınız varsa, hata yöntemini geçersiz kılın.

Coda Hale'den bu konuda iyi bir makale 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) yapıcı kullanımdan kaldırıldı. Java 8 kullanıyorsanız Java 8 tarih sınıflarını kullanırdım. Aksi halde joda tarih saati önerilir.


1

Aslında doğru davranış budur. Jersey, girdiniz için bir işleyici bulmaya çalışacak ve sağlanan girdiden bir nesne oluşturmaya çalışacaktır. Bu durumda kurucuya sağlanan X değeriyle yeni bir Date nesnesi oluşturmaya çalışır. Bu geçersiz bir tarih olduğundan, sözleşme gereği Jersey 404'ü iade edecektir.

Yapabileceğiniz şey doğum tarihini bir Dize olarak yeniden yazmak ve koymaktır, sonra ayrıştırmaya çalışın ve istediğiniz şeyi elde etmezseniz, istisna eşleme mekanizmalarından herhangi biri tarafından istediğiniz herhangi bir istisnayı atabilirsiniz (birkaç tane vardır) ).


1

Aynı sorunla karşı karşıyaydım.

Tüm hataları merkezi bir yerde yakalamak ve dönüştürmek istedim.

Aşağıda nasıl ele aldığımın kodu verilmiştir.

Bu sınıfa ek açıklama ExceptionMapperekleyen aşağıdaki sınıfı oluşturun @Provider. Bu, tüm istisnaları ele alacaktır.

toResponseYöntemi geçersiz kıl ve özelleştirilmiş verilerle doldurulmuş Response nesnesini döndür.

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

Yaklaşım 1: WebApplicationException sınıfını genişleterek

WebApplicationException öğesini genişleterek yeni kural dışı durum oluşturma

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

Şimdi gerektiğinde 'RestException' ifadesini atın.

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

Uygulamanın tamamını bu bağlantıda görebilirsiniz .

Yaklaşım 2: ExceptionMapper Uygula

Aşağıdaki eşleyici 'DataNotFoundException' türünün özel durumunu işliyor

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

}

Uygulamanın tamamını bu bağlantıda görebilirsiniz .


0

Tarayıcı giriş penceresini açmak istemeniz durumunda @Steven Lavine yanıtının bir uzantısı gibi. Kullanıcının henüz kimlik doğrulaması yapılmaması durumunda Yanıt'ı ( MDN HTTP Kimlik Doğrulaması ) Filtreden düzgün bir şekilde döndürmek zor buldum

Bu, tarayıcı girişini zorlamak için Yanıt oluşturmama yardımcı oldu, üstbilgilerin ek değişikliğine dikkat edin. Bu, durum kodunu 401 olarak ayarlar ve tarayıcının kullanıcı adı / şifre iletişim kutusunu açmasına neden olan üstbilgiyi ayarlar.

// 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"); }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.