public class User { @NotNull private Long id; @NotBlank @Size(min = 2, max = 50) private String firstName; @NotBlank @Size(min = 2, max = 50) private String lastName; @Email private String email; @NotNull @Min(18) @Max(99) private Integer age; @NotEmpty private List<String> hobbies; @Pattern(regexp = "[A-Z]{2}\d{4}") private String employeeId;2 使用自定义验证注解
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UniqueTitleValidator.class) public @interface UniqueTitle { String message() default "Title must be unique"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }接下来,我们创建一个PostRepository接口,目的是从数据库中检索帖子:
public interface PostRepository extends JpaRepository<Post, Long> { Post findByTitle(String title); }然后我们需要定义验证器类 UniqueTitleValidator,如下所示:
@Component public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> { @Autowired private PostRepository postRepository; @Override public boolean isValid(String title, ConstraintValidatorContext context) { if (title == null) { return true; } return Objects.isNull(postRepository.findByTitle(title)); } }UniqueTitleValidator实现了ConstraintValidator接口,它有两个泛型类型:第一个是自定义注解UniqueTitle,第二个是正在验证的字段类型(在本例中为String). 我们还自动装配了PostRepository 类以从数据库中检索帖子。isValid()方法通过查询 PostRepository 来检查 title 是否为 null 或者它是否是唯一的。如果 title 为 null 或唯一,则验证成功,并返回 true。
public class Post { @UniqueTitle private String title; @NotNull private String body; }我们已将 @UniqueTitle 注释应用于 Post 类中的 title 变量。验证此字段时,这将触发 UniqueTitleValidator 类中定义的验证逻辑。
public class UserDTO { @NotBlank private String username; @NotBlank private String password; }我们使用@NotBlank注解来确保username和password字段不为空或 null。接下来,我们可以创建一个控制器方法来处理 HTTP POST 请求并在创建新用户之前验证输入:
@RestController @RequestMapping("/users") @Validated public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) { userService.createUser(userDto); return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully"); } }我们使用 Spring 的@Validated注解来启用方法级验证,我们还将 @Valid 注释应用于 userDto 参数以触发验证过程。
public class User { @NotBlank(message = "用户名不能为空") private String name; @NotBlank(message = "Email不能为空") @Email(message = "无效的Emaild地址") private String email; @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于18") @Max(value = 99, message = "年龄必须小于99") private Integer age; }我们使用 message属性为每个验证注释提供了自定义错误消息。接下来,在我们的 Spring 控制器中,我们可以处理表单提交并使用 @Valid 注释验证用户输入:
@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<String> createUser(@Valid @RequestBody User user, BindingResult result) { if (result.hasErrors()) { List<String> errorMessages = result.getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.toList()); return ResponseEntity.badRequest().body(errorMessages.toString()); } // save the user to the database using UserService userService.saveUser(user); return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully"); } }我们使用 @Valid 注释来触发 User 对象的验证,并使用 BindingResult 对象来捕获任何验证错误。
# messages.properties user.name.required=Name is required. user.email.invalid=Invalid email format. user.age.invalid=Age must be a number between 18 and 99.接下来,为每种支持的语言创建一个 messages_xx.properties 文件,例如,中文的 messages_zh_CN.properties。
user.name.required=名称不能为空. user.email.invalid=无效的email格式. user.age.invalid=年龄必须在18到99岁之间.然后,更新您的验证注释以使用本地化的错误消息
public class User { @NotNull(message = "{user.id.required}") private Long id; @NotBlank(message = "{user.name.required}") private String name; @Email(message = "{user.email.invalid}") private String email; @NotNull(message = "{user.age.required}") @Min(value = 18, message = "{user.age.invalid}") @Max(value = 99, message = "{user.age.invalid}") private Integer age; }最后,在 Spring 配置文件中配置 MessageSource bean 以加载 i18n 消息文件
@Configuration public class AppConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } @Bean public LocalValidatorFactoryBean validator() { LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean(); validatorFactoryBean.setValidationMessageSource(messageSource()); return validatorFactoryBean; } }现在,当发生验证错误时,错误消息将根据随请求发送的“Accept-Language”标头以用户的首选语言显示。
public class User { @NotBlank(groups = Default.class) private String firstName; @NotBlank(groups = Default.class) private String lastName; @Email(groups = EmailNotEmpty.class) private String email; // getters and setters omitted for brevity public interface EmailNotEmpty {} public interface Default {} }请注意,我们在User类中定义了两个接口,EmailNotEmpty和 Default。这些将作为我们的验证组。
@RestController @RequestMapping("/users") @Validated public class UserController { public ResponseEntity<String> createUser( @Validated({org.example.model.ex6.User.EmailNotEmpty.class}) @RequestBody User userWithEmail, @Validated({User.Default.class}) @RequestBody User userWithoutEmail) { // Create the user and return a success response } }
我们已将@Validated注释添加到我们的控制器,表明我们想要使用验证组。我们还更新了 createUser 方法,将两个 User 对象作为输入,一个在 email 字段不为空时使用,另一个在它为空时使用。@Validated 注释用于指定将哪个验证组应用于每个 User 对象。对于 userWithEmail 参数,我们指定了 EmailNotEmpty 组,而对于 userWithoutEmail 参数,我们指定了 Default 组。进行这些更改后,现在将根据“电子邮件”字段是否为空对“用户”类进行不同的验证。如果为空,则 firstName 或 lastName 字段必须非空。否则,所有三个字段都将正常验证。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EndDateAfterStartDateValidator.class) public @interface EndDateAfterStartDate { String message() default "End date must be after start date"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }然后,我们创建验证器EndDateAfterStartDateValidator:
public class EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> { @Override public boolean isValid(TaskForm taskForm, ConstraintValidatorContext context) { if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) { return true; } return taskForm.getEndDate().isAfter(taskForm.getStartDate()); } }最后,我们将EndDateAfterStartDate注释应用于我们的表单对象TaskForm:
@EndDateAfterStartDate public class TaskForm { @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; }现在,当用户提交表单时,验证框架将自动检查结束日期是否晚于开始日期,如果不是,则提供有意义的错误消息。
@RestControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("status", status.value()); // Get all errors List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(x -> x.getDefaultMessage()) .collect(Collectors.toList()); body.put("errors", errors); return new ResponseEntity<>(body, headers, status); } }在这里,我们创建了一个用 @RestControllerAdvice 注解的 RestExceptionHandler 类来处理我们的 REST API 抛出的异常。然后我们创建一个用@ExceptionHandler注解的方法来处理在验证失败时抛出的 MethodArgumentNotValidException。在处理程序方法中,我们创建了一个 Map 对象来保存错误响应的详细信息,包括时间戳、HTTP 状态代码和错误消息列表。我们使用 MethodArgumentNotValidException 对象的 getBindingResult() 方法获取所有验证错误并将它们添加到错误消息列表中。
@DataJpaTest public class UserValidationTest { @Autowired private TestEntityManager entityManager; @Autowired private Validator validator; @Test public void testValidation() { User user = new User(); user.setFirstName("John"); user.setLastName("Doe"); user.setEmail("invalid email"); Set<ConstraintViolation<User>> violations = validator.validate(user); assertEquals(1, violations.size()); assertEquals("must be a well-formed email address", violations.iterator().next().getMessage()); } }我们使用 JUnit 5 编写一个测试来验证具有无效电子邮件地址的“用户”对象。然后我们使用 Validator 接口来验证 User 对象并检查是否返回了预期的验证错误。