Как создать собственную аннотацию в Jakarta Validation с динамическими сообщениями об ошибках, подобную @Size?
Я работаю с Jakarta Validation и хотел бы создать собственную аннотацию, похожую на @Size. Например, для поля типа String:
public class RegForm {
@Size(min=3, max=20)
private String login;
}
При ошибке валидации по умолчанию выводится сообщение, содержащее значения атрибутов min и max:
“размер должен находиться в диапазоне от 3 до 20”
Из исходников видно, что атрибут message аннотации выглядит так:
String message() default "{jakarta.validation.constraints.Size.message}";
Это означает, что сообщение по умолчанию должно находиться в MessageSource. Я использую Spring Boot, но в его автоконфигурации не нашел подобного функционала. Также не понятно, как значения атрибутов min и max попадают в MessageSource.
Подскажите, пожалуйста, как создать собственную аннотацию, сообщение которой может изменяться в зависимости от значений атрибутов аннотации?
Для создания собственной аннотации Jakarta Validation с динамическими сообщениями об ошибках вам потребуется создать три компонента: аннотацию, валидатор и конфигурацию сообщений. Система использует интерполяцию сообщений, где параметры в фигурных скобках автоматически заменяются значениями атрибутов аннотации.
Содержание
- Основы Jakarta Validation и интерполяция сообщений
- Создание собственной аннотации
- Реализация ConstraintValidator
- Конфигурация сообщений в Spring Boot
- Полный пример реализации
- Расширенные возможности
- Решение распространенных проблем
Основы Jakarta Validation и интерполяция сообщений
Jakarta Validation использует механизм интерполяции сообщений для создания динамических сообщений об ошибках. Когда валидация завершается с ошибкой, система автоматически подставляет значения атрибутов аннотации в шаблон сообщения.
Процесс интерполяции работает следующим образом:
- Находит шаблон сообщения на основе ключа (например,
{jakarta.validation.constraints.Size.message}) - Заменяет параметры в фигурных скобках значениями соответствующих атрибутов аннотации
- Поддерживает международную локализацию через MessageSource
Для аннотации @Size система автоматически подставляет значения min и max в шаблон сообщения. Вы можете использовать те же принципы для своих собственных аннотаций.
Создание собственной аннотации
Давайте создадим аннотацию @CustomSize, которая будет работать аналогично @Size, но с возможностью кастомных сообщений.
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
Теперь создадим валидатор, который будет проверять значения и передавать параметры в систему интерполяции:
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:
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;
}
}
Полный пример реализации
Давайте создадим полный пример использования нашей аннотации:
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;
// геттеры и сеттеры
}
Для тестирования можно использовать следующий сервис:
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);
}
}
Расширенные возможности
Условная валидация
Вы можете добавить дополнительные атрибуты для условной валидации:
@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:
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
String message() default "{com.example.validation.CustomSize.expression}";
String expression() default "min == max ? 'должен быть равен {min}' : 'должен быть между {min} и {max}'";
}
Кастомная логика интерполяции
Для сложных сценариев можно создать собственный интерполятор:
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());
}
// ... другие методы интерфейса
}
Решение распространенных проблем
Проблема: Параметры не подставляются в сообщение
Решение: Убедитесь, что:
- Валидатор добавляет параметры через
addDynamicValue() - Ключ сообщения совпадает с указанным в аннотации
- MessageSource правильно настроен
Проблема: Сообщения не локализуются
Решение: Проверьте:
- Имена файлов сообщений (messages_ru_RU.properties)
- Кодировка UTF-8
- Правильные локали в приложении
Проблема: Аннотация не работает в Spring Boot
Решение: Добавьте зависимость в pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
И убедитесь, что валидатор зарегистрирован как бин.
Проблема: Динамические значения не передаются
Решение: Используйте ConstraintValidatorContext для добавления динамических значений:
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"{com.example.validation.CustomSize.message}"
)
.addConstraintViolation()
.addDynamicValue("min", min)
.addDynamicValue("max", max);
Источники
- Configurable Model Validations with Jakarta Bean Validation, Spring Boot and Hibernate — Estafet
- Spring Boot Custom Bean Validations with Jakarta ConstraintValidator, Grouping Validation Constraints, GroupSequence and i18n messages · GitHub
- Java Bean Validation :: Spring Framework
- Spring Validation Message Interpolation | Baeldung
- Custom Validation in Spring Boot best explained – Part 1 – DevXperiences
- Hibernate Validator 9.0.1.Final - Jakarta Validation Reference
- Java Bean Validation with Javax/Jakarta Validation | Medium
- @Valid + Jakarta with DTO in Spring Boot 3 - DEV Community
- Custom Validation Messages in Spring Boot REST APIs | Medium
- Guide to Field Validation with Jakarta Validation in Spring | Medium
Заключение
- Для создания динамических сообщений валидации используйте механизм интерполяции Jakarta Validation с параметрами в фигурных скобках
- Собственная аннотация требует трех компонентов: самой аннотации, валидатора и конфигурации сообщений
- Spring Boot упрощает настройку через автоматическую конфигурацию валидации и MessageSource
- Для передачи динамических значений используйте метод
addDynamicValue()в ConstraintValidator - Система поддерживает международную локализацию через различные файлы свойств
Рекомендую начать с базовой реализации, как показано в примере, и постепенно добавлять сложные функции по мере необходимости.