[Java Backend Zero to Hello] BÀI 5.8: EXCEPTION HANDLING
📚 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