Spring Boot Generic Exception Handler
Spring Boot ile exception haneler mekanizmasına geçmeden öncelikle Java’daki exception yapılarını incelememiz gerekir.
Aşağıdaki görsel bu yapıyı çok iyi görstermektedir.
Görselde olduğu üzere Exception ve Error class’ları mevcut ve Exception’lar Checked ve Unchecked olarak ikiye ayrılmaktadır.
Runtime Exception, adı üstünde çalışma zamanında meydana gelebilecek exception’lardır ve dolayısıyla oluşulabilecek bu exception’ı yakalamak zorunda değilsinizdir.
Örnek olarak öncelikle UserNotFoundException diye bir exception üreteceğim ve bunu Exception class’ından kalıtacağım.
1 2 3 4 5 6 7 |
public class UserNotFoundException extends Exception{ } |
Şimdi de bir user bulunamadığında bu hatayı vermek istediğimiz bir durum oluşturalım.
1 2 3 4 5 6 7 |
public String login(String userEmail) { User foundUser = userRepository.findByEmail(userEmail).orElseThrow(UserNotFoundException::new); } |
Yukarıdaki kodu yazdığımda compile time’da hata alıyorum çünkü kullandığım ide bana şunu diyor. Bu fırlattığın exception ya handle et yani try-catch mekanizması yaz ya da yukarıya gönder.
Tabii bu hatayı fırlattığımız yerde yakalayıp çözebiliriz fakat kod try-cacth mekanizmalarıya dolacaktır ya da dediğimiz gibi en üst katmana kadar bu hatayı fırlatabilirsiniz.
Peki bu oluşturduğumuz hatayı Exception’dan değil de RunTimeException class’ından kalıtsaydım ne olurdu?
1 2 3 4 5 6 7 |
public class UserNotFoundException extends RuntimeException{ } |
Bu durumda artık compile time’da hata almıyor oluyorum böyle bir durum oluştuğunda direkt olarak bu hatayla karşılacağız. Aşağıdaki gibi bir hata karşımıza çıkacak.
1 2 3 4 5 |
exception.UserNotFoundException\n\tat java.base/java.util.Optional.orElseThrow(Optional.java:403) |
Peki bu hataların kullanıcıya kadar bu şekilde ulaşması doğru mu? Tabii ki hayır, bu hataları doğru bir şekilde yönetmemiz ve kullanıcıyı yönlendirici mesajlar içermelidir ama nasıl?
Bir web uygulaması yazdığım için aşağıdaki gibi tanımlar bizim için yeterli olacak.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ExceptionResponse> handle(UserNotFoundException exception) { return ResponseEntity.ok(new ExceptionResponse(exception.getMessage(), HttpStatus.BAD_REQUEST)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserNotFoundException extends RuntimeException { public UserNotFoundException(String message) { super(message); // TODO Auto-generated constructor stub } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ExceptionResponse { private String message; private HttpStatus httpStatus; //getter setter constructor } |
1 2 3 4 5 6 7 8 9 10 |
public String login(String userEmail) { User foundUser = userRepository.findByEmail(loginRequest.getEmail()) .orElseThrow(() -> new UserNotFoundException("user bulunamadı")); } |
ve artık kullanıcı bir hata mesajı değil, aşağıdaki gibi bir response yapısı görüyor olacak.
1 2 3 4 5 6 7 8 |
{ "message": "user bulunamadı", "httpStatus": "BAD_REQUEST" } |
Burada belirlediğimiz mesajları da bir dosya üzerinden yönetebiliriz ve farklı dillerde bu hata mesajlarını verebilirsiniz. Bunun yapımızı aşağıdaki gibi değiştirmemiz gerekli.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class UserNoFoundException extends RuntimeException { private String key; public UserNoFoundException(String key) { this.key = key; } public String getKey() { return key; } } |
Yukarıda görüldüğü artık parametrele olarak aldığımız exception message super ile üst class’a göndermiyoruz, burada tek yaptığımız işlem bir key almak ve bu aldığımız key değerini Properties dosyasından okumak için kullanacağız.
resource/messages/messages_tr.properties dosyasını oluşturuyorum ve içerisine exception mesajını bulacağım key-value şeklinde yazıyorum.
1 2 3 4 5 |
user.not.found=kullanıcı bulunamadı. |
Şimdi exception yakaladığımız yerde burada okumasını sağlamamız gerekli. Bunun için bir interface mevcut MessageSource.
Öncelikle oluşturduğumuz dosyanın yolunu belirtmemiz gerekli. Bunun için aşağıdaki gibi bir Bean tanımı yapıyorum.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Configuration public class MessageSourceConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } } |
Artık bütün yapımız hazır. Sıra geldi bu dosyadan belirtilen key ile mesajın bulunması.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@RestControllerAdvice public class GlobalExceptionHandler { @Autowired private MessageSource messageSource; @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ExceptionResponse> handle(UserNotFoundException.class exception) { String message = messageSource.getMessage(exception.getKey(), null, new Locale("tr")); return ResponseEntity.ok(new ExceptionResponse(message, HttpStatus.BAD_REQUEST)); } } |
Eğer Locale değerini değiştirirseniz bu değere uygun Properties dosyasından gerekli mesajı çekebilirsiniz.
Kaynak;
Resim 1: https://data-flair.training/blogs/java-exception/
Faydalı olması dileğiyle.