Spring Boot 3'te Exception Handling: Kapsamlı Rehber
Neden Exception Handling Bu Kadar Önemli?
Bir API geliştirdiğinizi hayal edin. Kullanıcı olmayan bir ID ile istek yapıyor, geçersiz bir JSON body gönderiyor ya da sunucuda beklenmedik bir hata oluşuyor. Bu durumların hepsinde istemciye anlamlı, tutarlı ve standart bir hata yanıtı dönmeniz gerekiyor. Aksi halde frontend geliştiricisi hata mesajlarını parse edemez, mobil uygulama crash eder, ve siz gece 3'te production loglarına bakarsınız.
Spring Boot 3, exception handling konusunda ciddi iyileştirmeler getirdi. Özellikle RFC 7807 ProblemDetail desteği ile artık hata yanıtlarınız uluslararası bir standarda uygun hale geliyor. Bu yazıda sıfırdan başlayıp production-ready bir exception handling altyapısı kuracağız.
Spring Boot'ta Exception Handling Temelleri
Spring Boot'ta exception handling mekanizması birkaç katmandan oluşur. En temelinde, Controller metodlarından fırlatılan exception'lar Spring'in DispatcherServlet tarafından yakalanır ve uygun bir handler'a yönlendirilir.
Üç temel yaklaşım vardır:
@ExceptionHandler: Tek bir controller sınıfı içinde hata yakalama
@ControllerAdvice: Tüm controller'lar için global hata yakalama
ResponseStatusException: Direkt olarak HTTP status code ile exception fırlatma
Basit bir örnekle başlayalım:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
User user = userService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return ResponseEntity.ok(UserDto.from(user));
}
// Controller seviyesinde exception handler
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}Bu yaklaşım işe yarar ama her controller'da aynı handler'ı tekrar yazmak zorunda kalırsınız. İşte burada @ControllerAdvice devreye giriyor.
@ControllerAdvice ile Global Exception Handling
@ControllerAdvice, tüm controller'lar için merkezi bir exception handler tanımlamanızı sağlar. Spring konteynerinde bir bean olarak yaşar ve herhangi bir controller'dan fırlatılan exception'ları yakalar.
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex, WebRequest request) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.value())
.message(ex.getMessage())
.path(request.getDescription(false).replace("uri=", ""))
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex, WebRequest request) {
log.error("Business error: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.UNPROCESSABLE_ENTITY.value())
.message(ex.getMessage())
.path(request.getDescription(false).replace("uri=", ""))
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(
Exception ex, WebRequest request) {
log.error("Unexpected error occurred", ex);
ErrorResponse error = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("Beklenmeyen bir hata olustu. Lutfen daha sonra tekrar deneyin.")
.path(request.getDescription(false).replace("uri=", ""))
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}Dikkat:
@RestControllerAdvice, hem@ControllerAdvicehem de@ResponseBodyannotation'larını içerir. Bu sayede dönüş değeri otomatik olarak JSON'a serialize edilir.
Custom Exception Sınıfları Tasarlamak
İyi bir exception hiyerarşisi, kodunuzu okunabilir ve bakımı kolay hale getirir. Base exception sınıfı oluşturup, iş mantığına göre alt sınıflar türetmenizi öneririm:
// Base exception
public abstract class BaseException extends RuntimeException {
private final String errorCode;
private final HttpStatus httpStatus;
protected BaseException(String message, String errorCode, HttpStatus httpStatus) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
}
public String getErrorCode() { return errorCode; }
public HttpStatus getHttpStatus() { return httpStatus; }
}
// Resource bulunamadı
public class ResourceNotFoundException extends BaseException {
public ResourceNotFoundException(String resourceName, Object id) {
super(
String.format("%s bulunamadi: %s", resourceName, id),
"RESOURCE_NOT_FOUND",
HttpStatus.NOT_FOUND
);
}
}
// Is kuralı ihlali
public class BusinessException extends BaseException {
public BusinessException(String message) {
super(message, "BUSINESS_ERROR", HttpStatus.UNPROCESSABLE_ENTITY);
}
}
// Yetkilendirme hatası
public class UnauthorizedException extends BaseException {
public UnauthorizedException(String message) {
super(message, "UNAUTHORIZED", HttpStatus.UNAUTHORIZED);
}
}
// Duplicate kayıt
public class DuplicateResourceException extends BaseException {
public DuplicateResourceException(String resourceName, String field, Object value) {
super(
String.format("%s zaten mevcut: %s = %s", resourceName, field, value),
"DUPLICATE_RESOURCE",
HttpStatus.CONFLICT
);
}
}Bu hiyerarşi ile global exception handler'ınızı basitleştirebilirsiniz:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(
BaseException ex, WebRequest request) {
ErrorResponse error = ErrorResponse.builder()
.status(ex.getHttpStatus().value())
.errorCode(ex.getErrorCode())
.message(ex.getMessage())
.path(extractPath(request))
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(ex.getHttpStatus()).body(error);
}
private String extractPath(WebRequest request) {
return request.getDescription(false).replace("uri=", "");
}
}RFC 7807 ProblemDetail ile Standart Hata Yanıtları
Spring Boot 3, RFC 7807 standardını native olarak destekler. Bu standart, HTTP API hata yanıtları için evrensel bir format tanımlar. ProblemDetail sınıfı bu standardın Java implementasyonudur.
Bir ProblemDetail yanıtı şu alanları içerir:
type: Hata türünü tanımlayan URI
title: İnsan tarafından okunabilir hata başlığı
status: HTTP status code
detail: Hatanın detaylı açıklaması
instance: Hatanın oluştuğu endpoint
@RestControllerAdvice
public class ProblemDetailExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
ex.getMessage()
);
problem.setTitle("Kaynak Bulunamadi");
problem.setType(URI.create("https://api.example.com/errors/not-found"));
problem.setProperty("errorCode", ex.getErrorCode());
problem.setProperty("timestamp", Instant.now());
return problem;
}
@ExceptionHandler(BusinessException.class)
public ProblemDetail handleBusinessError(BusinessException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.UNPROCESSABLE_ENTITY,
ex.getMessage()
);
problem.setTitle("Is Kurali Ihlali");
problem.setType(URI.create("https://api.example.com/errors/business-error"));
problem.setProperty("errorCode", ex.getErrorCode());
problem.setProperty("timestamp", Instant.now());
return problem;
}
}ProblemDetail kullanmak için application.properties dosyanıza şunu ekleyin:
spring.mvc.problemdetails.enabled=trueBu ayar aktif olduğunda, Spring Boot varsayılan hata yanıtlarını da ProblemDetail formatında döner. Örneğin 404 yanıtı şu şekilde olur:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User bulunamadi: 42",
"instance": "/api/users/42",
"errorCode": "RESOURCE_NOT_FOUND",
"timestamp": "2026-02-08T10:30:00Z"
}Validation Error Handling
Spring Boot'ta Bean Validation (@Valid, @NotBlank, @Size vb.) kullandığınızda, validation hataları MethodArgumentNotValidException olarak fırlatılır. Bu exception'ı yakalayıp kullanıcı dostu bir yanıt dönmeniz gerekir:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationErrors(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Validasyon hatasi"
);
problem.setTitle("Gecersiz Istek");
problem.setType(URI.create("https://api.example.com/errors/validation"));
// Alan bazlı hataları topla
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(error.getField(), error.getDefaultMessage());
}
problem.setProperty("fieldErrors", fieldErrors);
problem.setProperty("timestamp", Instant.now());
return problem;
}Bir DTO üzerinde validation annotation'ları şöyle kullanılır:
public record CreateUserRequest(
@NotBlank(message = "Isim bos olamaz")
@Size(min = 2, max = 100, message = "Isim 2 ile 100 karakter arasinda olmali")
String name,
@NotBlank(message = "Email bos olamaz")
@Email(message = "Gecerli bir email adresi giriniz")
String email,
@NotBlank(message = "Sifre bos olamaz")
@Size(min = 8, message = "Sifre en az 8 karakter olmali")
String password
) {}Controller'da @Valid annotation'ı ile validation tetiklenir:
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDto user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}Validation hatası durumunda API şuna benzer bir yanıt döner:
{
"type": "https://api.example.com/errors/validation",
"title": "Gecersiz Istek",
"status": 400,
"detail": "Validasyon hatasi",
"fieldErrors": {
"email": "Gecerli bir email adresi giriniz",
"password": "Sifre en az 8 karakter olmali"
}
}RequestParam ve PathVariable Validation
@PathVariable ve @RequestParam validation'ları farklı bir exception üretir: ConstraintViolationException. Bunu da yakalamanız gerekir:
@ExceptionHandler(ConstraintViolationException.class)
public ProblemDetail handleConstraintViolation(ConstraintViolationException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Parametre validasyon hatasi"
);
problem.setTitle("Gecersiz Parametre");
Map<String, String> violations = new HashMap<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
String path = violation.getPropertyPath().toString();
String field = path.contains(".") ? path.substring(path.lastIndexOf('.') + 1) : path;
violations.put(field, violation.getMessage());
}
problem.setProperty("violations", violations);
return problem;
}Controller sınıfına @Validated eklemeyi unutmayın:
@RestController
@RequestMapping("/api/users")
@Validated // Bu olmadan PathVariable/RequestParam validation calismaz
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(
@PathVariable @Min(value = 1, message = "ID pozitif olmali") Long id) {
// ...
}
}Production Best Practices
Production ortamında exception handling için birkaç önemli kural vardır:
Asla stack trace'i istemciye göndermeyin. Internal server error'larda generic bir mesaj dönün, detayları loglayın.
Her exception'ı loglayın. Ama log seviyelerini doğru kullanın: business error'lar
WARN, unexpected error'larERROR.Error code kullanın. String mesajlar dile bağımlıdır. "INSUFFICIENT_BALANCE" gibi kodlar frontend tarafında i18n'e çevrilebilir.
Correlation ID ekleyin. Her request'e unique bir ID atayıp hata yanıtına dahil edin. Bu sayede loglardan hata takibi kolaylaşır.
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpectedError(Exception ex, HttpServletRequest request) {
String correlationId = UUID.randomUUID().toString();
log.error("Unexpected error [correlationId={}]: {}", correlationId, ex.getMessage(), ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"Beklenmeyen bir hata olustu"
);
problem.setTitle("Sunucu Hatasi");
problem.setProperty("correlationId", correlationId);
problem.setProperty("timestamp", Instant.now());
return problem;
}Exception handler sırasını kontrol edin. Spring, en spesifik exception handler'ı seçer.
Exception.classhandler'ı her zaman en sonda olmalı.
Tamamlanmış ErrorResponse Sınıfı
İşte production-ready bir ErrorResponse record sınıfı:
public record ErrorResponse(
int status,
String errorCode,
String message,
String path,
LocalDateTime timestamp,
Map<String, String> fieldErrors
) {
public static ErrorResponseBuilder builder() {
return new ErrorResponseBuilder();
}
public static class ErrorResponseBuilder {
private int status;
private String errorCode;
private String message;
private String path;
private LocalDateTime timestamp;
private Map<String, String> fieldErrors;
public ErrorResponseBuilder status(int status) {
this.status = status; return this;
}
public ErrorResponseBuilder errorCode(String errorCode) {
this.errorCode = errorCode; return this;
}
public ErrorResponseBuilder message(String message) {
this.message = message; return this;
}
public ErrorResponseBuilder path(String path) {
this.path = path; return this;
}
public ErrorResponseBuilder timestamp(LocalDateTime timestamp) {
this.timestamp = timestamp; return this;
}
public ErrorResponseBuilder fieldErrors(Map<String, String> fieldErrors) {
this.fieldErrors = fieldErrors; return this;
}
public ErrorResponse build() {
return new ErrorResponse(status, errorCode, message, path, timestamp, fieldErrors);
}
}
}Özet
@ControllerAdvice ile tüm controller'lar için merkezi exception handling yapın
Custom exception hiyerarşisi oluşturun:
BaseExceptionaltında iş mantığına uygun sınıflarRFC 7807 ProblemDetail kullanarak standart hata yanıtları dönün
Validation hatalarını
MethodArgumentNotValidExceptionveConstraintViolationExceptionile yakalayınProduction'da stack trace göndermeyin, correlation ID kullanın, log seviyelerini doğru ayarlayın
Her zaman error code döndürün — frontend i18n desteği için kritik
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
Bu konuyu derinlemesine öğrenmek ister misin?
Spring Boot: Sıfırdan İleri Seviyeye
İlgili Yazılar
Spring Boot'ta Exception Handling: @ControllerAdvice ile Profesyonel Hata Yönetimi
Spring Boot uygulamalarında hata yönetimini profesyonel seviyeye taşıyın. @ControllerAdvice, @ExceptionHandler, custom e...
Spring Boot Nedir? Neden Spring Boot Kullanmalısınız?
Spring Boot nedir, ne işe yarar? Auto-configuration, starter dependencies, embedded server özellikleri. Java backend gel...
Spring Boot'ta Custom Annotation Yazma
Spring Boot'ta kendi annotation'ınızı yazın: meta-annotation, AOP ile cross-cutting concerns, validation ve composed ann...