Spring Exception Handling Rehberi
6 min read

Spring Exception Handling Rehberi

Spring Exception Handling için @ResponseStatus, @ExceptionHandler ve @ControllerAdvice anotasyonlarını detaylıca inceliyoruz.
Spring Exception Handling Rehberi
Photo by Markus Spiske / Unsplash

Spring Web modülüyle gelen @ResponseStatus, @ExceptionHandler ve @ControllerAdvice anotasyonları sayesinde exception'ları handle ederek daha güçlü ve dayanaklı uygulamalar oluşturabilirsiniz. Bu yazımızda her birini nasıl kullanacağınızı detaylıca göstereceğiz.

Spring Web modülü, basit 'try-catch' blokları yerine tüm exception'ları handle edebilmek için bize gelişmiş bazı araçlar sağlar. Bu araçları kullanmak için kullanabileceğimiz anotasyonları görelim:

  • @ResponseStatus
  • @ExceptionHandler
  • @ControllerAdvice

Bu anotasyonlara geçmeden önce, Spring'in web controller'lar tarafından atılan exception'ları nasıl handle ettiğine bakalım.

1. Spring Web Varsayılan Exception Handling Mekanizması

Belirli bir ID'ye sahip ürünü bulamadığında getProduct(...) metodu tarafından NoSuchElementFoundException runtime exception'u atan ProductController adlı bir controller olduğunu varsayalım:

@RestController
@RequestMapping("/product")
public class ProductController {
  private final ProductService productService;
  //constructor omitted for brevity...
  
  @GetMapping("/{id}")
  public Response getProduct(@PathVariable String id){
    // this method throws a "NoSuchElementFoundException" exception
    return productService.getProduct(id);
  }
  
}

/product endpoint'i geçersiz bir ID ile çağırırsak, service bir NoSuchElementFoundException runtime exception fırlatır ve aşağıdaki yanıtı alırız:

{
  "timestamp": "2020-11-28T13:24:02.239+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/product/1"
}

İyi biçimlendirilmiş bir hata yanıtının yanı sıra, payload'ın bize yararlı bir bilgi vermediğini görebiliriz. Sorunun ne olduğunu söylememesinin yanı sıra, basit bir "ID si 1 olan öğe bulunamadı" gibi bir detay mesajı bile bulunmuyor.

Hata iletisi sorununu çözerek başlayalım.

Spring, yanıt payload'ına exception mesajını, exception sınıfını ve hatta stack trace'i bile ekleyebilmemizi şu konfigürasyon parametreleriyle bize sağlıyor:

server:
  error:
  include-message: always
  include-binding-errors: always
  include-stacktrace: on_trace_param
  include-exception: false

Application.yml dosyanıza yukarıdaki parametreleri eklediğinizd aynı hata mesajı şu şekilde görünmeye başlayacaktır.

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Item with id 1 not found",
  "path": "/product/1"
} 

include-stacktrace özelliğini on_trace_param olarak ayarladığımızı unutmayın, bu da yalnızca izleme parametresini URL'ee (?trace=true) eklersek, yanıt payload'ında bir stack trace alacağımız anlamına gelir:

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Item with id 1 not found",
  "trace": "io.reflectoring.exception.exception.NoSuchElementFoundException: Item with id 1 not found...", 
  "path": "/product/1"
} 

Uygulamamızın işleyişini ortaya çıkarabileceğinden, include-stacktrace parametresini production ortamda tutmamak daha güvenli olabilir.

Durum ve hata iletisi - 500 - sunucu kodumuzda bir sorun olduğunu gösterir, ancak istemci geçersiz bir kimlik sağladığından aslında bu hata bir istemci hatasıdır.

Mevcut durum kodumuz bunu doğru yansıtmıyor. Ne yazık ki, server.error yapılandırma özellikleriyle gidebileceğimiz en uzak yer burası, bu yüzden Spring Web modülünün sunduğu anotasyonlara bakmamız gerekecek.

2. @ResponseStatus

Adından da anlaşılacağı gibi, @ResponseStatus yanıtımızın HTTP durumunu değiştirmemize izin verir. Bu anotasyon aşağıdaki yerlerde uygulanabilir:

  • Exception sınıfının kendisinde
  • Metodlarda @ExceptionHandler anotasyonu ile birlikte
  • Sınıflarda @ControllerAdvice anotasyonu ile birlikte

Bu bölümde, sadece ilk duruma bakacağız.

