Global Exception Handling in Spring Boot completely decouples error-handling logic from Controllers, ensuring that all error responses returned to the client have a consistent, readable format.
I. Global Exception Handling
1. Core Concepts
@ExceptionHandler: Annotation dùng trên một phương thức để khai báo rằng phương thức đó sẽ bắt và xử lý một (hoặc một số) loạiExceptioncụ thể khi nó xảy ra trong quá trình xử lý request.@ControllerAdvice/@RestControllerAdvice: Cho phép gom nhóm các phương thức@ExceptionHandlervào một nơi để áp dụng cho toàn bộ các Controller trong ứng dụng (AOP pattern).@RestControllerAdvice=@ControllerAdvice+@ResponseBody(Tự động serialize kết quả trả về thành JSON).
2. API Error Response Design
Một DTO trả về cho Client khi gặp lỗi nên có cấu trúc nhất quán.
@Getter
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL) // Bỏ qua các trường null khi serialize ra JSON
public class ErrorResponse {
private int code;
private String message;
private LocalDateTime timestamp;
private List<FieldErrorDetail> errors; // Dùng khi validate form/request body
@Getter
@AllArgsConstructor
public static class FieldErrorDetail {
private String field;
private String message;
}
}
3. Global Exception Handler
Dưới đây là một GlobalExceptionHandler đáp ứng các kịch bản lỗi phổ biến trong thực tế:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. Xử lý các Custom Business Exception (Do developer tự định nghĩa)
@ExceptionHandler(AppException.class)
public ResponseEntity<ErrorResponse> handleAppException(AppException ex) {
log.error("AppException: {}", ex.getMessage());
ErrorCode errorCode = ex.getErrorCode();
ErrorResponse response = ErrorResponse.builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(errorCode.getHttpStatus()).body(response);
}
// 2. Xử lý lỗi Validation khi dùng @Valid / @Validated trên Request Body
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
List<ErrorResponse.FieldErrorDetail> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorResponse.FieldErrorDetail(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.code(4000) // Custom code cho validation
.message("Invalid request payload")
.errors(fieldErrors)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
// 3. Fallback handler: Xử lý tất cả các Exception không được khai báo cụ thể (Uncaught Exceptions)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unhandled Exception: ", ex); // Log chi tiết stacktrace để debug internal
ErrorResponse response = ErrorResponse.builder()
.code(5000)
.message("An unexpected error occurred. Please try again later.")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
4. Managing Errors via ErrorCode Enum (Best Practice)
Thay vì hardcode chuỗi message và status code ở nhiều nơi, hãy gom chúng thành một Enum:
@Getter
@AllArgsConstructor
public enum ErrorCode {
INVALID_REQUEST(4000, "Invalid request body or query parameter", HttpStatus.BAD_REQUEST),
USER_NOT_FOUND(4041, "User does not exist in system", HttpStatus.NOT_FOUND),
UNAUTHORIZED(4010, "Authentication token is missing or invalid", HttpStatus.UNAUTHORIZED),
INTERNAL_SERVER_ERROR(5000, "Internal server error", HttpStatus.INTERNAL_SERVER_ERROR);
private final int code;
private final String message;
private final HttpStatus httpStatus;
}
and custom Exception
@Getter
public class BusinessLogicException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessLogicException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Khi muốn quăng lỗi ở tầng Business Service:
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessLogicException(ErrorCode.USER_NOT_FOUND));
}
5. Các dạng JSON Response trả về cho Client
Trường hợp 1: Lỗi Business logic thông thường (VD: Bad Request)
{
"code": 4000,
"message": "Invalid request body or query parameter",
"timestamp": "2026-07-30T11:40:52"
}
Trường hợp 2: Lỗi Validation dữ liệu đầu vào (VD: @NotNull, @Email)
{
"code": 4000,
"message": "Invalid request payload",
"timestamp": "2026-07-30T11:40:52",
"errors": [
{
"field": "email",
"message": "Email must be a valid email address"
},
{
"field": "age",
"message": "Age must be greater than or equal to 18"
}
]
}
6. Lưu ý bảo mật khi trả về Error Response
-
Không trả lại StackTrace ra JSON Response trên môi trường Production: Điều này có thể để lộ cấu trúc thư mục, phiên bản thư viện, và thông tin nhạy cảm của hệ thống.
-
Log đầy đủ thông tin lỗi ở tầng Server qua Log4j2/Logback (
log.error(...)), nhưng chỉ trả về message ngắn gọn, đã được lọc cẩn thận cho Client.
II. Log
@Slf4j là một Annotation thuộc thư viện Lombok.
-
SLF4J (Simple Logging Facade for Java): Là một Abstraction Layer (Interface) cho việc ghi log trong Java. Nó không trực tiếp ghi log mà định nghĩa các chuẩn API. Bên dưới, Spring Boot sử dụng Logback làm thư viện thực thi mặc định (Logging Implementation).
-
Khi bạn đặt
@Slf4jlên trên một Class, Lombok sẽ tự động sinh ra mộtstatic final Loggerinstance tại thời điểm biên dịch (Compile-time).
Nó tương đương với việc bạn tự khai báo thủ công:
// Dùng @Slf4j
@RestController
@Slf4j
public class UserController {
public void doSomething() {
log.info("Processing user request...");
}
}
// Không dùng Lombok (Viết thủ công):
@RestController
public class UserController {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(UserController.class);
public void doSomething() {
log.info("Processing user request...");
}
}
Các Log Level phổ biến:
Theo thứ tự mức độ nghiêm trọng tăng dần:
TRACE < DEBUG < INFO < WARN < ERROR
log.trace("Very detailed trace information");
log.debug("Debug level info: userId={}", userId); // Dùng {} placeholder để tối ưu hiệu năng
log.info("User created successfully: {}", user.getId());
log.warn("High memory usage detected: {}%", memoryUsage);
log.error("Failed to process payment for order: {}", orderId, exception); // Log full Exception stacktrace
Best Practice: Luôn dùng dạng Parameterized Logging log.info("User: {}", username) thay vì nối chuỗi log.info("User: " + username) để tránh tốn tài nguyên JVM khi log level đó chưa được bật.
2. Cấu hình Logging trong Spring Boot
Spring Boot đã tích hợp sẵn spring-boot-starter-logging (đi kèm Logback). Bạn có thể cấu hình log theo 2 cách: qua application.yml (đơn giản) hoặc qua logback-spring.xml (nâng cao).
Cách 1: Cấu hình nhanh qua application.yml (Khuyên dùng cho dự án vừa & nhỏ)
logging:
# 1. Thiết lập Log Level cho từng package
level:
root: INFO # Default level cho toàn bộ app
com.example.service: DEBUG # Bật DEBUG cho package chứa business logic
org.hibernate.SQL: DEBUG # Log các câu lệnh SQL của Hibernate
org.hibernate.type.descriptor.sql: TRACE # Log các giá trị binding parameter trong SQL
# 2. Cấu hình ghi Log ra File
file:
name: logs/application.log # Đường dẫn file log
# 3. Cấu hình Log Rotation (Xoay vòng file log để không làm đầy ổ đĩa)
logback:
rollingpolicy:
max-file-size: 10MB # File đạt 10MB sẽ tách ra file mới
max-history: 30 # Giữ tối đa 30 ngày log
total-size-cap: 3GB # Tổng dung lượng các file log không vượt quá 3GB
file-name-pattern: "logs/archived/app-%d{yyyy-MM-dd}.%i.log"
# 4. Cấu hình Pattern hiển thị trên Console
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
Cách 2: Cấu hình nâng cao qua logback-spring.xml (Production Standard)
Khi cần gửi log sang hệ thống tập trung (ELK Stack / Grafana Loki), tách file log theo Level, hoặc log dạng JSON, bạn nên tạo file src/main/resources/logback-spring.xml.
Ví dụ cấu hình log dạng JSON (dành cho ELK/Splunk) kết hợp Async Appender để tăng hiệu năng:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 1. Console Appender (In ra màn hình console) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 2. File Appender ghi dạng JSON (Dùng logstash-logback-encoder) -->
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app-json.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/archived/app-json-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>14</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<!-- 3. Async Appender: Đưa việc ghi log vào Queue riêng để không cản trở Thread của Controller/Service -->
<appender name="ASYNC_JSON" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE_JSON" />
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- 4. Multi-environment configuration -->
<!-- Profile 'dev': Log Console với DEBUG level -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<logger name="com.example" level="DEBUG" />
</springProfile>
<!-- Profile 'prod': Log Async JSON ra File để ELK thu thập -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_JSON" />
</root>
</springProfile>
</configuration>
3. Best Practice: Trace ID trong Distributed System (MDC Log)
Khi làm ứng dụng Microservices hoặc High-concurrency System, nhiều Request chạy song song làm cho các dòng Log bị trộn lẫn vào nhau.
Bằng cách kết hợp MDC (Mapped Diagnostic Context) với Spring AOP hoặc Filter, bạn có thể gán một traceId duy nhất cho toàn bộ chuỗi xử lý của một Request:
@Component
public class TraceIdFilter implements Filter {
private static final String TRACE_ID = "traceId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// Tạo traceId ngẫu nhiên cho mỗi request
String traceId = UUID.randomUUID().toString().substring(0, 8);
MDC.put(TRACE_ID, traceId);
chain.doFilter(request, response);
} finally {
// Xóa MDC sau khi request kết thúc để tránh leak memory giữa các Thread trong Pool
MDC.clear();
}
}
}
Trong pattern log, bạn chỉ cần thêm %X{traceId}:
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n
Kết quả Log thu được:
2026-07-31 17:30:00.123 [a1b2c3d4] [http-nio-8080-exec-1] INFO c.e.service.UserService - Fetching user details
2026-07-31 17:30:00.145 [a1b2c3d4] [http-nio-8080-exec-1] ERROR c.e.handler.GlobalExceptionHandler - User not found
(Tất cả dòng log cùng một request sẽ có chung id a1b2c3d4, giúp việc filter log cực kỳ dễ dàng).