Master Spring Boot 3 Validation: 9 Essential Annotation Techniques
This article walks through Spring Boot 3's powerful annotation‑based parameter validation, covering basic constraints, custom validators, group validation, nested objects, method‑level checks, internationalized messages, programmatic validation, composite annotations, and cross‑field verification with complete code examples.
Introduction
Ensuring that method parameters are valid is essential for stable systems. Traditional validation code is verbose and hard to maintain, while annotation‑based validation (e.g., Spring Validation) simplifies this by using annotations such as @NotNull and @Size on fields or method parameters.
Practical Cases
2.1 Basic Annotations
Spring Validation provides a set of ready‑to‑use constraints. Example:
public class UserDTO {
private Long id;
@NotBlank(message = "用户名必须填写")
@Size(min = 4, max = 20, message = "用户名必须是4到20个字符")
private String username;
@Email(message = "无效的邮箱")
private String email;
@Min(value = 18, message = "年龄必须大于18")
private Integer age;
@Max(value = 120, message = "年龄必须小于120")
private Integer age;
@Past(message = "错误的出生日期")
private LocalDate birthDate;
@Pattern(regexp = "^1[3-9]\d{9}$", message = "电话号码错误")
private String phone;
}Controller method validation:
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody @Valid UserDTO userDTO, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
throw new ValidationException(bindingResult);
}
return ResponseEntity.ok(userDTO);
}
}2.2 Custom Annotation Validation
Define a custom constraint:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueUsernameValidator.class)
public @interface UniqueUsername {
String message() default "用户名已存在";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Validator implementation:
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
@Resource
private UserRepository userRepository;
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
if (username == null) {
return true; // @NotNull handles null
}
return !userRepository.existsByUsername(username);
}
}2.3 Group Validation
Define validation groups for different scenarios:
public interface ValidationGroups {
interface Create {}
interface Update {}
}Apply groups to a DTO:
public class ProductDTO {
@Null(groups = ValidationGroups.Create.class, message = "创建商品时ID必须为空")
@NotNull(groups = ValidationGroups.Update.class, message = "更新商品时商品ID不能为空")
private Long id;
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String name;
}Controller usage:
@PostMapping
public ResponseEntity<ProductDTO> createProduct(@RequestBody @Validated(ValidationGroups.Create.class) ProductDTO productDTO) {
return ResponseEntity.ok(productDTO);
}
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(@PathVariable Long id, @RequestBody @Validated(ValidationGroups.Update.class) ProductDTO productDTO) {
return ResponseEntity.ok(productDTO);
}2.4 Nested Validation
Validate nested objects and collections:
public class OrderDTO {
@NotNull
@Valid // nested validation
private CustomerDTO customer;
@NotEmpty
@Valid // nested validation for each item
private List<OrderItemDTO> items;
}
public class CustomerDTO {
@NotBlank
private String name;
@Email
private String email;
@Valid
private AddressDTO address;
}2.5 Method‑Level Validation
Apply validation in service layer:
@Service
@Validated
public class UserService {
public User createUser(@Valid UserDTO userDTO) {
return new User();
}
@NotNull
public User findById(@Min(1) Long id) {
return new User();
}
}Note: When validating controller methods, @Validated on the class is not required.
2.6 Error Message Internationalization
Direct message definition:
@NotEmpty(message = "姓名不能为空")
private String name;Using placeholders with resource bundles:
@NotEmpty(message = "{name.empty.error}")
private String name;Resource files:
# messages_zh_CN.properties
name.empty.error=姓名必须填写
address.empty.error=地址必须填写
# messages_en_US.properties
name.empty.error=nameEmptyError
address.empty.error=addressEmptyError2.7 Programmatic Validation
Manually trigger validation when needed:
@Service
public class ValidationService {
private final Validator validator;
public ValidationService(Validator validator) {
this.validator = validator;
}
public <T> void validate(T object) {
Set<ConstraintViolation<T>> violations = validator.validate(object);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}2.8 Composite Validation
Combine multiple constraints into a single reusable annotation:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface StrongPassword {
String message() default "密码不符合安全要求";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PasswordChangeDTO {
@StrongPassword
private String newPassword;
}2.9 Cross‑Field Validation
Class‑level constraint to ensure two fields match (e.g., password and confirm password):
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
public @interface PasswordMatches {
String message() default "密码不匹配";
String field();
String fieldMatch();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
private String field;
private String fieldMatch;
@Override
public void initialize(PasswordMatches constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
Field f1 = value.getClass().getDeclaredField(field);
f1.setAccessible(true);
Object v1 = f1.get(value);
Field f2 = value.getClass().getDeclaredField(fieldMatch);
f2.setAccessible(true);
Object v2 = f2.get(value);
if (v1 == null) {
return v2 == null;
}
return v1.equals(v2);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Failed to validate password fields", e);
}
}
}
@PasswordMatches(field = "password", fieldMatch = "confirmPassword", message = "两次密码不匹配")
public class UserRegistrationDTO {
@NotBlank(message = "密码不能为空")
@Size(min = 8, message = "密码长度必须大于等于8个字符")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody UserRegistrationDTO dto, BindingResult result) {
if (result.hasErrors()) {
List<String> errors = result.getAllErrors().stream()
.map(err -> err.getDefaultMessage())
.toList();
return ResponseEntity.badRequest().body(errors);
}
return ResponseEntity.ok("Registration successful");
}Conclusion
The article demonstrates a complete set of Spring Boot 3 validation techniques, from simple field constraints to advanced custom and cross‑field validators, including internationalization and programmatic approaches.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
