Spring Boot Global Exception Handling
Why Global Exception Handling
In web applications, exception handling is essential:
- User Experience: Friendly error messages instead of stack traces
- System Security: Hide internal implementation details
- Clean Code: Avoid try-catch in every method
- Unified Standards: Consistent error response format
Approach 1: @ControllerAdvice + @ExceptionHandler (Recommended)
1. Global Exception Handler
java@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e) { log.warn("Business exception: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(e.getCode(), e.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Result.error(400, message)); } @ExceptionHandler(Exception.class) public ResponseEntity<Result<Void>> handleException(Exception e) { log.error("System exception", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error(500, "System busy, please try again later")); } }
2. Unified Response Class
java@Data public class Result<T> { private Integer code; private String message; private T data; private Long timestamp; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }
3. Custom Business Exception
java@Getter public class BusinessException extends RuntimeException { private final Integer code; public BusinessException(String message) { super(message); this.code = 500; } public BusinessException(Integer code, String message) { super(message); this.code = code; } }
Approach 2: Implement HandlerExceptionResolver
java@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CustomExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); PrintWriter writer = response.getWriter(); Result<Void> result; if (ex instanceof BusinessException) { BusinessException be = (BusinessException) ex; response.setStatus(HttpServletResponse.SC_BAD_REQUEST); result = Result.error(be.getCode(), be.getMessage()); } else { result = Result.error(500, "System error"); } writer.write(JSON.toJSONString(result)); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
Approach 3: @ResponseStatus Annotation
java@ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } } @ResponseStatus(HttpStatus.BAD_REQUEST) public class InvalidParameterException extends RuntimeException { public InvalidParameterException(String message) { super(message); } }
Parameter Validation Exception Handling
1. Entity with Validation Annotations
java@Data public class UserCreateDTO { @NotBlank(message = "Username cannot be empty") @Size(min = 3, max = 20, message = "Username must be 3-20 characters") private String username; @NotBlank(message = "Password cannot be empty") @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", message = "Password must contain uppercase, lowercase, and number") private String password; @NotBlank(message = "Email cannot be empty") @Email(message = "Invalid email format") private String email; @Min(value = 0, message = "Age cannot be negative") @Max(value = 150, message = "Age cannot exceed 150") private Integer age; }
2. Group Validation
javapublic interface CreateGroup {} public interface UpdateGroup {} @Data public class UserDTO { @NotNull(groups = UpdateGroup.class) private Long id; @NotBlank(groups = CreateGroup.class) private String username; }
3. Controller Usage
java@RestController @RequestMapping("/users") public class UserController { @PostMapping public Result<Void> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { return Result.success(); } @PutMapping public Result<Void> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { return Result.success(); } }
404 Exception Handling Configuration
Enable 404 exception throwing:
yamlspring: web: resources: add-mappings: false mvc: throw-exception-if-no-handler-found: true
java@RestControllerAdvice public class NotFoundExceptionHandler { @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Result<Void> handleNoHandlerFound(NoHandlerFoundException e) { return Result.error(404, "Path not found: " + e.getRequestURL()); } }
Logging Best Practices
java@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e, WebRequest request) { log.warn("Business exception [{}] - URL: {}, Message: {}", e.getCode(), request.getDescription(false), e.getMessage()); return ResponseEntity.badRequest() .body(Result.error(e.getCode(), e.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<Result<Void>> handleException(Exception e, WebRequest request) { String traceId = UUID.randomUUID().toString(); log.error("System exception [TraceId: {}] - URL: {}", traceId, request.getDescription(false), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error(500, "System busy [TraceId: " + traceId + "]")); } }
Summary
| Approach | Use Case | Pros | Cons |
|---|---|---|---|
| @ControllerAdvice | Most scenarios | Centralized, flexible | None |
| HandlerExceptionResolver | Full control needed | Highest priority | Complex |
| @ResponseStatus | Simple exceptions | Concise | Less flexible |
Global exception handling is fundamental for building robust REST APIs. Recommendations:
- Unified error response format
- Clear error code system
- Proper logging (distinguish warn/error)
- Hide sensitive information in production