Spring Boot REST API Hata İşleme Kılavuzu

Yayınlanan: 2022-03-11

API istemcisinin sorunlara doğru şekilde yanıt vermesine yardımcı olabileceğinden, anlamlı hata mesajları sağlarken API'lerdeki hataları doğru şekilde ele almak çok istenen bir özelliktir. Varsayılan davranış, anlaşılması zor ve sonuçta API istemcisi için işe yaramaz yığın izleri döndürme eğilimindedir. Hata bilgilerini alanlara bölmek, API istemcisinin bunları ayrıştırmasını ve kullanıcıya daha iyi hata mesajları sunmasını da sağlar. Bu makalede, Spring Boot ile bir REST API oluştururken doğru hata işlemenin nasıl yapıldığını ele alacağız.

Kişi, şifreli ve uzun bir hata mesajı hakkında kafası karıştı

Spring ile REST API'leri oluşturmak, son birkaç yılda Java geliştiricileri için standart yaklaşım haline geldi. Spring Boot'u kullanmak, çok sayıda ortak kodu ortadan kaldırdığı ve çeşitli bileşenlerin otomatik olarak yapılandırılmasını sağladığı için büyük ölçüde yardımcı olur. Burada açıklanan bilgileri uygulamadan önce bu teknolojilerle API geliştirmenin temellerini bildiğinizi varsayacağız. Temel bir REST API'sinin nasıl geliştirileceğinden hala emin değilseniz, Spring MVC hakkındaki bu makaleyle veya Spring REST Hizmeti oluşturma hakkında başka bir makaleyle başlamalısınız.

Hata Yanıtlarını Daha Net Hale Getirme

Bu makale boyunca, kuşları temsil eden nesneleri almak için REST API uygulayan bir uygulamanın GitHub'da barındırılan kaynak kodunu kullanacağız. Bu makalede açıklanan özelliklere ve birkaç hata işleme senaryosu örneğine sahiptir. İşte bu uygulamada uygulanan uç noktaların bir özeti:

</tr>
GET /birds/{birdId} Bir kuş hakkında bilgi alır ve bulunamazsa bir istisna atar.
GET /birds/noexception/{birdId} Bu çağrı aynı zamanda bir kuş hakkında da bilgi alır, ancak kuşun bulunamaması durumunda bir istisna oluşturmaz.
POST /birds Bir kuş yaratır.

Spring framework MVC modülü, hata işlemeye yardımcı olacak bazı harika özelliklerle birlikte gelir. Ancak, istisnaları ele almak ve API istemcisine anlamlı yanıtlar döndürmek için bu özellikleri kullanmak geliştiriciye bırakılmıştır.

