0

[Java Backend Zero to Hello] BÀI 5.8: EXCEPTION HANDLING

Java Backend Zero to Hello

📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 5: Spring Framework & Spring Boot | Bài 54/86


BÀI 5.8: EXCEPTION HANDLING

Mục tiêu

  • Tạo Custom Exception
  • Sử dụng @ControllerAdvice
  • Xử lý các loại exception
  • Trả về ErrorResponse chuẩn

1. HỆ THỐNG EXCEPTION

Throwable
├── Error (JVM, không xử lý)
└── Exception
    ├── IOException (Checked)
    ├── SQLException (Checked)
    └── RuntimeException (Unchecked)
        ├── NullPointerException
        ├── IllegalArgumentException
        └── Custom Exception

2. CUSTOM EXCEPTION

2.1 Business Exception

public class NotFoundException extends RuntimeException {
    public NotFoundException(String message) {
        super(message);
    }
}

public class DuplicateException extends RuntimeException {
    public DuplicateException(String message) {
        super(message);
    }
}

public class BusinessException extends RuntimeException {
    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

2.2 Error Code Enum

public enum ErrorCode {
    USER_NOT_FOUND("USER_001", "Không tìm thấy user"),
    DUPLICATE_EMAIL("USER_002", "Email đã tồn tại"),
    INVALID_INPUT("INPUT_001", "Dữ liệu không hợp lệ"),
    UNAUTHORIZED("AUTH_001", "Chưa xác thực"),
    FORBIDDEN("AUTH_002", "Không có quyền truy cập"),
    INTERNAL_ERROR("SYS_001", "Lỗi hệ thống");

    private final String code;
    private final String message;

    ErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() { return code; }
    public String getMessage() { return message; }
}

3. ERROR RESPONSE

public record ErrorResponse(
    String code,
    String message,
    Object details,
    LocalDateTime timestamp,
    String path
) {
    public static ErrorResponse of(ErrorCode errorCode, String path) {
        return new ErrorResponse(
            errorCode.getCode(),
            errorCode.getMessage(),
            null,
            LocalDateTime.now(),
            path
        );
    }

    public static ErrorResponse of(ErrorCode errorCode, Object details, String path) {
        return new ErrorResponse(
            errorCode.getCode(),
            errorCode.getMessage(),
            details,
            LocalDateTime.now(),
            path
        );
    }
}

4. @RESTCONTROLLERADVICE

4.1 Global Exception Handler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            NotFoundException ex, HttpServletRequest request) {
        log.warn("Not found: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of(ErrorCode.USER_NOT_FOUND, request.getRequestURI()));
    }

    @ExceptionHandler(DuplicateException.class)
    public ResponseEntity<ErrorResponse> handleDuplicate(
            DuplicateException ex, HttpServletRequest request) {
        log.warn("Duplicate: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse.of(ErrorCode.DUPLICATE_EMAIL, request.getRequestURI()));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex, HttpServletRequest request) {
        log.warn("Business error: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(
                ex.getCode(),
                ex.getMessage(),
                null,
                LocalDateTime.now(),
                request.getRequestURI()
            ));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex, HttpServletRequest request) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
            .forEach(fe -> errors.put(fe.getField(), fe.getDefaultMessage()));

        return ResponseEntity.badRequest()
            .body(ErrorResponse.of(ErrorCode.INVALID_INPUT, errors, request.getRequestURI()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(
            Exception ex, HttpServletRequest request) {
        log.error("Unexpected error", ex);
        return ResponseEntity.internalServerError()
            .body(ErrorResponse.of(ErrorCode.INTERNAL_ERROR, request.getRequestURI()));
    }
}

5. XỬ LÝ CÁC EXCEPTION CỤ THỂ

5.1 Validation

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
        MethodArgumentNotValidException ex) {
    // Xử lý @Valid trên @RequestBody
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraint(
        ConstraintViolationException ex) {
    // Xử lý @Valid trên @PathVariable, @RequestParam
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleNotReadable(
        HttpMessageNotReadableException ex) {
    // JSON không parse được
}

5.2 Database

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrity(
        DataIntegrityViolationException ex) {
    // Vi phạm constraint
}

@ExceptionHandler(OptimisticLockingFailureException.class)
public ResponseEntity<ErrorResponse> handleOptimisticLock(
        OptimisticLockingFailureException ex) {
    // Optimistic lock conflict
}

5.3 Security

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(
        AccessDeniedException ex) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN)
        .body(ErrorResponse.of(ErrorCode.FORBIDDEN, null));
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuth(
        AuthenticationException ex) {
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
        .body(ErrorResponse.of(ErrorCode.UNAUTHORIZED, null));
}

6. SỬ DỤNG TRONG SERVICE

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public UserResponse getById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found: " + id));
        return UserResponse.from(user);
    }

    public UserResponse create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.email())) {
            throw new DuplicateException("Email already exists: " + request.email());
        }
        // ...
    }
}

7. PROBLEM DETAILS (RFC 7807)

Spring 6 hỗ trợ chuẩn Problem Details.

@ExceptionHandler(NotFoundException.class)
public ProblemDetail handleNotFound(NotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.NOT_FOUND, ex.getMessage()
    );
    problem.setTitle("Resource Not Found");
    problem.setType(URI.create("https://example.com/errors/not-found"));
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

8. LOG EXCEPTION

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex,
                                                    HttpServletRequest request) {
    // Log đầy đủ thông tin
    log.error("Unexpected error at {}: {}",
        request.getRequestURI(), ex.getMessage(), ex);

    // Trả về thông tin chung cho client (không lộ chi tiết)
    return ResponseEntity.internalServerError()
        .body(ErrorResponse.of(ErrorCode.INTERNAL_ERROR, request.getRequestURI()));
}

9. CUSTOM ERROR CONTROLLER

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public ResponseEntity<ErrorResponse> handleError(HttpServletRequest request) {
        Integer status = (Integer) request.getAttribute("jakarta.servlet.error.status_code");
        // ...
    }
}

10. BÀI TẬP THỰC HÀNH

Bài 1: Custom Exceptions

Tạo các exception: NotFoundException, DuplicateException, BusinessException.

Bài 2: Global Handler

Implement GlobalExceptionHandler xử lý tất cả exception.

Bài 3: ErrorResponse

Tạo ErrorResponse chuẩn với code, message, details, timestamp.


11. TÓM TẮT

Khái niệm Mô tả
Custom Exception Kế thừa RuntimeException
ErrorCode Enum mã lỗi
ErrorResponse Format response lỗi
@RestControllerAdvice Global handler
@ExceptionHandler Xử lý exception cụ thể
ProblemDetail RFC 7807
Logging Log đầy đủ thông tin

Bài tiếp theo: 5.9 Spring Security cơ bản


🧭 Điều Hướng Series

⬅️ Bài trước: BÀI 5.7: VALIDATION

📋 Lộ trình tổng quan: Xem Toàn Bộ Series

➡️ Bài tiếp theo: BÀI 5.9: SPRING SECURITY CƠ BẢN


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí