Cài đặt bộ xử lý ngoại lệ toàn cục trong Spring MVC để trả về JSON

Trong các ứng dụng Spring MVC hiện đại, việc trả về phản hồi dạng JSON khi xảy ra lỗi là yêu cầu phổ biến. Thay vì sử dụng JSP để render lỗi — phương pháp đã lỗi thời và không phù hợp với kiến trúc API — ta nên triển khai HandlerExceptionResolver một cách thuần túy, kết hợp với @ResponseBody và cấu hình đúng kiểu nội dung (MIME type) để đảm bảo tính nhất quán giữa thành công và thất bại.

Giải pháp dưới đây loại bỏ hoàn toàn phụ thuộc vào JSP, thay vào đó sử dụng cơ chế view resolution dựa trên MappingJackson2JsonView hoặc trực tiếp trả về ResponseEntity<?>, đồng thời phân tách rõ ràng các lớp ngoại lệ tùy chỉnh và logic xử lý tương ứng.

1. Định nghĩa lớp ngoại lệ tùy chỉnh

package vn.example.exception;

public class ApiValidationException extends RuntimeException {
    private final String errorCode;
    private final Object details;

    public ApiValidationException(String message, String code, Object details) {
        super(message);
        this.errorCode = code;
        this.details = details;
    }

    // Getters...
    public String getErrorCode() { return errorCode; }
    public Object getDetails() { return details; }
}

2. Triển khai bộ xử lý ngoại lệ toàn cục

Lớp này không dùng ModelAndView mà trả về ResponseEntity trực tiếp — đảm bảo đầu ra luôn là JSON, không qua view layer:

package vn.example.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ApiValidationException.class)
    public ResponseEntity<Map<String, Object>> handleValidationException(
            ApiValidationException ex, WebRequest request) {
        
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("timestamp", LocalDateTime.now());
        errorResponse.put("status", HttpStatus.BAD_REQUEST.value());
        errorResponse.put("error", "Validation Failed");
        errorResponse.put("code", ex.getErrorCode());
        errorResponse.put("message", ex.getMessage());
        errorResponse.put("details", ex.getDetails());
        errorResponse.put("path", request.getDescription(false).replace("uri=", ""));

        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGenericException(
            Exception ex, WebRequest request) {
        
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("timestamp", LocalDateTime.now());
        errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        errorResponse.put("error", "Internal Server Error");
        errorResponse.put("message", "An unexpected error occurred.");
        errorResponse.put("path", request.getDescription(false).replace("uri=", ""));

        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

3. Sử dụng trong controller

@RestController
@RequestMapping("/api/v1")
public class AuthController {

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(
            @RequestParam String username,
            @RequestParam String password) {

        if (username == null || username.trim().isEmpty()) {
            throw new ApiValidationException(
                "Username is required", 
                "VALIDATION_001", 
                Map.of("field", "username", "reason", "cannot be empty")
            );
        }

        // Xử lý đăng nhập thực tế...
        Map<String, String> success = new HashMap<>();
        success.put("status", "success");
        success.put("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");

        return ResponseEntity.ok(success);
    }
}

4. Cấu hình Spring Boot (nếu dùng)

Không cần khai báo bean thủ công — @RestControllerAdvice tự động được quét và đăng ký. Đảm bảo đã thêm dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

5. Gọi từ phía client (JavaScript)

fetch('/api/v1/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ username: 'test', password: '123' })
})
.then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
})
.then(data => console.log('Success:', data))
.catch(err => console.error('Error:', err.message));

Thẻ: spring-mvc rest-api JSON exception-handling spring-boot

Đăng vào ngày 29 tháng 6 lúc 06:15