表单验证

未匹配的标注

Spring Boot 表单验证完整指南

Spring Boot 提供了强大的表单验证功能,主要基于 Java Bean Validation API (JSR 380) 实现。以下是完整的表单验证解决方案:

  1. 基础配置

添加依赖

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

启用验证
Spring Boot 自动配置了验证功能,无需额外配置。

  1. 常用验证注解

字段级验证
| 注解 | 说明 |
|——|——|
| @NotNull | 不能为null |
| @NotEmpty | 不能为null或空(字符串、集合、Map、数组) |
| @NotBlank | 字符串不能为null且必须包含非空白字符 |
| @Size(min=, max=) | 长度限制 |
| @Min(value) | 数字最小值 |
| @Max(value) | 数字最大值 |
| @DecimalMin(value) | 小数值最小值 |
| @DecimalMax(value) | 小数值最大值 |
| @Digits(integer=, fraction=) | 数字位数限制 |
| @Email | 电子邮件格式 |
| @Pattern(regexp=) | 正则表达式匹配 |
| @Past | 必须是过去日期 |
| @PastOrPresent | 过去或现在日期 |
| @Future | 必须是将来日期 |
| @FutureOrPresent | 将来或现在日期 |

类级验证
| 注解 | 说明 |
|——|——|
| @Valid | 级联验证关联对象 |
| @Validated | 分组验证 |

  1. 完整实现示例

实体类定义

public class UserForm {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @Min(value = 18, message = "年龄必须大于18岁")
    @Max(value = 100, message = "年龄必须小于100岁")
    private Integer age;

    @NotNull(message = "生日不能为空")
    @Past(message = "生日必须是过去日期")
    private LocalDate birthday;

    // getters and setters
}

控制器处理

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody UserForm userForm, 
                                      BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            // 处理验证错误
            return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
        }

        // 业务逻辑处理
        return ResponseEntity.ok("用户创建成功");
    }
}
  1. 高级功能

自定义验证注解

// 定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "无效的手机号码";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 实现验证逻辑
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // 结合@NotNull使用
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

分组验证

public interface CreateGroup {}
public interface UpdateGroup {}

public class UserForm {
    @Null(groups = CreateGroup.class, message = "ID必须为空")
    @NotNull(groups = UpdateGroup.class, message = "ID不能为空")
    private Long id;

    // 其他字段...
}

// 控制器中使用
@PostMapping
public ResponseEntity<?> createUser(@Validated(CreateGroup.class) @RequestBody UserForm userForm) {
    // ...
}

国际化错误消息

  1. resources/messages.properties 中添加:

    userForm.username.notBlank=用户名不能为空
    userForm.username.size=用户名长度必须在{min}{max}个字符之间
  2. 在实体类中使用:

    @NotBlank(message = "{userForm.username.notBlank}")
    @Size(min = 2, max = 20, message = "{userForm.username.size}")
    private String username;
  3. 统一异常处理

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationException(
            MethodArgumentNotValidException ex) {

        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.toList());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("errors", errors);

        return ResponseEntity.badRequest().body(body);
    }
}
  1. 前端表单验证配合

Thymeleaf 表单示例

<form th:action="@{/users}" th:object="${userForm}" method="post">
    <div>
        <label>用户名:</label>
        <input type="text" th:field="*{username}">
        <span th:if="${#fields.hasErrors('username')}" 
              th:errors="*{username}" class="error"></span>
    </div>
    <!-- 其他字段... -->
    <button type="submit">提交</button>
</form>
  1. 测试验证
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void createUserWithInvalidData() throws Exception {
        String invalidUserJson = "{\"username\":\"a\",\"password\":\"123\",\"email\":\"invalid\"}";

        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidUserJson))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errors").isArray())
                .andDo(print());
    }
}

总结

Spring Boot 表单验证提供了从简单到复杂的完整解决方案:

  1. 使用标准注解快速实现基础验证
  2. 通过自定义注解扩展验证规则
  3. 利用分组验证实现不同场景的验证逻辑
  4. 结合国际化支持多语言错误消息
  5. 统一异常处理返回友好的错误信息
  6. 前端后端协同验证提升用户体验

对于复杂业务场景,还可以结合 Spring 的 Validator 接口实现更灵活的验证逻辑。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
唐章明
讨论数量: 0
发起讨论 只看当前版本


暂无话题~