60 phút
Exception Handling và Validation
Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(NotFoundException ex, HttpServletRequest req) {
return new ErrorResponse(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
req.getRequestURI(),
null
);
}
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleBusiness(BusinessException ex, HttpServletRequest req) {
return new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Bad Request",
ex.getMessage(),
req.getRequestURI(),
null
);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest req
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage()));
return new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
"Invalid input",
req.getRequestURI(),
errors
);
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse handleAccessDenied(AccessDeniedException ex, HttpServletRequest req) {
return new ErrorResponse(
LocalDateTime.now(),
HttpStatus.FORBIDDEN.value(),
"Forbidden",
"Access denied",
req.getRequestURI(),
null
);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneric(Exception ex, HttpServletRequest req) {
log.error("Unhandled exception", ex);
return new ErrorResponse(
LocalDateTime.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"An unexpected error occurred",
req.getRequestURI(),
null
);
}
}
public record ErrorResponse(
LocalDateTime timestamp,
int status,
String error,
String message,
String path,
Map<String, String> details
) {}
Custom Exceptions
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
Validation
Request validation
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
String name,
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
String email,
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
message = "Password must contain uppercase, lowercase and digit"
)
String password,
@NotNull
@Min(value = 18, message = "Must be at least 18")
@Max(value = 120, message = "Age must be reasonable")
Integer age
) {}
Custom validators
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
@RequiredArgsConstructor
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
private final UserRepository userRepository;
@Override
public boolean isValid(String email, ConstraintValidatorContext ctx) {
if (email == null) return true;
return !userRepository.existsByEmail(email);
}
}
// Sử dụng
public record RegisterRequest(
@NotBlank String name,
@Email @UniqueEmail String email,
@Size(min = 8) String password
) {}
Validation groups
public interface CreateGroup {}
public interface UpdateGroup {}
public record UserRequest(
@Null(groups = CreateGroup.class)
@NotNull(groups = UpdateGroup.class)
Long id,
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
String name
) {}
// Controller
@PostMapping
public void create(@Validated(CreateGroup.class) @RequestBody UserRequest req) {}
@PutMapping
public void update(@Validated(UpdateGroup.class) @RequestBody UserRequest req) {}
Problem Details (RFC 7807)
@RestControllerAdvice
public class ProblemDetailsHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ProblemDetail handleNotFound(NotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Resource Not Found");
pd.setType(URI.create("https://example.com/errors/not-found"));
return pd;
}
}
Logging Best Practices
@Slf4j
@Service
public class UserService {
public UserResponse findById(Long id) {
log.debug("Finding user by id: {}", id);
return userRepository.findById(id)
.map(UserResponse::from)
.orElseThrow(() -> {
log.warn("User not found: {}", id);
return new NotFoundException("User not found");
});
}
}
Bài tập thực hành
Hãy implement error handling toàn diện cho API!