Spring MVC’de İstisna Yönetimi: @ControllerAdvice, @ExceptionHandler ve HandlerExceptionResolver
Giriş
Modern web uygulamaları, kullanıcılarla etkileşim kurarken ve karmaşık iş mantığını yürütürken çeşitli hata ve istisna durumlarıyla karşılaşabilir. Bu durumlar, beklenmedik veri girişlerinden, ağ sorunlarına, veritabanı bağlantı hatalarına veya iş kurallarının ihlallerine kadar geniş bir yelpazeyi kapsar. Bir uygulamanın sağlamlığı ve kullanıcı deneyimi, bu istisnaların ne kadar etkili bir şekilde yönetildiğine bağlıdır. Spring MVC, bu tür senaryoları ele almak için güçlü ve esnek mekanizmalar sunar. Doğru istisna yönetimi stratejileri uygulamak, uygulamanızın daha güvenilir, bakımı daha kolay ve kullanıcı dostu olmasını sağlar.
Bu makalede, Spring MVC ekosisteminde istisna yönetiminin temel taşları olan @ExceptionHandler anotasyonu, @ControllerAdvice anotasyonu ve HandlerExceptionResolver arayüzünü derinlemesine inceleyeceğiz. Her bir yaklaşımın ne zaman kullanılacağını, avantajlarını ve dezavantajlarını örnek kodlarla açıklayacak, böylece geliştiricilerin uygulamalarında en uygun stratejiyi seçmelerine yardımcı olacağız. Amacımız, Spring MVC’deki istisna yönetiminin farklı katmanlarını ve bu katmanların nasıl bir araya gelerek kapsamlı bir hata işleme çözümü sunduğunu net bir şekilde ortaya koymaktır.
Temel İstisna Yönetimi Kavramları
İstisna yönetimine dalmadan önce, Java’daki temel istisna kavramlarını ve web uygulamalarında istisna yönetiminin neden bu kadar kritik olduğunu anlamak önemlidir.
Java’da İstisnalar ve Hatalar
Java’da istisnalar (exceptions) ve hatalar (errors), programın normal akışını bozan olayları temsil eder. Throwable sınıfı, tüm istisna ve hata sınıflarının süper sınıfıdır.
* Hatalar (Errors): Genellikle uygulamanın kontrolü dışındaki ciddi sorunları (örn: OutOfMemoryError, StackOverflowError) temsil eder. Bu tür durumlarda, uygulamayı kurtarmak genellikle mümkün değildir ve JVM’in kapanması gerekebilir. Geliştiricilerin doğrudan yakalayıp işlemeye çalıştığı durumlar değildir.
* İstisnalar (Exceptions): Uygulamanın çalışması sırasında meydana gelen ancak genellikle yakalanıp işlenebilecek sorunları temsil eder. İstisnalar iki ana kategoriye ayrılır:
* Kontrol Edilen İstisnalar (Checked Exceptions): Exception sınıfından türeyen ancak RuntimeException sınıfından türemeyen istisnalardır. Derleme zamanında kontrol edilirler ve metot imzalarında throws anahtar kelimesiyle belirtilmeli veya try-catch bloğu ile yakalanmalıdır. Örnek: IOException, SQLException.
* Kontrol Edilmeyen İstisnalar (Unchecked Exceptions) veya Çalışma Zamanı İstisnaları (Runtime Exceptions): RuntimeException sınıfından türeyen istisnalardır. Derleme zamanında kontrol edilmezler ve metot imzasında belirtilmesi zorunlu değildir. Genellikle programcı hatalarını (örn: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException) veya kurtarılamayacak mantıksal hataları gösterirler. Spring MVC genellikle bu tür istisnalarla daha sık karşılaşır.
Web Uygulamalarında İstisna Yönetiminin Önemi
Web uygulamalarında istisna yönetimi sadece teknik bir gereklilik değil, aynı zamanda kullanıcı deneyimi, güvenlik ve uygulama bakımı açısından da hayati öneme sahiptir.
* Kullanıcı Deneyimi: Beklenmedik bir hata durumunda, kullanıcıya anlamsız bir yığın izi (stack trace) göstermek yerine, anlaşılır ve yardımcı bir hata mesajı sunmak, uygulamanın profesyonel algısını artırır. Hatta bazen kullanıcıyı hatanın nasıl düzeltileceği konusunda yönlendirmek mümkündür.
* Güvenlik: Yığın izleri veya ayrıntılı hata mesajları, saldırganlara uygulamanızın iç yapısı, kullanılan kütüphaneler veya veritabanı şeması hakkında hassas bilgiler sağlayabilir. Doğru istisna yönetimi, bu tür bilgilerin açığa çıkmasını engeller.
* Bakım Kolaylığı: Merkezi bir istisna yönetim mekanizması, hataların tek bir yerden ele alınmasını, loglanmasını ve izlenmesini sağlar. Bu, sorun giderme ve hata ayıklama süreçlerini önemli ölçüde hızlandırır.
* API Tutarlılığı: RESTful API’lar geliştirirken, hata yanıtlarının tutarlı bir formatta (örn: JSON) ve anlamlı HTTP durum kodlarıyla (örn: 400 Bad Request, 404 Not Found, 500 Internal Server Error) dönmesi, API’yi kullanan istemcilerin (frontend uygulamaları, mobil uygulamalar) hataları kolayca işlemesini sağlar.
Spring MVC’de İstisna Yönetimine Genel Bakış
Spring MVC, istisna yönetimi için çeşitli seviyelerde ve esnekliklerde mekanizmalar sunar. Bu mekanizmalar, basit, controller’a özgü hata işleminden, tüm uygulama genelinde geçerli olacak karmaşık ve merkezi çözümlere kadar değişir. Spring’in bu konudaki felsefesi, geliştiricilere ihtiyaçlarına en uygun aracı sağlamaktır.
* Controller Seviyesi: @ExceptionHandler anotasyonu ile belirli bir controller içindeki metotlar tarafından fırlatılan istisnaları yakalamak.
* Global Seviye: @ControllerAdvice anotasyonu ile @ExceptionHandler‘ı birleştirerek tüm uygulama genelindeki controller’lar için merkezi bir istisna yönetim katmanı oluşturmak.
* Düşük Seviye/Programatik: HandlerExceptionResolver arayüzünü implemente ederek Spring’in istisna çözümleme mekanizmasına tam kontrolle müdahale etmek. Bu, Spring’in kendi iç istisna çözücülerinin (örn: DefaultHandlerExceptionResolver, SimpleMappingExceptionResolver) temelidir.
Bu yaklaşımların her birini detaylı olarak inceleyelim.
@ExceptionHandler Anotasyonu
@ExceptionHandler anotasyonu, Spring MVC’de istisna yönetiminin en temel ve doğrudan yoludur. Belirli bir controller içinde fırlatılan istisnaları yakalamak ve işlemek için kullanılır.
Controller Seviyesinde İstisna Yönetimi
Bir Controller sınıfı içinde bir metodu @ExceptionHandler ile işaretlediğinizde, bu metot sadece o controller içindeki diğer metotlar tarafından fırlatılan belirtilen istisna türlerini yakalar. Bu, belirli bir controller’ın kendi özel hata işleme mantığına sahip olması gerektiğinde kullanışlıdır.
Nasıl Çalışır:
1. Bir client bir HTTP isteği gönderir.
2. Spring MVC, isteği ilgili controller metoduna yönlendirir.
3. Controller metodu çalışırken bir istisna fırlatır.
4. Spring, aynı controller içinde bu istisna türünü veya onun bir süper sınıfını yakalamak için @ExceptionHandler ile işaretlenmiş bir metot olup olmadığını kontrol eder.
5. Eğer uygun bir metot bulunursa, Spring bu metodu çağırır ve istisnayı ona iletir.
6. @ExceptionHandler metodu, istisnayı işler ve uygun bir yanıt (örn: hata mesajı, HTTP durum kodu) döndürür.
Avantajları:
* Basitlik: Belirli bir controller’a özgü istisnaları yönetmek için kolay ve anlaşılır bir yol sunar.
* Özgünlük: Her controller, kendi özel istisna işleme mantığına sahip olabilir.
Dezavantajları:
* Tekrar: Eğer birden fazla controller aynı istisna türünü aynı şekilde ele alması gerekiyorsa, @ExceptionHandler kodunu her controller’a kopyalamak zorunda kalırsınız. Bu, kod tekrarına (DRY – Don’t Repeat Yourself prensibinin ihlali) yol açar ve bakımı zorlaştırır.
* Genelleştirilemez: Uygulama genelindeki istisnaları tek bir noktadan yönetmek için uygun değildir.
Birden Fazla İstisna Türünü Yakalama
Bir @ExceptionHandler metodu, tek bir istisna türünü veya bir istisna dizisini yakalayabilir. Bu, value veya exception özniteliği kullanılarak belirtilir.
@ExceptionHandler({ MyCustomException.class, AnotherCustomException.class })
public ResponseEntity handleMultipleExceptions(Exception ex) {
// İstisnayı işle ve yanıt döndür
return new ResponseEntity<>("Birden fazla istisna türü yakalandı: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
}
Örnek Kod
Aşağıdaki örnek, bir ProductController içinde ProductNotFoundException istisnasını nasıl yakalayacağınızı gösterir:
package com.example.demo.controller;
import com.example.demo.exception.ProductNotFoundException;
import com.example.demo.model.Product;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/products")
public class ProductController {
private List products = new ArrayList<>();
public ProductController() {
products.add(new Product(1L, "Laptop", 1200.0));
products.add(new Product(2L, "Mouse", 25.0));
}
@GetMapping("/{id}")
public ResponseEntity getProductById(@PathVariable Long id) {
Optional product = products.stream()
.filter(p -> p.getId().equals(id))
.findFirst();
if (product.isPresent()) {
return ResponseEntity.ok(product.get());
} else {
throw new ProductNotFoundException("ID " + id + " ile ürün bulunamadı.");
}
}
// Bu controller'a özgü ProductNotFoundException'ı yakalayan metot
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity handleProductNotFoundException(ProductNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
// Genel bir istisnayı yakalayan metot (daha spesifik olanlar önce yakalanır)
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(Exception ex) {
return new ResponseEntity<>("Bir hata oluştu: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Product.java
package com.example.demo.model;
public class Product {
private Long id;
private String name;
private double price;
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
// ProductNotFoundException.java
package com.example.demo.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
Yukarıdaki örnekte, /products/3 adresine yapılan bir istek, ProductNotFoundException fırlatacak ve bu istisna handleProductNotFoundException metodu tarafından yakalanarak 404 NOT FOUND durum koduyla bir yanıt dönecektir.
@ControllerAdvice Anotasyonu
@ExceptionHandler anotasyonunun controller seviyesindeki sınırlamalarını aşmak ve uygulama genelinde merkezi bir istisna yönetim mekanizması oluşturmak için Spring, @ControllerAdvice anotasyonunu sunar.
Global İstisna Yönetimi Çözümü
@ControllerAdvice anotasyonu, Spring’in bileşen taraması sırasında keşfedilen ve tüm uygulamadaki controller’lar için ortak özellikler (istisna yönetimi, veri bağlama, model öznitelikleri) tanımlayan bir sınıfı işaretlemek için kullanılır. Özellikle istisna yönetimi bağlamında, @ControllerAdvice ile işaretlenmiş bir sınıf içinde tanımlanan @ExceptionHandler metotları, tüm uygulama genelindeki controller’lar tarafından fırlatılan istisnaları yakalayabilir.
Nasıl Çalışır:
1. Bir @ControllerAdvice sınıfı oluşturulur.
2. Bu sınıfın içine, bir controller’daki gibi @ExceptionHandler ile işaretlenmiş metotlar tanımlanır.
3. Uygulamanın herhangi bir yerindeki bir controller metodu bir istisna fırlattığında, Spring önce o controller içindeki @ExceptionHandler‘ları arar.
4. Eğer controller içinde uygun bir işleyici bulunamazsa, Spring @ControllerAdvice ile işaretlenmiş sınıflardaki @ExceptionHandler metotlarını arar.
5. Uygun bir metot bulunursa, istisna o metot tarafından işlenir.
Avantajları:
* Merkezi Yönetim (DRY): Tüm uygulama genelindeki istisnaları tek bir yerden yönetmenizi sağlar, kod tekrarını önler.
* Temiz Kod: Controller’ların iş mantığı ve istisna işleme mantığı ayrı tutularak daha temiz ve okunaklı bir kod yapısı elde edilir.
* Tutarlılık: Tüm hata yanıtlarının aynı formatta ve HTTP durum kodlarıyla dönmesini sağlayarak API tutarlılığını artırır.
* Esneklik: Belirli istisna türleri için özel yanıtlar veya loglama mekanizmaları tanımlamak kolaydır.
@ExceptionHandler ile Birlikte Kullanımı
@ControllerAdvice tek başına istisna yönetimi yapmaz; @ExceptionHandler anotasyonlu metotları barındırmak için bir konteyner görevi görür. Bu, bir GlobalExceptionHandler sınıfı oluşturup içine çeşitli @ExceptionHandler metotları ekleyerek yapılır.
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity handleProductNotFoundException(ProductNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity
// ErrorDetails.java - Hata yanıtı için model
package com.example.demo.exception;
import java.time.LocalDateTime;
public class ErrorDetails {
private LocalDateTime timestamp;
private String message;
private String details;
public ErrorDetails(LocalDateTime timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
// Getters
public LocalDateTime getTimestamp() { return timestamp; }
public String getMessage() { return message; }
public String getDetails() { return details; }
}
Bu GlobalExceptionHandler sınıfı, uygulamanızdaki herhangi bir controller’dan fırlatılan ProductNotFoundException, IllegalArgumentException, MethodArgumentNotValidException ve diğer tüm Exception türlerini merkezi olarak ele alacaktır.
Hedef Kapsamını Belirleme (@ControllerAdvice’ın Özellikleri)
@ControllerAdvice anotasyonu, hangi controller’lar için geçerli olacağını belirlemek üzere çeşitli öznitelikler sunar:
* annotations(): Sadece belirtilen anotasyonlara sahip controller’lar için geçerli olur. Örneğin, @ControllerAdvice(annotations = RestController.class) sadece @RestController ile işaretlenmiş controller’lar için geçerli olacaktır.
* basePackages() (veya value()): Sadece belirtilen paketler içindeki controller’lar için geçerli olur. Örneğin, @ControllerAdvice(basePackages = "com.example.demo.controller") sadece bu paketteki controller’lar için istisnaları işler.
* assignableTypes(): Sadece belirtilen türlerden türeyen veya bu türleri implemente eden controller’lar için geçerli olur.
Bu özellikler, uygulamanızda farklı modüller veya bağlamlar için farklı istisna işleme stratejileri uygulamak istediğinizde oldukça kullanışlıdır. Çoğu durumda, tüm uygulama genelinde geçerli olması için herhangi bir öznitelik belirtilmeden @ControllerAdvice kullanılması yeterlidir.
HandlerExceptionResolver Arayüzü
Spring MVC’deki @ExceptionHandler ve @ControllerAdvice anotasyonları, HandlerExceptionResolver arayüzünün üzerine inşa edilmiş yüksek seviyeli soyutlamalardır. Bu arayüz, Spring’in istisna yönetiminin temelini oluşturur ve Spring MVC’nin istisnaları nasıl işlediğine dair en düşük seviyeli kontrolü sağlar.
Spring’in Temel İstisna Çözümleme Mekanizması
Bir istisna fırlatıldığında, Spring’in DispatcherServlet‘i bu istisnayı yakalar ve kayıtlı tüm HandlerExceptionResolver‘ları sırayla çağırarak istisnayı çözmeye çalışır. Her çözücü, istisnayı işleyip bir ModelAndView döndürebilir veya istisnayı işlemeyi reddederek null döndürebilir, bu durumda bir sonraki çözücüye geçilir.
Spring, varsayılan olarak birkaç HandlerExceptionResolver implementasyonu sağlar:
* DefaultHandlerExceptionResolver: Spring MVC’nin kendisi tarafından fırlatılan birçok standart istisnayı (örn: HttpRequestMethodNotSupportedException, MissingServletRequestParameterException, TypeMismatchException) ele alır ve uygun HTTP durum kodlarına dönüştürür.
* ExceptionHandlerExceptionResolver: Bu çözücü, @ExceptionHandler ve @ControllerAdvice anotasyonlarını işleyen mekanizmadır. Yani, @ControllerAdvice ve @ExceptionHandler kullanırken aslında bu ExceptionHandlerExceptionResolver devreye girer.
* ResponseStatusExceptionResolver: @ResponseStatus anotasyonuyla işaretlenmiş istisnaları işler. Bir istisna sınıfını @ResponseStatus(HttpStatus.NOT_FOUND) gibi işaretlediğinizde, bu çözücü devreye girer.
* SimpleMappingExceptionResolver: Bir istisna türünü belirli bir hata görünümüne (view) eşlemek için kullanılabilen yapılandırılabilir bir çözücüdür. Genellikle geleneksel MVC uygulamalarında hata sayfalarını göstermek için kullanılır.
Özel HandlerExceptionResolver Uygulamaları
Çoğu durumda, @ControllerAdvice ve @ExceptionHandler kombinasyonu yeterli olacaktır. Ancak, çok özel veya karmaşık istisna işleme gereksinimleriniz olduğunda, kendi HandlerExceptionResolver implementasyonunuzu yazmanız gerekebilir. Bu, aşağıdaki senaryolarda faydalı olabilir:
* Daha düşük seviyeli kontrol: Spring’in istisna işleme akışına tam olarak müdahale etmek istediğinizde.
* Harici sistemlerle entegrasyon: İstisnaları özel bir loglama sistemine, izleme aracına veya hata bildirim servisine göndermek istediğinizde.
* Farklı görünüm (view) çözümleri: İstisna türüne göre farklı JSP sayfaları, Thymeleaf şablonları veya diğer görünüm teknolojileri döndürmeniz gerektiğinde.
* Çoklu istisna yönetimi stratejileri: Uygulamanızın farklı bölümleri için tamamen farklı istisna işleme mantığına sahip olmak istediğinizde.
HandlerExceptionResolver’ın Çalışma Prensibi
HandlerExceptionResolver arayüzü tek bir metot tanımlar:
public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex);
}
* request: İstek nesnesi.
* response: Yanıt nesnesi.
* handler: İstisnayı fırlatan işleyici (genellikle controller metodu).
* ex: Fırlatılan istisna.
Bu metot, istisnayı işleyip bir ModelAndView döndürebilir. Eğer istisnayı başarıyla işler ve bir görünüm döndürmek istiyorsanız, bir ModelAndView nesnesi döndürün. Eğer istisnayı işlemeyi reddediyorsanız veya başka bir çözücünün işlemesini istiyorsanız null döndürün.
Örnek Kod
Aşağıda, özel bir HandlerExceptionResolver örneği verilmiştir. Bu örnek, bir istisna fırlatıldığında özel bir hata sayfasını (veya REST API için JSON yanıtını) döndürmek yerine, sadece loglama yapıp Spring’in diğer çözücülerinin devreye girmesine izin verebilir veya tamamen farklı bir yanıt üretebilir.
package com.example.demo.resolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import java.util.HashMap;
import java.util.Map;
@Component
public class CustomExceptionHandlerResolver implements HandlerExceptionResolver {
private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandlerResolver.class);
@Override
public ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
logger.error("CustomExceptionHandlerResolver tarafından yakalanan istisna: " + ex.getMessage(), ex);
// Eğer bu bir REST API isteğiyse, JSON yanıtı döndürebiliriz
if (request.getHeader("Accept") != null && request.getHeader("Accept").contains("application/json")) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500 Internal Server Error
MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
Map model = new HashMap<>();
model.put("error", "Özel Resolver Hatası");
model.put("message", "Beklenmeyen bir hata oluştu: " + ex.getMessage());
model.put("timestamp", System.currentTimeMillis());
return new ModelAndView(jsonView, model);
} else {
// Web uygulamaları için hata sayfasına yönlendirebiliriz
// response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// return new ModelAndView("error-page"); // error-page.html veya .jsp
// İstisnayı başka bir çözücüye bırakmak için null döndür
return null;
}
}
}
Bu özel çözücüyü etkinleştirmek için, Spring yapılandırmanıza eklemeniz gerekebilir, ancak @Component anotasyonu sayesinde Spring Boot uygulamalarında otomatik olarak algılanıp kaydedilecektir. Bu örnek, istisnayı loglarken aynı zamanda isteğin Accept başlığına göre farklı yanıtlar döndürme potansiyelini göstermektedir.
Farklı İstisna Yönetimi Yaklaşımlarının Karşılaştırılması
Spring MVC’de istisna yönetimi için üç ana yaklaşımı inceledik. Şimdi bu yaklaşımları ne zaman ve neden kullanacağımıza dair bir karşılaştırma yapalım.
@ExceptionHandler (Controller Seviyesi)
* Avantajları:
* Belirli bir controller’a özgü, basit istisna senaryoları için hızlı ve kolay çözüm.
* Controller’ın kendi içinde kalması nedeniyle yerel bağlamda anlaşılması kolaydır.
* Dezavantajları:
* Kod tekrarına yol açar; aynı istisna türü farklı controller’larda aynı şekilde işlenecekse her birine ayrı ayrı yazılmalıdır.
* Uygulama genelinde tutarlı bir hata yanıtı formatı sağlamak zordur.
* Ne Zaman Kullanılır: Sadece tek bir controller’da ortaya çıkan ve başka hiçbir yerde işlenmesi gerekmeyen çok özel bir istisna türü olduğunda veya hızlı prototipleme yaparken. Nadiren tercih edilmelidir.
@ControllerAdvice + @ExceptionHandler (Global)
* Avantajları:
* Merkezi İstisna Yönetimi: Tüm uygulama genelindeki istisnaları tek bir yerden yönetir. Bu, kod tekrarını ortadan kaldırır ve bakımı kolaylaştırır.
* Tutarlılık: Tüm API’ler için standart ve tutarlı bir hata yanıtı formatı (örn: JSON) sağlamayı kolaylaştırır.
* Temiz Controller’lar: Controller’ların sadece iş mantığına odaklanmasını sağlar, istisna işleme detaylarını onlardan ayırır.
* Esneklik: Belirli istisna türleri için özelleştirilmiş hata mesajları ve HTTP durum kodları döndürmek kolaydır.
* Dezavantajları:
* Çok küçük, tek controller’lı uygulamalar için biraz fazla soyutlama gibi görünebilir (ancak yine de önerilir).
* Belirli bir controller’ın istisnayı kendi özel yöntemleriyle ele almasını engellemez, ancak @ControllerAdvice‘daki işleyiciler, controller’daki işleyicilerden sonra devreye girer.
* Ne Zaman Kullanılır: Modern Spring Boot ve RESTful API geliştirmenin standart ve en çok önerilen yoludur. Uygulamanızın boyutu ne olursa olsun, tutarlı ve sürdürülebilir bir istisna yönetimi için bu yaklaşım tercih edilmelidir.
HandlerExceptionResolver (Daha Düşük Seviye)
* Avantajları:
* Tam Kontrol: Spring’in istisna işleme akışına en düşük seviyede ve en kapsamlı şekilde müdahale etme olanağı sunar.
* Gelişmiş Senaryolar: Özel loglama, harici sistem entegrasyonları, istisna türüne göre farklı görünüm döndürme gibi karmaşık ve özelleştirilmiş senaryolar için idealdir.
* Spring’in varsayılan istisna işleme mekanizmalarını genişletmek veya değiştirmek için kullanılır.
* Dezavantajları:
* Karmaşıklık: @ControllerAdvice‘a göre daha fazla kod yazmayı ve Spring’in iç işleyişi hakkında daha derin bilgi sahibi olmayı gerektirir.
* Genellikle Gerek Duyulmaz: Çoğu uygulama için @ControllerAdvice yeterli olduğundan, bu seviyeye inmek nadiren gereklidir.
* Ne Zaman Kullanılır: Standart Spring mekanizmalarının yetersiz kaldığı, çok özel ve düşük seviyeli istisna işleme mantığına ihtiyaç duyulduğu durumlarda. Örneğin, farklı istisna türleri için tamamen farklı hata sayfaları göstermeniz veya istisnaları özel bir şekilde harici bir sisteme raporlamanız gerektiğinde.
Özetle, çoğu geliştirici için @ControllerAdvice ile @ExceptionHandler kombinasyonu, Spring MVC’de istisna yönetiminin altın standardıdır. Bu yaklaşım, temiz, merkezi ve sürdürülebilir bir çözüm sunar. @ExceptionHandler tek başına çok sınırlıdır ve HandlerExceptionResolver ise genellikle aşırıya kaçan bir karmaşıklık sunar.
İstisna Yönetiminde En İyi Uygulamalar
Etkili bir istisna yönetimi stratejisi, sadece teknik implementasyonlardan ibaret değildir. Uygulamanın genel kalitesini artıran bazı en iyi uygulamalar da vardır.
Anlamlı Hata Mesajları ve Kodları
* Kullanıcı Dostu Mesajlar: Son kullanıcılara gösterilen hata mesajları teknik jargon içermemeli, anlaşılır ve eyleme dönüştürülebilir olmalıdır. Örneğin, “Veritabanı bağlantısı kesildi” yerine “Şu anda işleminizi gerçekleştiremiyoruz, lütfen daha sonra tekrar deneyin.”
* Geliştirici Dostu Mesajlar: Loglara veya geliştiriciye dönen hata mesajları, sorunun kök nedenini anlamak için yeterli detayı içermelidir (örn: istisna türü, fırlatıldığı yer, ilgili parametreler).
* Tutarlı Hata Kodları: RESTful API’lar için, hata yanıtlarında standart HTTP durum kodlarının (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 500 Internal Server Error vb.) yanı sıra, uygulamanıza özgü dahili hata kodları da sağlayabilirsiniz. Bu, istemci uygulamaların belirli hata durumlarını programatik olarak tanımasını ve işlemesini sağlar.
* JSON API Problem Details: RFC 7807 (Problem Details for HTTP APIs) standardı, API’lar için tutarlı ve makine tarafından okunabilir hata yanıtları oluşturmak için harika bir yaklaşımdır. Spring Boot, bu standardı desteklemek için araçlar sunar.
Loglama
* Tüm istisnaları uygun log seviyesinde (örn: ERROR veya WARN) loglayın.
* ex.printStackTrace() yerine bir loglama çerçevesi (örn: SLF4J/Logback) kullanın.
* Log mesajlarına, hatanın bağlamını anlamak için gerekli olabilecek ek bilgileri (kullanıcı ID’si, istek URL’si, parametreler vb.) ekleyin.
* Yığın izlerini (stack traces) loglamak, hatanın nerede meydana geldiğini hızlıca tespit etmek için kritik öneme sahiptir.
Güvenlik
* Hata mesajlarında veya yığın izlerinde hassas bilgileri (veritabanı kimlik bilgileri, API anahtarları, kullanıcı şifreleri, sunucu yolları vb.) asla açığa çıkarmayın.
* Üretim ortamlarında ayrıntılı hata mesajlarını ve yığın izlerini doğrudan kullanıcıya göstermeyin. Bunun yerine genel bir hata mesajı ve bir referans kodu (bu kodla loglarda hatayı bulabilirsiniz) gösterin.
Uluslararasılaşma (i18n)
* Eğer uygulamanız birden fazla dili destekliyorsa, hata mesajlarınızı da uluslararasılaştırmanız gerekir. Spring’in MessageSource arayüzü bu konuda size yardımcı olabilir.
Test Edilebilirlik
* İstisna senaryolarınızı test etmek, uygulamanızın beklenmedik durumlarda doğru şekilde davrandığından emin olmak için çok önemlidir.
* Birim testleri ve entegrasyon testleri ile controller’larınızın ve @ControllerAdvice sınıflarınızın farklı istisnaları doğru şekilde ele aldığını doğrulayın.
Sonuç
Spring MVC’de istisna yönetimi, modern ve sağlam bir web uygulamasının temel bir bileşenidir. @ExceptionHandler, @ControllerAdvice ve HandlerExceptionResolver gibi güçlü araçlar sayesinde Spring, geliştiricilere istisnaları farklı seviyelerde ve esnekliklerde ele alma imkanı sunar.
Makale boyunca, controller seviyesinde @ExceptionHandler‘ın basit ama sınırlı kullanımından, tüm uygulama genelinde merkezi ve tutarlı bir çözüm sunan @ControllerAdvice + @ExceptionHandler kombinasyonuna ve daha düşük seviyeli, tam kontrol sağlayan HandlerExceptionResolver arayüzüne kadar çeşitli yaklaşımları inceledik. Çoğu senaryo için @ControllerAdvice‘ın en dengeli ve önerilen çözüm olduğunu vurguladık.
Unutulmamalıdır ki, istisna yönetimi sadece teknik bir implementasyon meselesi değil, aynı zamanda kullanıcı deneyimi, güvenlik ve uygulama bakımı gibi alanlarda da kritik öneme sahiptir. Anlamlı hata mesajları, doğru loglama, güvenlik bilinci ve uluslararasılaşma gibi en iyi uygulamaları benimsemek, uygulamanızın kalitesini önemli ölçüde artıracaktır. Bu araçları ve prensipleri doğru bir şekilde uygulayarak, daha güvenilir, kullanıcı dostu ve sürdürülebilir Spring MVC uygulamaları geliştirebilirsiniz.