Şimdi elimizdeki soruna geri dönelim, bu da hata yanıtlarımızın bize daha açıklayıcı bir durum kodu yerine her zaman HTTP durumu 500'ü vermesidir.

Bunu gidermek için Exception sınıfımıza @ResponseStatus anotasyonu ekleyebilir ve değer olarak istenen HTTP yanıt durumunu geçirebiliriz:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
  ...
}

Bu değişiklik, controller'ı geçersiz bir id çağırırsak çok daha iyi bir yanıt dönmesine neden olur:

{
  "timestamp": "2020-11-29T09:42:12.287+00:00",
  "status": 404,
  "error": "Not Found",
  "message": "Item with id 1 not found",
  "path": "/product/1"
} 

Aynı şeyi başarmanın başka bir yolu da ResponseStatusException sınıfını extend etmektir:

public class NoSuchElementFoundException extends ResponseStatusException {

  public NoSuchElementFoundException(String message){
    super(HttpStatus.NOT_FOUND, message);
  }

  @Override
  public HttpHeaders getResponseHeaders() {
      // return response headers
  }
}

GetResponseHeaders() metodunu override edebildiğimiz için, yanıt header'ını da manipüle etmek istediğimizde bu yaklaşım daha kullanışlı olur. Fakat istediğiniz şey sadece durum koduysa, anotasyon yeterli olacaktır.

@ResponseStatus ve server.error yapılandırma özellikleriyle birlikte header'i, yanıt kodunu ve payload'ı Spring'in varsayılanları doğrultusunda değiştirebildik.

Peki ya response payload yapısını özgürce manipüle etmek istiyorsanız?

Bir sonraki bölümde bunu nasıl başaracağımıza bakalım.

3. @ExceptionHandler

@ExceptionHandler anotasyonu, exception'ları işleme konusunda bize çok fazla esneklik sağlar. Bunu kullanabilmek için, controller'ın kendisinde veya @ControllerAdvice ile anotated edilmiş bir sınıfta bir metod oluşturmamız ve @ExceptionHandler anotasyonu eklememiz yeterlidir:

@RestController
@RequestMapping("/product")
public class ProductController { 
    
  private final ProductService productService;
  
  //constructor omitted for brevity...

  @GetMapping("/{id}")
  public Response getProduct(@PathVariable String id) {
    return productService.getProduct(id);
  }

  @ExceptionHandler(NoSuchElementFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ResponseEntity<String> handleNoSuchElementFoundException(
      NoSuchElementFoundException exception
  ) {
    return ResponseEntity
        .status(HttpStatus.NOT_FOUND)
        .body(exception.getMessage());
  }

}

Exception handler metodu, tanımlanan metodda işlemek için bir exception veya exception listesini argüman olarak alır. İşlememiz gereken exception'ı ve döndürmek istediğimiz durum kodunu tanımlamak için yönteme @ExceptionHandler ve @ResponseStatus anotasyonu ekliyoruz.

Bu anotasyonları kullanmak istemiyorsak, exceptionu metodun bir parametresi olarak tanımlamak da aynı şeyleri yapacaktır:

@ExceptionHandler
public ResponseEntity<String> handleNoSuchElementFoundException(
    NoSuchElementFoundException exception)

Metod imzasında zaten belirtmiş olsak da, anotasyonda exception sınıfından bahsetmek iyi bir fikir olabilir. Çünkü bize daha iyi bir okunabilirlik sağlar.

Ayrıca, ResponseEnity'ye geçirilen HTTP durumu öncelikli olacağından, handler metoda eklenen anotasyon @ResponseStatus(HttpStatus.NOT_FOUND) gerekli değildir, ancak yine de aynı okunabilirlik nedenleriyle tutabilirsiniz.

Exception parametresinin yanı sıra, parametre olarak HttpServletRequest, WebRequest veya HttpSession tiplerini de kullanabiliriz.

Benzer şekilde, handler metodları ResponseEntity, String ve hatta void gibi çeşitli dönüş tiplerini de destekler.

@ExceptionHandler javadoc sayfasında daha fazla input ve return tipi bulabilirsiniz.

Exception handler metodumuzda hem input parametreleri hem de return tiplerinde kullanabileceğimiz birçok farklı seçenekle, hata yanıtının tam kontrolü bizdedir.

Peki @ExceptionHandler anotasyonunu tüm controller'lara tek tek eklemek yerine tek bir sınıfta toplayarak hem duplicate kodu engellemek, hem de okunabilirliği artırmak istersek ne yapmalıyız? Cevabı bir sonraki başlıkta.

4. @ControllerAdvice

Controller advice sınıfları, uygulamamızdaki birden fazla veya tüm controller için exception handler yazmamıza izin verir.

'Advice' terimi, mevcut metodların etrafına cross-cutting kodu ("advice" olarak adlandırılır) enjekte etmemizi sağlayan Aspect-Oriented Programlama (AOP)'dan gelir. Bir controller advice, exception'ları handle etmek için controller metodlarının dönüş değerlerini yakalamamıza ve değiştirmemize olanak tanır.

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(ItemNotFoundException.class)
  public ResponseEntity<Object> handleItemNotFoundException(
      ItemNotFoundException itemNotFoundException, 
      WebRequest request
  ){
      return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(itemNotFoundException.getMessage());  
  }

