表单验证

未匹配的标注

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 接口实现更灵活的验证逻辑。

在 Java Web 开发中实现类似 Laravel 的表单错误处理机制,可以通过以下两种方式实现(分别演示基础 Servlet 和 Spring MVC 的实现):


方法一:基础 Servlet + JSP 实现

  1. 表单页面(form.jsp)

jsp

复制

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
  <%-- 显示错误信息 --%>
  <% if (request.getSession().getAttribute("errors") != null) { %>
    <div class="alert alert-danger">
      <%= request.getSession().getAttribute("errors") %>
    </div>
    <% request.getSession().removeAttribute("errors"); %>
  <% } %>

  <form action="/submit" method="post">
    <input type="text" name="username" 
           value="<%= session.getAttribute("old_username") != null ? 
                   session.getAttribute("old_username") : "" %>">
    <button type="submit">提交</button>
  </form>
</body>
</html>
  1. 处理表单的 Servlet

java

复制

@WebServlet("/submit")
public class FormServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取表单数据
        String username = request.getParameter("username");

        // 验证逻辑
        if (username == null || username.trim().isEmpty()) {
            HttpSession session = request.getSession();

            // 存储旧数据和错误信息
            session.setAttribute("old_username", username);
            session.setAttribute("errors", "用户名不能为空");

            // 重定向回表单页
            response.sendRedirect("/form.jsp");
            return;
        }

        // 验证通过的处理...
        response.sendRedirect("/success");
    }
}

方法二:Spring MVC 实现(推荐)

  1. 控制器类

java

复制

@Controller
public class FormController {

    @GetMapping("/form")
    public String showForm(Model model) {
        // 初始化表单对象
        if (!model.containsAttribute("form")) {
            model.addAttribute("form", new FormData());
        }
        return "form";
    }

    @PostMapping("/submit")
    public String handleSubmit(
            @Valid @ModelAttribute("form") FormData formData,
            BindingResult bindingResult,
            RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            // 自动携带错误信息和表单数据回传
            redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.form", bindingResult);
            redirectAttributes.addFlashAttribute("form", formData);
            return "redirect:/form";
        }

        // 验证通过的处理...
        return "redirect:/success";
    }
}
  1. 表单对象(DTO)

java

复制

public class FormData {
    @NotBlank(message = "用户名不能为空")
    private String username;

    // Getter/Setter
}
  1. Thymeleaf 模板(form.html)

html

运行

复制

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <!-- 错误信息显示 -->
  <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">
    <ul>
      <li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
    </ul>
  </div>

  <form action="/submit" method="post" th:object="${form}">
    <input type="text" th:field="*{username}" 
           th:class="${#fields.hasErrors('username') ? 'is-invalid' : ''}">
    <button type="submit">提交</button>
  </form>
</body>
</html>

关键实现要点:

  1. 错误信息传递:
    • 使用 RedirectAttributes(Spring)或 HttpSession(Servlet)存储错误信息

    • 通过 addFlashAttribute() 实现数据自动清除

  2. 旧数据保持:
    • 在表单字段中使用 th:field(Thymeleaf)或 JSP 表达式回显旧值

    • Spring 会自动保持绑定对象的数据

  3. 验证机制:
    • 使用 JSR-303 验证注解(如 @NotBlank

    • 通过 BindingResult 获取验证错误

  4. 视图技术集成:
    • Thymeleaf 的 #fields 工具对象可简化错误显示

    • 支持动态添加错误样式(如 is-invalid


工作流程示例:

  1. 用户访问 /form 显示空表单

  2. 提交无效数据到 /submit

  3. 服务器验证失败后:
    • 将错误信息和表单数据存入 Flash 域

    • 重定向回 /form

  4. 表单页面自动显示错误信息

  5. 用户修正后重新提交


扩展功能建议:

  1. 自定义错误格式:

java

复制

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleValidationExceptions(MethodArgumentNotValidException ex, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("errors", ex.getBindingResult().getAllErrors());
        return "redirect:/form";
    }
}
  1. 国际化错误消息:

properties

复制

# messages.properties
NotBlank.formData.username=用户名不能为空
  1. AJAX 提交支持:

javascript

复制

$.ajax({
    type: "POST",
    url: "/submit",
    data: formData,
    success: function(response) {
        if (response.errors) {
            // 动态更新错误显示
        }
    }
});

这种方式既保持了类似 Laravel 的便捷性,又符合 Java 企业级应用的开发规范,推荐在 Spring Boot 项目中结合 Thymeleaf 模板引擎使用。

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

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


暂无话题~