НейроАгент

Как создать собственную аннотацию в Jakarta Validation

Полное руководство по созданию кастомных аннотаций с динамическими сообщениями об ошибках в Jakarta Validation и Spring Boot

Как создать собственную аннотацию в Jakarta Validation с динамическими сообщениями об ошибках, подобную @Size?

Я работаю с Jakarta Validation и хотел бы создать собственную аннотацию, похожую на @Size. Например, для поля типа String:

java
public class RegForm {
    @Size(min=3, max=20)
    private String login;
}

При ошибке валидации по умолчанию выводится сообщение, содержащее значения атрибутов min и max:
“размер должен находиться в диапазоне от 3 до 20”

Из исходников видно, что атрибут message аннотации выглядит так:

java
String message() default "{jakarta.validation.constraints.Size.message}";

Это означает, что сообщение по умолчанию должно находиться в MessageSource. Я использую Spring Boot, но в его автоконфигурации не нашел подобного функционала. Также не понятно, как значения атрибутов min и max попадают в MessageSource.

Подскажите, пожалуйста, как создать собственную аннотацию, сообщение которой может изменяться в зависимости от значений атрибутов аннотации?

НейроАгент

Для создания собственной аннотации Jakarta Validation с динамическими сообщениями об ошибках вам потребуется создать три компонента: аннотацию, валидатор и конфигурацию сообщений. Система использует интерполяцию сообщений, где параметры в фигурных скобках автоматически заменяются значениями атрибутов аннотации.

Содержание

Основы Jakarta Validation и интерполяция сообщений

Jakarta Validation использует механизм интерполяции сообщений для создания динамических сообщений об ошибках. Когда валидация завершается с ошибкой, система автоматически подставляет значения атрибутов аннотации в шаблон сообщения.

Процесс интерполяции работает следующим образом:

  1. Находит шаблон сообщения на основе ключа (например, {jakarta.validation.constraints.Size.message})
  2. Заменяет параметры в фигурных скобках значениями соответствующих атрибутов аннотации
  3. Поддерживает международную локализацию через MessageSource

Для аннотации @Size система автоматически подставляет значения min и max в шаблон сообщения. Вы можете использовать те же принципы для своих собственных аннотаций.

Создание собственной аннотации

Давайте создадим аннотацию @CustomSize, которая будет работать аналогично @Size, но с возможностью кастомных сообщений.

java
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
    String message() default "{com.example.validation.CustomSize.message}";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
    
    int min() default 0;
    
    int max() default Integer.MAX_VALUE;
}

Ключевые моменты:

  • @Constraint указывает на класс-валидатор
  • message() использует шаблон с ключом для интерполяции
  • min() и max() определяют диапазон значений
  • Аннотация может применяться к полям и параметрам методов

Реализация ConstraintValidator

Теперь создадим валидатор, который будет проверять значения и передавать параметры в систему интерполяции:

java
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;

public class CustomSizeValidator implements ConstraintValidator<CustomSize, CharSequence> {
    private int min;
    private int max;
    
    @Override
    public void initialize(CustomSize constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }
    
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // null-значения проверяются аннотацией @NotNull
        }
        
        int length = value.length();
        
        if (length < min || length > max) {
            // Добавляем параметры для интерполяции сообщения
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(
                context.getDefaultConstraintMessageTemplate()
            )
            .addConstraintViolation()
            .addPropertyNode("value")
            .addDynamicValue("min", min)
            .addDynamicValue("max", max)
            .addConstraintViolation();
            
            return false;
        }
        
        return true;
    }
}

Важно: метод addDynamicValue() добавляет значения для параметров интерполяции.

Конфигурация сообщений в Spring Boot

Для работы интерполяции сообщений нужно настроить MessageSource. Создайте файл сообщений:

messages.properties

com.example.validation.CustomSize.message=Размер должен быть между {min} и {max} символами

messages_ru_RU.properties

com.example.validation.CustomSize.message=Размер должен быть между {min} и {max} символами

Добавьте конфигурацию в Spring Boot:

java
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class ValidationConfig {
    
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = 
            new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(10);
        return messageSource;
    }
    
    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource());
        return bean;
    }
}

Полный пример реализации

