🛡 Кастомные валидаторы в Spring Boot: Как сделать валидацию под себя



Валидация входных данных — важная часть любой системы, обеспечивающая целостность данных и предотвращение ошибок. Иногда стандартные аннотации Jakarta Bean Validation (например, @NotNull, @Size, @Pattern) не покрывают все наши потребности, и нам приходится писать свои собственные валидаторы. В этом посте мы разберем два подхода к созданию кастомных валидаторов в Spring Boot, а также узнаем, как настроить отображение ошибки валидации.



1. Кастомные валидации с мета-аннотациями



Если стандартных аннотаций не хватает, можно создать свою собственную аннотацию для валидации. Это удобно, если вам нужно сочетать несколько стандартных аннотаций в одной или переиспользовать уже имеющиеся аннотации. Например, давайте создадим аннотацию @CardNumber, которая проверяет, соответствует ли строка формату номера кредитной карты.





@Target({ElementType.FIELD, ElementType.PARAMETER})

@Retention(RetentionPolicy.RUNTIME)

@Pattern(regexp = "([0-9]{4}-){3}[0-9]{4}$", message = "Invalid credit card number")

@Constraint(validatedBy = {})

public @interface CardNumber {

String message() default "Invalid credit card number";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}





В этом примере аннотация @CardNumber использует @Pattern для проверки формата номера кредитной карты. Такой подход упрощает код, потому что вся логика валидации сконцентрирована в одной аннотации.





@PostMapping("/card")

void checkCardNumber(@RequestParam @Valid @CardNumber String cardNumber) {

}







@Test

public void validCardNumber() throws Exception {

mockMvc.perform(post("/card")

.param("cardNumber", "1111-1111-1111-1111"))

.andExpect(status().isOk())

.andDo(print());

}



@Test

public void invalidCardNumber() throws Exception {

mockMvc.perform(post("/card")

.param("cardNumber", "1111-1111-1111"))

.andExpect(status().is(400))

.andDo(print());

}





2. Реализация собственных валидаторов



Когда стандартные аннотации не подходят, можно создать собственные валидаторы. Для этого нужно реализовать интерфейс ConstraintValidator.



Предположим, нам нужно валидировать пароли с определенными требованиями. Для этого создадим аннотацию @ValidPassword и валидатор PasswordValidator.





@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})

@Retention(RetentionPolicy.RUNTIME)

@Constraint(validatedBy = PasswordValidator.class)

public @interface ValidPassword {

String message() default "Invalid password";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

int minLength() default 8;

String specialChars() default "!@#$%^&*()";

}



public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {

private int minLength;

private String specialChars;



@Override

public void initialize(ValidPassword constraintAnnotation) {

this.minLength = constraintAnnotation.minLength();

this.specialChars = constraintAnnotation.specialChars();

}



@Override

public boolean isValid(String password, ConstraintValidatorContext context) {

if (password == null) {

return false;

}



boolean hasUpperCase = !password.equals(password.toLowerCase());

boolean hasLowerCase = !password.equals(password.toUpperCase());

boolean hasDigit = password.chars().anyMatch(Character::isDigit);

boolean hasSpecialChar = password.chars().anyMatch(ch -> specialChars.indexOf(ch) >= 0);

boolean isLongEnough = password.length() >= minLength;



return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar && isLongEnough;

}

}





В данном примере аннотация @ValidPassword проверяет, что пароль содержит и верхний, и нижний регистр, цифры, специальные символы и имеет достаточную длину. Логика валидации инкапсулирована в PasswordValidator.



Продолжение в комментариях 👇



#SpringBoot #SpringTips