Aşağıdaki JSON nesnesiyle /birds uç noktasına bir HTTP POST yayınladığımızda varsayılan Spring Boot yanıtının bir örneğine bakalım; bu, bir tamsayı beklemesi gereken “mass” alanında “aaa” dizesine sahiptir:

 { "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }

Spring Boot varsayılan yanıtı, uygun hata işleme olmadan:

 { "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }

Pekala… yanıt mesajının bazı iyi alanları var, ancak istisnanın ne olduğuna çok fazla odaklanıyor. Bu arada, bu Spring DefaultErrorAttributes sınıfıdır. Zaman timestamp alanı, zaman damgasının hangi ölçü biriminde olduğu bilgisini bile taşımayan bir tam sayıdır. exception alanı yalnızca Java geliştiricileri için ilgi çekicidir ve mesaj API tüketicisini, kendileri için alakasız olan tüm uygulama ayrıntılarında kaybetmesine neden olur. . Peki ya hatanın kaynaklandığı istisnadan çıkarabileceğimiz daha fazla ayrıntı varsa? Öyleyse, API istemcilerimiz için hayatı kolaylaştırmak için bu istisnaları nasıl düzgün bir şekilde ele alacağımızı öğrenelim ve bunları daha güzel bir JSON temsiline dönüştürelim.

Java 8 tarih ve saat sınıflarını kullanacağımızdan, önce Jackson JSR310 dönüştürücüler için bir Maven bağımlılığı eklememiz gerekiyor. @JsonFormat ek açıklamasını kullanarak Java 8 tarih ve saat sınıflarını JSON temsiline dönüştürmeye özen gösterirler:

 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

Tamam, API hatalarını temsil edecek bir sınıf tanımlayalım. REST çağrıları sırasında meydana gelen hatalarla ilgili bilgileri tutmak için yeterli alana sahip ApiError adında bir sınıf oluşturacağız.

 class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
  • status özelliği, işlem çağrısı durumunu tutar. İstemci hatalarını belirtmek için 4xx'den veya sunucu hatalarını belirtmek için 5xx'ten herhangi bir şey olacaktır. Yaygın bir senaryo, örneğin istemci geçersiz bir e-posta adresi gibi yanlış biçimlendirilmiş bir alan gönderdiğinde, BAD_REQUEST anlamına gelen bir http kodu 400'dür.

  • Zaman timestamp özelliği, hatanın meydana geldiği tarih-saat örneğini tutar.

  • message özelliği, hata hakkında kullanıcı dostu bir mesaj tutar.

  • debugMessage özelliği, hatayı daha ayrıntılı olarak açıklayan bir sistem mesajını tutar.

  • subErrors özelliği, meydana gelen bir dizi alt hatayı tutar. Bu, tek bir çağrıda birden çok hatayı temsil etmek için kullanılır. Bir örnek, birden çok alanın doğrulamada başarısız olduğu doğrulama hataları olabilir. ApiSubError sınıfı, bunları kapsüllemek için kullanılır.

 abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }

O halde ApiValidationError , ApiSubError genişleten ve REST çağrısı sırasında karşılaşılan doğrulama sorunlarını ifade eden bir sınıftır.

Aşağıda, burada açıklanan iyileştirmeleri uyguladıktan sonra oluşturulan JSON yanıtlarının bazı örneklerini göreceksiniz, yalnızca bu makalenin sonunda ne elde edeceğimiz hakkında bir fikir edinmek için.

GET /birds/2 bitiş noktası çağrılırken bir varlık bulunmadığında döndürülen bir JSON örneği aşağıda verilmiştir:

 { "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }

Kuşun kütlesi için geçersiz bir değere sahip bir POST /birds çağrısı yayınlarken döndürülen başka bir JSON örneği:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }

Yay Önyükleme Hatası İşleme

İstisnaları işlemek için kullanılacak bazı Spring notlarını inceleyelim.

RestController , REST işlemlerini işleyen sınıflar için temel açıklamadır.

ExceptionHandler , işleyicilerin (Denetleyici işlemleri) yürütülmesi sırasında oluşturulan istisnaları ele almak için bir mekanizma sağlayan bir Yay ek açıklamasıdır. Bu açıklama, denetleyici sınıflarının yöntemlerinde kullanılıyorsa, yalnızca bu denetleyici içinde oluşturulan istisnaları işlemek için giriş noktası görevi görecektir. Toplamda, en yaygın yol @ControllerAdvice @ExceptionHandler sınıflarının yöntemlerinde kullanmaktır, böylece istisna işleme genel olarak veya bir denetleyici alt kümesine uygulanır.

ControllerAdvice , Spring 3.2'de tanıtılan bir açıklamadır ve adından da anlaşılacağı gibi, birden çok denetleyici için "Tavsiye"dir. Tek bir ExceptionHandler birden çok denetleyiciye uygulanmasını sağlamak için kullanılır. Bu şekilde, böyle bir istisnanın nasıl ele alınacağını tek bir yerde tanımlayabiliriz ve istisna bu ControllerAdvice tarafından kapsanan sınıflardan atıldığında bu işleyici çağrılır. Etkilenen denetleyicilerin alt kümesi, @ControllerAdvice üzerinde şu seçiciler kullanılarak tanımlanabilir: annotations() , basePackageClasses() ve basePackages() . Seçici sağlanmazsa, ControllerAdvice genel olarak tüm denetleyicilere uygulanır.

Böylece @ExceptionHandler ve @ControllerAdvice kullanarak, istisnaları ele almak ve bunları varsayılan Spring Boot hata işleme mekanizmasından daha iyi bir organizasyona sahip bir ApiError nesnesine sarmak için merkezi bir nokta tanımlayabileceğiz.

İstisnaları İşleme

Başarılı ve başarısız bir REST istemci çağrısıyla ne olduğunun temsili

Bir sonraki adım, istisnaları ele alacak sınıfı oluşturmaktır. Basit olması için, buna RestExceptionHandler ve Spring Boot'un ResponseEntityExceptionHandler uzanması gerekiyor. Spring MVC istisnalarının bazı temel işlemlerini zaten sağladığı için ResponseEntityExceptionHandler genişleteceğiz, bu nedenle mevcut istisnaları iyileştirirken yeni istisnalar için işleyiciler ekleyeceğiz.

ResponseEntityExceptionHandler'da İşlenen İstisnaları Geçersiz Kılma

ResponseEntityExceptionHandler kaynak koduna bir göz atarsanız, handle******() adında, handleHttpMessageNotReadable() veya handleHttpMessageNotWritable() ) gibi bir çok yöntem görürsünüz. Önce, HttpMessageNotReadableException istisnalarını işlemek için handleHttpMessageNotReadable() nasıl genişletebileceğimizi görelim. RestExceptionHandler handleHttpMessageNotReadable() yöntemini geçersiz kılmamız gerekiyor:

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }

