← Назад к вопросам

Какие знаешь способы валидации данных?

2.3 Middle🔥 231 комментариев
#REST API и микросервисы#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы валидации данных в Java

Валидация — критически важная часть любого приложения. Неправильные данные могут привести к ошибкам, уязвимостям безопасности и деградации системы. Давайте разберёмся в различных подходах.

1. Аннотационная валидация (JSR-380 / Jakarta Validation)

Этот способ стал стандартом в Java благодаря простоте и декларативности:

import jakarta.validation.constraints.*;

class User {
    @NotNull(message = "ID не может быть null")
    @Positive(message = "ID должен быть больше 0")
    private Integer id;
    
    @NotBlank(message = "Имя обязательно")
    @Size(min = 2, max = 50, message = "Имя должно быть от 2 до 50 символов")
    private String name;
    
    @NotNull
    @Email(message = "Некорректный формат email")
    private String email;
    
    @Min(value = 18, message = "Возраст должен быть >= 18")
    @Max(value = 150, message = "Возраст должен быть <= 150")
    private Integer age;
    
    @Pattern(regexp = "^\\d{3}-\\d{3}-\\d{4}$", message = "Некорректный номер телефона")
    private String phone;
    
    @URL(message = "Некорректный URL")
    private String website;
}

Встроенные аннотации:

  • @NotNull — не null
  • @NotBlank — не пусто и не только пробелы
  • @NotEmpty — не пусто (для коллекций)
  • @Email — валидный email
  • @Size — размер строки, коллекции
  • @Pattern — regex паттерн
  • @Positive/@Negative — положительное/отрицательное число
  • @Min/@Max — минимум/максимум
  • @URL — валидный URL
  • @Future/@Past — дата в будущем/прошлом

Использование валидатора:

import jakarta.validation.Validator;
import jakarta.validation.ConstraintViolation;

public class UserValidator {
    private final Validator validator;
    
    public void validateUser(User user) {
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        
        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation<User> violation : violations) {
                sb.append(violation.getPropertyPath())
                  .append(": ")
                  .append(violation.getMessage())
                  .append("\n");
            }
            throw new ValidationException(sb.toString());
        }
    }
}

В Spring Boot (автоматически):

import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/users")
public class UserController {
    
    @PostMapping
    public User createUser(@Valid @RequestBody User user) {
        // user уже валидирован, иначе выбросится ConstraintViolationException
        return userService.save(user);
    }
}

2. Кастомная валидация

Для сложных правил можно создать свои аннотации:

import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;

// Определение аннотации
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
    String message() default "Некорректный номер телефона";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Реализация валидатора
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true; // Null обрабатывается @NotNull
        
        // Проверка: 10 цифр
        return value.matches("^[0-9]{10}$");
    }
}

// Использование
class User {
    @ValidPhoneNumber
    private String phone;
}

3. Валидация на уровне бизнес-логики

Для правил, которые зависят от состояния системы:

public class UserService {
    private final UserRepository repository;
    
    public User createUser(CreateUserRequest request) {
        // Базовая валидация
        if (request.getName() == null || request.getName().isBlank()) {
            throw new IllegalArgumentException("Имя обязательно");
        }
        
        // Бизнес-валидация
        if (repository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("Email уже зарегистрирован");
        }
        
        if (request.getAge() < 18) {
            throw new AgeRestrictionException("Возраст должен быть >= 18");
        }
        
        // Валидация через зависимости
        if (!emailService.isValidDomain(request.getEmail())) {
            throw new InvalidEmailDomainException("Домен email недопустим");
        }
        
        return repository.save(mapToUser(request));
    }
}

4. Программная валидация (Manual Validation)

Для простых случаев без фреймворков:

public class OrderValidator {
    
    public static void validateOrder(Order order) throws ValidationException {
        List<String> errors = new ArrayList<>();
        
        if (order.getId() == null || order.getId() <= 0) {
            errors.add("Order ID должен быть положительным числом");
        }
        
        if (order.getItems() == null || order.getItems().isEmpty()) {
            errors.add("Заказ должен содержать хотя бы один товар");
        }
        
        if (order.getPrice() == null || order.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
            errors.add("Цена должна быть больше 0");
        }
        
        if (!order.getEmail().matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            errors.add("Некорректный email");
        }
        
        if (!errors.isEmpty()) {
            throw new ValidationException(String.join(", ", errors));
        }
    }
}

5. Валидация данных от клиента (Frontend + Backend)

Frontend (первичная валидация)

// HTML5 валидация
<input type="email" required pattern="^[a-z]+@[a-z]+\\.[a-z]{2,}$" />
<input type="number" min="18" max="150" />

Backend (обязательная валидация)

// Никогда не доверяй только frontend-валидации!
// Backend ДОЛЖЕН проверять все данные
@PostMapping("/register")
public ResponseEntity<User> register(@Valid @RequestBody RegisterRequest request) {
    // Даже если frontend выполнил валидацию,
    // backend повторно проверяет ВСЕ данные
    return ResponseEntity.ok(userService.register(request));
}

6. Валидация во время парсинга данных

JSON валидация (при десериализации)

class UserDeserializer extends StdDeserializer<User> {
    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectMapper mapper = (ObjectMapper) p.getCodec();
        ObjectNode node = mapper.readTree(p);
        
        String name = node.get("name").asText();
        if (name == null || name.isEmpty()) {
            throw new JsonMappingException("name field is required");
        }
        
        return new User(name);
    }
}

SQL валидация (на уровне БД)

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    age INT CHECK (age >= 18 AND age <= 150),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

7. Валидация с использованием Object-Oriented подхода

// Вместо примитивных типов используем Value Objects
public class Email {
    private final String value;
    
    public Email(String value) {
        if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Некорректный email: " + value);
        }
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
}

public class Age {
    private final int value;
    
    public Age(int value) {
        if (value < 18 || value > 150) {
            throw new IllegalArgumentException("Возраст должен быть 18-150");
        }
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

// Использование
class User {
    private Email email;
    private Age age;
    
    public User(Email email, Age age) {
        this.email = email;  // Уже валидированы!
        this.age = age;
    }
}

8. Валидация с использованием Specification Pattern

public interface Specification<T> {
    boolean isSatisfiedBy(T candidate);
}

public class MinAgeSpecification implements Specification<User> {
    private final int minAge;
    
    public MinAgeSpecification(int minAge) {
        this.minAge = minAge;
    }
    
    @Override
    public boolean isSatisfiedBy(User user) {
        return user.getAge() >= minAge;
    }
}

// Использование
Specification<User> isAdult = new MinAgeSpecification(18);
if (!isAdult.isSatisfiedBy(user)) {
    throw new IllegalStateException("User is not adult");
}

9. Валидация Request/Response

В Spring Boot

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());
        
        return ResponseEntity.badRequest()
            .body(new ErrorResponse(400, "Validation failed", errors));
    }
}

Лучшие практики валидации

  1. Многоуровневая валидация — frontend + backend + БД
  2. Никогда не доверяй входным данным — валидируй всегда
  3. Используй JSR-380 для декларативной валидации
  4. Кастомные валидаторы для бизнес-правил
  5. Fail-fast — останови при первой ошибке или собери все
  6. Value Objects — использование типов вместо примитивов
  7. Чистые сообщения об ошибках — помогают клиентам исправить
  8. Логирование валидационных ошибок для анализа
  9. Тесты для валидаторов — обязательны
  10. Безопасность — валидация защищает от инъекций и атак