  @ExceptionHandler(RuntimeException.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseEntity<Object> handleAllUncaughtException(
      RuntimeException exception, 
      WebRequest request
  ){
      return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(exception.getMessage()); 
  }
  
}

Tekrar hatırlatmak gerekirse; ResponseEntity ile belirlediğimiz return durumu daha önceliklidir fakar okunabilirlik açısından @ResponseStatus anotasyonunu da tutabilirsiniz.

Buradaki diğer bir önemli nokta ise, bu handler'ların sadece ProductController tarafından değil, uygulamadaki tüm controller'lar tarafından atılan exceptionlar'ı işliyor olmasıdır. Eğer buna bir sınırlama getirmek istiyorsanız iki farklı yöntemle yapabilirsiniz:

  • @ControllerAdvice("com.kerteriz.controller"): Anotasyonun değerine veya basePackages parametresine bir paket adı veya paket adları listesi geçirebiliriz. Böylece, controller advice yalnızca bu paketin controller'larının exception'larını işler.
  • @ControllerAdvice(annotations = Advised.class): Yeni bir anotasyon oluşturarak, bu oluşturduğunuz anotasyonu parametre olarak verebilirsiniz. Örneğin burada yalnızca @Advised anotasyonu ile işaretlenmiş controller'lar controller advise tarafından işlenecektir.

ResponseEntityExceptionHandler, controller advice sınıfları için uygun bir base sınıftır. handleMethodArgumentNotValid() ve handleExceptionInternal() metodlarını override ederek Spring dahilindeki exceptionları da handle edebilirsiniz. Eğer bu sınıfı extend etmezseniz, tüm exception'lar DefaultHandlerExceptionResolver sınıfına yönlendirilir ve bu da bir ModelAndView nesnesi döndürür. Fakat REST servisleriniz için ModelAndView objesi yerine bir ResponseEntity objesi kullanacağınız için bu sınıfı extend etmelisiniz.

Pratik olarak REST uygulamalarınızda daha kısa ve kullanışlı olan @RestControllerAdvice anotasyonunu kullanabilirsiniz. Aynı örnek üzerinde görürsek:

@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(ItemNotFoundException.class)
  public ResponseEntity<Object> handleItemNotFoundException(
      ItemNotFoundException itemNotFoundException
  ){
      return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(itemNotFoundException.getMessage());  
  }

  @ExceptionHandler(RuntimeException.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseEntity<Object> handleAllUncaughtException(
      RuntimeException exception
  ){
      return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(exception.getMessage()); 
  }
  
}

Kısaca;

  • @ControllerAdvice - MVC ve REST web servislerinde kullanabiliriz
  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody -  REST web servislerinde kullanabiliriz

5. Spring Exception'ları Nasıl İşler?

Spring'te exceptionları handle etmek için kullanabileceğimiz mekanizmaları tanıttığımıza göre, Spring'in bunları nasıl handle ettiğini ve bir mekanizmanın diğerine göre ne zaman önceliklendirildiğini kısaca anlayalım.

Kendi exception handler'ımızı oluşturmadıysak, Spring'in varsayılan exceptionhandle etme işlemini aşağıdaki akış şemasında görebilirsiniz:

Spring exception handle etme akışı

6. Özet

Bu makalede, Spring'in exception'ları client'lar için kullanıcı dostu bir çıktıya nasıl çevirdiğini ve ayrıca onları arzu ettiğimiz şekilde nasıl manipüle etmemizi sağlayan yapılandırmaları ve anotasyonları gördük. Bir sonraki yazımızda görüşmek üzere..