Bir HttpMessageNotReadableException fırlatılması durumunda hata mesajının “Yanlış biçimlendirilmiş JSON isteği” olacağını ve hatanın ApiError nesnesi içinde kapsülleneceğini beyan ettik. Aşağıda, bu yeni yöntemin geçersiz kılındığı bir REST çağrısının yanıtını görebiliriz:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }

Özel İstisnaları İşleme

Şimdi Spring Boot'un ResponseEntityExceptionHandler içinde henüz bildirilmemiş bir istisnayı işleyen bir yöntemin nasıl oluşturulacağını göreceğiz.

Veritabanı çağrılarını işleyen bir Spring uygulaması için yaygın bir senaryo, bir havuz sınıfı kullanarak kimliğine göre bir kayıt bulmak için bir çağrıya sahip olmaktır. Ancak CrudRepository.findOne() yöntemine bakarsak, bir nesne bulunamazsa null döndürdüğünü görürüz. Bunun anlamı, eğer hizmetimiz sadece bu yöntemi çağırırsa ve doğrudan kontrolöre dönerse, kaynak bulunmasa bile bir HTTP kodu 200 (Tamam) alacağız. Aslında, uygun yaklaşım, HTTP/1.1 spesifikasyonunda belirtildiği gibi bir HTTP kodu 404 (BULUNAMADI) döndürmektir.

Bu durumu ele almak için EntityNotFoundException adında özel bir istisna oluşturacağız. Bu, özel olarak oluşturulmuş bir istisnadır ve nesne oluşturmayı kolaylaştıran bazı kurucular sağladığı için javax.persistence.EntityNotFoundException 'dan farklıdır ve javax.persistence istisnasını farklı şekilde ele almayı seçebilir.

Başarısız bir REST çağrısı örneği

Bununla birlikte, RestExceptionHandler sınıfımızda bu yeni oluşturulan EntityNotFoundException için bir ExceptionHandler oluşturalım. Bunu yapmak için, handleEntityNotFound() adlı bir yöntem oluşturun ve ona EntityNotFoundException.class sınıf nesnesini EntityNotFoundException.class @ExceptionHandler ile açıklama ekleyin. Bu, EntityNotFoundException her atıldığında, Spring'in işlemek için bu yöntemi çağırması gerektiğini bildirir. @ExceptionHandler ile bir yönteme açıklama eklerken, WebRequest , Locale ve burada açıklanan diğerleri gibi çok çeşitli otomatik olarak enjekte edilen parametreleri kabul edecektir. Bu handleEntityNotFound yöntemi için parametre olarak yalnızca EntityNotFoundException özel durumunu sağlayacağız.

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }

Harika! handleEntityNotFound() yönteminde, HTTP durum kodunu NOT_FOUND olarak ayarlıyoruz ve yeni istisna mesajını kullanıyoruz. GET /birds/2 uç noktasının yanıtı şu anda şöyle görünür:

 { "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }

Çözüm

Bu istisnaları ApiError nesnesiyle düzgün bir şekilde eşleştirebilmemiz ve API istemcilerinin ne olduğunu bilmesini sağlayan önemli bilgiler sağlayabilmemiz için istisna işlemenin kontrolünü ele almak önemlidir. Buradan sonraki adım, uygulama kodu içinde oluşturulan istisnalar için daha fazla işleyici yöntemi (@ExceptionHandler içerenler) oluşturmak olacaktır. GitHub kodunda MethodArgumentTypeMismatchException , ConstraintViolationException ve diğerleri gibi diğer bazı genel istisnalar için daha fazla örnek vardır.

İşte bu makalenin oluşturulmasına yardımcı olan bazı ek kaynaklar:

  • Baeldung - Yaylı REST için hata işleme

  • Spring Blog - Spring MVC'de istisna işleme