Давайте создадим полный пример использования нашей аннотации:

java
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    
    @PostMapping("/users")
    public ResponseEntity<String> createUser(@Valid @RequestBody UserDto user, BindingResult result) {
        if (result.hasErrors()) {
            // Обработка ошибок валидации
            return ResponseEntity.badRequest().body(result.getAllErrors().toString());
        }
        return ResponseEntity.ok("Пользователь успешно создан");
    }
}

// DTO с нашей аннотацией
public class UserDto {
    @CustomSize(min=3, max=20)
    private String username;
    
    @CustomSize(min=6, max=30)
    private String password;
    
    // геттеры и сеттеры
}

Для тестирования можно использовать следующий сервис:

java
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Service
@Validated
public class UserService {
    
    public void validateUsername(@CustomSize(min=3, max=20) String username) {
        // Метод с валидацией параметров
        System.out.println("Username валиден: " + username);
    }
}

Расширенные возможности

Условная валидация

Вы можете добавить дополнительные атрибуты для условной валидации:

java
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
    // ... существующие атрибуты
    
    String message() default "{com.example.validation.CustomSize.message}";
    
    boolean includeMin() default true;
    
    boolean includeMax() default true;
}

Интерполяция с Expression Language

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

java
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
    String message() default "{com.example.validation.CustomSize.expression}";
    
    String expression() default "min == max ? 'должен быть равен {min}' : 'должен быть между {min} и {max}'";
}

Кастомная логика интерполяции

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

java
import jakarta.validation.MessageInterpolator;
import org.springframework.context.MessageSource;

public class CustomMessageInterpolator implements MessageInterpolator {
    private final MessageSource messageSource;
    
    public CustomMessageInterpolator(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    
    @Override
    public String interpolate(String messageTemplate, Context context) {
        // Ваша кастомная логика интерполяции
        return messageSource.getMessage(messageTemplate, null, Locale.getDefault());
    }
    
    // ... другие методы интерфейса
}

Решение распространенных проблем

Проблема: Параметры не подставляются в сообщение

Решение: Убедитесь, что:

  1. Валидатор добавляет параметры через addDynamicValue()
  2. Ключ сообщения совпадает с указанным в аннотации
  3. MessageSource правильно настроен

Проблема: Сообщения не локализуются

Решение: Проверьте:

  1. Имена файлов сообщений (messages_ru_RU.properties)
  2. Кодировка UTF-8
  3. Правильные локали в приложении

Проблема: Аннотация не работает в Spring Boot

Решение: Добавьте зависимость в pom.xml:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

И убедитесь, что валидатор зарегистрирован как бин.

Проблема: Динамические значения не передаются

Решение: Используйте ConstraintValidatorContext для добавления динамических значений:

java
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
    "{com.example.validation.CustomSize.message}"
)
.addConstraintViolation()
.addDynamicValue("min", min)
.addDynamicValue("max", max);

Источники

  1. Configurable Model Validations with Jakarta Bean Validation, Spring Boot and Hibernate — Estafet
  2. Spring Boot Custom Bean Validations with Jakarta ConstraintValidator, Grouping Validation Constraints, GroupSequence and i18n messages · GitHub
  3. Java Bean Validation :: Spring Framework
  4. Spring Validation Message Interpolation | Baeldung
  5. Custom Validation in Spring Boot best explained – Part 1 – DevXperiences
  6. Hibernate Validator 9.0.1.Final - Jakarta Validation Reference
  7. Java Bean Validation with Javax/Jakarta Validation | Medium
  8. @Valid + Jakarta with DTO in Spring Boot 3 - DEV Community
  9. Custom Validation Messages in Spring Boot REST APIs | Medium
  10. Guide to Field Validation with Jakarta Validation in Spring | Medium

Заключение

  • Для создания динамических сообщений валидации используйте механизм интерполяции Jakarta Validation с параметрами в фигурных скобках
  • Собственная аннотация требует трех компонентов: самой аннотации, валидатора и конфигурации сообщений
  • Spring Boot упрощает настройку через автоматическую конфигурацию валидации и MessageSource
  • Для передачи динамических значений используйте метод addDynamicValue() в ConstraintValidator
  • Система поддерживает международную локализацию через различные файлы свойств

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