Spring MVC 表单处理和验证

目录

  1. 表单处理概述
  2. 基础表单创建
  3. Spring Form标签库
  4. 数据绑定和转换
  5. 数据验证机制
  6. JSR-303Bean验证
  7. 自定义验证器
  8. 错误处理和显示
  9. 复杂表单处理
  10. 文件上传处理
  11. 表单国际化
  12. AJAX表单处理
  13. 最佳实践
  14. 常见问题解决
  15. 总结

表单处理概述

在Web应用中,表单是用户与系统交互的重要方式。Spring MVC提供了强大的表单处理机制,包括数据绑定、验证、错误处理等完整功能。

表单处理流程

  1. 表单显示: 控制器返回包含表单的页面
  2. 用户提交: 用户填写信息并提交表单
  3. 数据绑定: Spring MVC将请求参数绑定到对象
  4. 数据验证: 验证用户输入的数据
  5. 错误处理: 如果有验证错误,返回表单页面
  6. 数据处理: 验证通过后,执行业务逻辑
  7. 结果反馈: 返回成功页面或重定向

Spring MVC表单处理组件

  • Form标签库: 简化的表单标签
  • 数据绑定: 自动的参数绑定机制
  • 验证框架: JSR-303 Bean验证
  • 错误处理: BindingResult错误收集
  • 转换器: 类型转换和格式化

基础表单创建

简单用户注册表单

实体类定义

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;
    private String confirmPassword;
    private Date birthDate;
    private String gender;
    private boolean active = true;
    
    // 构造函数
    public User() {}
    
    // Getter和Setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    
    public String getConfirmPassword() { return confirmPassword; }
    public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; }
    
    public Date getBirthDate() { return birthDate; }
    public void setBirthDate(Date birthDate) { this.birthDate = birthDate; }
    
    public String getGender() { return gender; }
    public void setGender(String gender) { this.gender = gender; }
    
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
}

控制器处理

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 显示注册表单
    @GetMapping("/register")
    public String showRegisterForm(Model model) {
        model.addAttribute("user", new User());
        return "user/register-form";
    }
    
    // 处理注册提交
    @PostMapping("/register")
    public String processRegistration(@ModelAttribute User user,
                                     BindingResult bindingResult,
                                     Model model) {
        
        // 手动验证
        if (user.getPassword() != null && user.getConfirmPassword() != null &&
            !user.getPassword().equals(user.getConfirmPassword())) {
            bindingResult.rejectValue("confirmPassword", "password.mismatch",
                                      "两次输入的密码不一致");
        }
        
        if (bindingResult.hasErrors()) {
            return "user/register-form";
        }
        
        // 保存用户
        userService.save(user);
        return "redirect:/user/success";
    }
    
    // 显示成功页面
    @GetMapping("/success")
    public String showSuccess() {
        return "user/success";
    }
}

HTML表单页面

传统HTML表单

<!-- register-form.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>用户注册</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .form-group { margin-bottom: 15px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
        .form-group input, .form-group select { 
            width: 300px; padding: 8px; border: 1px solid #ddd; 
            border-radius: 3px; box-sizing: border-box; 
        }
        .error { color: red; font-size: 14px; }
        .btn { padding: 10px 20px; margin: 5px; border: none; 
               border-radius: 3px; cursor: pointer; }
        .btn-primary { background-color: #007bff; color: white; }
        .btn-secondary { background-color: #6c757d; color: white; }
    </style>
</head>
<body>
    <h1>用户注册</h1>
    
    <form action="<c:url value='/user/register'/>" method="post">
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" 
                   value="${user.username}" placeholder="请输入用户名"/>
            <c:if test="${not empty errors}">
                <c:forEach var="error" items="${errors}">
                    <c:if test="${error.field == 'username'}">
                        <span class="error">${error.defaultMessage}</span>
                    </c:if>
                </c:forEach>
            </c:if>
        </div>
        
        <div class="form-group">
            <label for="email">邮箱:</label>
            <input type="email" id="email" name="email" 
                   value="${user.email}" placeholder="请输入邮箱"/>
        </div>
        
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" 
                   placeholder="请输入密码"/>
        </div>
        
        <div class="form-group">
            <label for="confirmPassword">确认密码:</label>
            <input type="password" id="confirmPassword" name="confirmPassword" 
                   placeholder="请再次输入密码"/>
        </div>
        
        <div class="form-group">
            <label for="gender">性别:</label>
            <select id="gender" name="gender">
                <option value="">请选择性别</option>
                <option value="male" ${user.gender == 'male' ? 'selected' : ''}>男</option>
                <option value="female" ${user.gender == 'female' ? 'selected' : ''}>女</option>
                <option value="other" ${user.gender == 'other' ? 'selected' : ''}>其他</option>
            </select>
        </div>
        
        <div class="form-group">
            <label for="birthDate">生日:</label>
            <input type="date" id="birthDate" name="birthDate" 
                   value="${user.birthDate}"/>
        </div>
        
        <div class="form-group">
            <label>
                <input type="checkbox" name="active" value="true" 
                       ${user.active ? 'checked' : ''}/>
                激活账户
            </label>
        </div>
        
        <div>
            <button type="submit" class="btn btn-primary">注册</button>
            <button type="reset" class="btn btn-secondary">重置</button>
            <a href="<c:url value='/user/login'/>" class="btn btn-secondary">已有账号?登录</a>
        </div>
    </form>
</body>
</html>

Spring Form标签库

Form标签库概述

Spring Form标签库提供了简化和增强的表单标签,与Spring MVC的数据绑定和验证机制紧密集成。

引入标签库

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

基础Form标签使用

form:form标签

<!-- 使用Spring form标签 -->
<form:form modelAttribute="user" action="/user/register" method="post" cssClass="registration-form">
    <!-- 表单内容 -->
</form:form>

<!-- 指定提交路径 -->
<form:form modelAttribute="user" action="/user/register">
    <!-- 等价于 action="/user/register" method="post" -->
</form:form>

<!-- 多选提交 -->
<form:form modelAttribute="user" action="/user/register" method="post" enctype="multipart/form-data">
    <!-- 文件上传表单 -->
</form:form>

输入标签

<form:form modelAttribute="user" action="/user/register">
    <!-- 文本输入 -->
    <div class="form-group">
        <label for="username">用户名:</label>
        <form:input path="username" id="username" placeholder="请输入用户名"/>
        <form:errors path="username" cssClass="error"/>
    </div>
    
    <!-- 密码输入 -->
    <div class="form-group">
        <label for="password">密码:</label>
        <form:password path="password" id="password" showPassword="false"/>
        <form:errors path="password" cssClass="error"/>
    </div>
    
    <!-- 隐藏字段 -->
    <form:hidden path="id"/>
    
    <!-- 文本区域 -->
    <div class="form-group">
        <label for="description">个人描述:</label>
        <form:textarea path="description" rows="5" cols="50" 
                       placeholder="请输入个人描述"/>
        <form:errors path="description" cssClass="error"/>
    </div>
</form:form>

选择标签

单选下拉框

<form:form modelAttribute="user">
    <!-- 性别选择 -->
    <div class="form-group">
        <label>性别:</label>
        <form:select path="gender">
            <form:option value="">请选择性别</form:option>
            <form:option value="male">男</form:option>
            <form:option value="female">女</form:option>
            <form:option value="other">其他</form:option>
        </form:select>
        <form:errors path="gender" cssClass="error"/>
    </div>
    
    <!-- 使用Map数据的选择框 -->
    <div class="form-group">
        <label>部门:</label>
        <form:select path="departmentId">
            <form:option value="">请选择部门</form:option>
            <form:options items="${departments}"/>
        </form:select>
    </div>
    
    <!-- 使用对象列表的选择框 -->
    <div class="form-group">
        <label>所属城市:</label>
        <form:select path="cityId">
            <form:option value="">请选择城市</form:option>
            <form:options items="${cities}" itemValue="id" itemLabel="name"/>
        </form:select>
    </div>
</form:form>

多选下拉框

<form:form modelAttribute="user">
    <!-- 爱好多选 -->
    <div class="form-group">
        <label>爱好:</label>
        <form:select path="hobbies" multiple="multiple" size="5">
            <form:options items="${hobbies}"/>
        </form:select>
        <form:errors path="hobbies" cssClass="error"/>
    </div>
</form:form>

单选按钮

<form:form modelAttribute="user">
    <div class="form-group">
        <label>账户类型:</label>
        <form:radiobuttons path="accountType" 
                           items="${accountTypes}" 
                           delimiter="&nbsp;&nbsp;|&nbsp;&nbsp;"/>
        <form:errors path="accountType" cssClass="error"/>
    </div>
    
    <!-- 单个单选按钮 -->
    <div class="form-group">
        <form:radiobutton path="active" value="true" id="active-true"/>
        <label for="active-true">激活账户</label>
    </div>
</form:form>

复选框

<form:form modelAttribute="user">
    <!-- 多个复选框 -->
    <div class="form-group">
        <label>订阅通知:</label>
        <form:checkboxes path="notifications" 
                         items="${notificationTypes}"
                         delimiter="<br/>"/>
        <form:errors path="notifications" cssClass="error"/>
    </div>
    
    <!-- 单个复选框 -->
    <div class="form-group">
        <form:checkbox path="agree" id="agree"/>
        <label for="agree">我同意<a href="#">服务条款</a></label>
        <form:errors path="agree" cssClass="error"/>
    </div>
</form:form>

高级Form标签功能

自定义CSS类和属性

<form:form modelAttribute="user" cssClass="user-form">
    <!-- 自定义CSS类 -->
    <form:input path="username" cssClass="form-control"/>
    
    <!-- 自定义属性 -->
    <form:input path="email" cssErrorClass="error-input" 
                cssClass="form-control" 
                placeholder="请输入邮箱地址"/>
    
    <!-- 多个CSS类 -->
    <form:input path="phone" 
                cssClass="form-control phone-input" 
                cssErrorClass="error-input"/>
</form:form>

错误显示控制

<form:form modelAttribute="user">
    <!-- 显示字段错误 -->
    <form:errors path="username" cssClass="field-error"/>
    
    <!-- 显示全局错误 -->
    <form:errors cssClass="global-error"/>
    
    <!-- 错误提示样式 -->
    <div class="field-container">
        <form:input path="email" cssErrorClass="error-field"/>
        <form:errors path="email" cssClass="error-message"/>
    </div>
</form:form>

数据绑定和转换

基础数据绑定

自动类型转换

@Controller
public class DataBindingController {
    
    @GetMapping("/user/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        // id自动从String转换为Long
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user/detail";
    }
    
    @PostMapping("/user/search")
    public String searchUsers(@RequestParam(defaultValue = "1") int page,
                             @RequestParam(defaultValue = "10") int size,
                             @RequestParam(required = false) Long minId,
                             @RequestParam(required = false) Long maxId,
                             Model model) {
        // 参数自动转换
        List<User> users = userService.search(page, size, minId, maxId);
        model.addAttribute("users", users);
        return "user/search-results";
    }
}

自定义类型转换

编写自定义转换器

// 日期转换器
@Component
public class StringToDateConverter implements Converter<String, Date> {
    
    @Override
    public Date convert(String source) {
        if (StringUtils.isEmpty(source)) {
            return null;
        }
        
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            return dateFormat.parse(source);
        } catch (ParseException e) {
            throw new IllegalArgumentException("无效的日期格式: " + source);
        }
    }
}

// 枚举转换器
@Component
public class StringToGenderConverter implements Converter<String, Gender> {
    
    @Override
    public Gender convert(String source) {
        if (StringUtils.isEmpty(source)) {
            return null;
        }
        
        try {
            return Gender.valueOf(source.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("无效的性别: " + source);
        }
    }
}

// 复合对象转换器
@Component
public class StringToAddressConverter implements Converter<String, Address> {
    
    @Override
    public Address convert(String source) {
        if (StringUtils.isEmpty(source)) {
            return new Address();
        }
        
        // 例如:格式 "城市, 省份"
        String[] parts = source.split(",");
        if (parts.length >= 2) {
            return new Address(parts[0].trim(), parts[1].trim());
        } else {
            throw new IllegalArgumentException("地址格式应为:城市, 省份");
        }
    }
}

注册转换器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private StringToDateConverter stringToDateConverter;
    
    @Autowired
    private StringToGenderConverter stringToGenderConverter;
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(stringToDateConverter);
        registry.addConverter(stringToGenderConverter);
    }
}

数组和集合绑定

数组绑定

@PostMapping("/user/batch")
public String batchUpdateUsers(@RequestParam Long[] userIds,
                              @RequestParam String action,
                              Model model) {
    // userIds自动绑定为Long数组
    List<Long> idList = Arrays.asList(userIds);
    int processed = userService.batchProcess(idList, action);
    
    model.addAttribute("processed", processed);
    return "user/batch-result";
}

集合绑定

// 表单绑定到集合
@PostMapping("/user/hobbies")
public String updateHobbies(@RequestParam List<String> hobbies,
                           @AuthenticationPrincipal User currentUser) {
    currentUser.setHobbies(hobbies);
    userService.update(currentUser);
    return "redirect:/user/profile";
}

// Map参数绑定
@PostMapping("/user/filter")
public String filterUsers(@RequestParam Map<String, String> filters,
                         Model model) {
    List<User> users = userService.findByFilters(filters);
    model.addAttribute("users", users);
    model.addAttribute("filters", filters);
    return "user/filter-results";
}

嵌套对象绑定

地址对象绑定

public class Address {
    private String street;
    private String city;
    private String province;
    private String zipCode;
    
    // getters and setters
}

public class User {
    private String name;
    private String email;
    private Address address;  // 嵌套对象
    
    // getters and setters
}

嵌套绑定处理

@PostMapping("/user/profile")
public String updateProfile(@ModelAttribute User user,
                           BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "user/profile-edit";
    }
    
    userService.updateProfile(user);
    return "redirect:/user/profile";
}

表单中的嵌套对象

<form:form modelAttribute="user">
    <div class="form-group">
        <label>姓名:</label>
        <form:input path="name"/>
        <form:errors path="name"/>
    </div>
    
    <div class="form-group">
        <label>邮箱:</label>
        <form:input path="email"/>
        <form:errors path="email"/>
    </div>
    
    <!-- 嵌套对象字段 -->
    <fieldset>
        <legend>地址信息</legend>
        <div class="form-group">
            <label>街道:</label>
            <form:input path="address.street"/>
            <form:errors path="address.street"/>
        </div>
        
        <div class="form-group">
            <label>城市:</label>
            <form:input path="address.city"/>
            <form:errors path="address.city"/>
        </div>
        
        <div class="form-group">
            <label>省份:</label>
            <form:input path="address.province"/>
            <form:errors path="address.province"/>
        </div>
        
        <div class="form-group">
            <label>邮编:</label>
            <form:input path="address.zipCode"/>
            <form:errors path="address.zipCode"/>
        </div>
    </fieldset>
</form:form>

数据验证机制

Spring验证框架

Validator接口实现

@Component
public class UserValidator implements Validator {
    
    @Override
    public boolean supports(Class<?> clazz) {
        return User.class.isAssignableFrom(clazz);
    }
    
    @Override
    public void validate(Object target, Errors errors) {
        User user = (User)target;
        
        // 用户名验证
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", 
                                                "username.required", "用户名不能为空");
        
        if (user.getUsername() != null && user.getUsername().length() < 3) {
            errors.rejectValue("username", "username.minlength", 
                               "用户名长度至少3个字符");
        }
        
        // 邮箱验证
        ValidationUtils.rejectIfEmpty(errors, "email", 
                                     "email.required", "邮箱不能为空");
        
        if (user.getEmail() != null && !isValidEmail(user.getEmail())) {
            errors.rejectValue("email", "email.format", 
                              "邮箱格式不正确");
        }
        
        // 密码验证
        ValidationUtils.rejectIfEmpty(errors, "password", 
                                     "password.required", "密码不能为空");
        
        if (user.getPassword() != null && user.getPassword().length() < 6) {
            errors.rejectValue("password", "password.minlength", 
                               "密码长度至少6个字符");
        }
        
        // 密码确认验证
        if (user.getPassword() != null && user.getConfirmPassword() != null &&
            !user.getPassword().equals(user.getConfirmPassword())) {
            errors.rejectValue("confirmPassword", "password.mismatch", 
                              "两次输入的密码不一致");
        }
        
        // 生日验证
        if (user.getBirthDate() != null) {
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.YEAR, -120);
            if (user.getBirthDate().before(cal.getTime())) {
                errors.rejectValue("birthDate", "birthdate.too.old", 
                                   "生日日期不能超过120年");
            }
            
            cal = Calendar.getInstance();
            cal.add(Calendar.YEAR, -10);
            if (user.getBirthDate().after(cal.getTime())) {
                errors.rejectValue("birthDate", "birthdate.too.young", 
                                   "注册年龄必须大于10岁");
            }
        }
    }
    
    private boolean isValidEmail(String email) {
        String emailRegex = "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$";
        return email.matches(emailRegex);
    }
}

控制器中使用验证器

@Controller
public class UserController {
    
    @Autowired
    private UserValidator userValidator;
    
    @PostMapping("/user/register")
    public String registerUser(@ModelAttribute User user,
                               BindingResult bindingResult,
                               Model model) {
        
        // 执行验证
        userValidator.validate(user, bindingResult);
        
        // 业务逻辑验证
        if (user.getEmail() != null && 
            userService.existsByEmail(user.getEmail())) {
            bindingResult.rejectValue("email", "email.exists", "邮箱已存在");
        }
        
        if (bindingResult.hasErrors()) {
            return "user/register-form";
        }
        
        // 保存用户
        userService.save(user);
        return "redirect:/user/success";
    }
}

分组验证

定义验证组

// 接口定义验证组
public interface UserGroup {
    interface Create {}
    interface Update {}
    interface PasswordChange {}
}

// 实体类中使用验证组
public class User {
    @NotNull(groups = {UserGroup.Create.class, UserGroup.Update.class})
    @Size(min = 3, max = 20, groups = UserGroup.Create.class)
    private String username;
    
    @NotBlank(groups = UserGroup.Create.class)
    @Email(groups = {UserGroup.Create.class, UserGroup.Update.class})
    private String email;
    
    @NotBlank(groups = UserGroup.Create.class)
    @Size(min = 6, groups = UserGroup.Create.class)
    private String password;
    
    @NotBlank(groups = UserGroup.PasswordChange.class)
    @Size(min = 6, groups = UserGroup.PasswordChange.class)
    private String newPassword;
    
    @NotNull(groups = {UserGroup.Create.class, UserGroup.Update.class})
    @Past(groups = UserGroup.Create.class)
    private Date birthDate;
    
    // getters and setters
}

控制器中的分组验证

@Controller
public class UserController {
    
    @PostMapping("/user/register")
    public String register(@Validated(UserGroup.Create.class) @ModelAttribute User user,
                          BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/register-form";
        }
        
        userService.save(user);
        return "redirect:/user/success";
    }
    
    @PostMapping("/user/profile")
    public String updateProfile(@Validated(UserGroup.Update.class) @ModelAttribute User user,
                               BindingResult bindingResult,
                               @AuthenticationPrincipal User currentUser) {
        if (bindingResult.hasErrors()) {
            return "user/profile-edit";
        }
        
        currentUser.setUsername(user.getUsername());
        currentUser.setEmail(user.getEmail());
        currentUser.setBirthDate(user.getBirthDate());
        
        userService.update(currentUser);
        return "redirect:/user/profile";
    }
    
    @PostMapping("/user/change-password")
    public String changePassword(@Validated(UserGroup.PasswordChange.class) @ModelAttribute User user,
                                BindingResult bindingResult,
                                @AuthenticationPrincipal User currentUser) {
        if (bindingResult.hasErrors()) {
            return "user/change-password";
        }
        
        // 验证当前密码
        if (!userService.verifyPassword(currentUser, user.getPassword())) {
            bindingResult.rejectValue("password", "password.incorrect", "当前密码错误");
            return "user/change-password";
        }
        
        userService.changePassword(currentUser.getId(), user.getNewPassword());
        return "redirect:/user/profile?password=changed";
    }
}

JSR-303 Bean验证

Hibernate Validator集成

Maven依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.5.Final</version>
</dependency>

<dependency>
    <groupId>org.glassfish.expressly</groupId>
    <artifactId>expressly</artifactId>
    <version>5.0.0</version>
</dependency>

常用验证注解

基础验证注解使用

public class UserRegistrationForm {
    
    @NotNull(message = "用户名不能为空")
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @Size(max = 100, message = "邮箱长度不能超过100个字符")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 50, message = "密码长度必须在6-50个字符之间")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{6,}$", 
             message = "密码必须包含至少一个字母和一个数字")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    
    @NotNull(message = "生日不能为空")
    @Past(message = "生日必须是过去的时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthDate;
    
    @NotNull(message = "性别不能为空")
    private String gender;
    
    @NotNull(message = "同意条款不能为空")
    @AssertTrue(message = "必须同意用户协议")
    private boolean agreeTerms;
    
    // getters and setters
}

数值验证注解

public class ProductForm {
    
    @NotNull(message = "价格不能为空")
    @DecimalMin(value = "0.01", message = "价格必须大于0")
    @DecimalMax(value = "999999.99", message = "价格不能超过999999.99")
    @Digits(integer = 6, fraction = 2, message = "价格格式不正确")
    private BigDecimal price;
    
    @Min(value = 0, message = "库存数量不能小于0")
    @Max(value = 999999, message = "库存数量不能超过999999")
    private Integer stock;
    
    @Positive(message = "数量必须为正数")
    private Integer quantity;
    
    @NegativeOrZero(message = "折扣不能为正数")
    private BigDecimal discount;
    
    // getters and setters
}

自定义验证注解

创建自定义验证注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "邮箱已存在";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 对应验证器
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    
    @Autowired
    private UserService userService;
    
    @Override
    public void initialize(UniqueEmail constraintAnnotation) {
        // 初始化验证器
    }
    
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null || email.trim().isEmpty()) {
            return true; // 空值由@NotBlank处理
        }
        
        return !userService.existsByEmail(email);
    }
}

// 使用自定义验证注解
public class UserForm {
    @UniqueEmail(message = "该邮箱已被注册")
    @Email(message = "邮箱格式不正确")
    @NotBlank(message = "邮箱不能为空")
    private String email;
    
    // getters and setters
}

复杂自定义验证

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
public @interface PasswordMatches {
    String message() default "两次输入的密码不一致";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Component
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, UserRegistrationForm> {
    
    @Override
    public void initialize(PasswordMatches constraintAnnotation) {
        // 初始化
    }
    
    @Override
    public boolean isValid(UserRegistrationForm form, ConstraintValidatorContext context) {
        if (form.getPassword() == null || form.getConfirmPassword() == null) {
            return true; // 其他注解会处理空值
        }
        
        boolean matches = form.getPassword().equals(form.getConfirmPassword());
        
        if (!matches) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("两次输入的密码不一致")
                   .addPropertyNode("confirmPassword")
                   .addConstraintViolation();
        }
        
        return matches;
    }
}

// 类级别验证
@PasswordMatches
public class UserRegistrationForm {
    @NotBlank(message = "密码不能为空")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    
    // getters and setters
}

错误处理和显示

BindingResult错误处理

控制器中的错误处理

@Controller
public class ValidationController {
    
    @PostMapping("/user/register")
    public String register(@Valid @ModelAttribute User user,
                          BindingResult bindingResult,
                          Model model,
                          HttpServletRequest request) {
        
        // 全局错误处理
        if (bindingResult.hasErrors()) {
            // 添加表单辅助数据
            populateFormData(model);
            
            // 记录错误日志
            logBindingErrors(bindingResult);
            
            return "user/register-form";
        }
        
        // 处理业务逻辑
        userService.save(user);
        return "redirect:/user/success";
    }
    
    private void populateFormData(Model model) {
        // 添加选项数据
        model.addAttribute("genders", Arrays.asList("male", "female", "other"));
        model.addAttribute("departments", departmentService.getAllDepartments());
    }
    
    private void logBindingErrors(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            logger.warn("表单验证失败: {}", bindingResult.getErrorCount());
            
            for (FieldError error : bindingResult.getFieldErrors()) {
                logger.warn("字段错误 - {}: {}", error.getField(), error.getDefaultMessage());
            }
            
            for (ObjectError error : bindingResult.getGlobalErrors()) {
                logger.warn("全局错误: {}", error.getDefaultMessage());
            }
        }
    }
}

错误信息显示

JSP页面错误显示

<form:form modelAttribute="user" action="/user/register">
    
    <!-- 全局错误显示 -->
    <div class="error-summary">
        <form:errors path="*" cssClass="global-error"/>
    </div>
    
    <div class="form-group">
        <label for="username">用户名:</label>
        <form:input path="username" 
                    cssClass="form-control" 
                    cssErrorClass="form-control error"/>
        <form:errors path="username" cssClass="field-error"/>
    </div>
    
    <div class="form-group">
        <label for="email">邮箱:</label>
        <form:input path="email" 
                    cssClass="form-control"
                    cssErrorClass="form-control error"/>
        <form:errors path="email" cssClass="field-error"/>
    </div>
    
    <div class="form-group">
        <label for="password">密码:</label>
        <form:password path="password" 
                       cssClass="form-control"
                       cssErrorClass="form-control error"/>
        <form:errors path="password" cssClass="field-error"/>
    </div>
    
    <div class="form-group">
        <label for="confirmPassword">确认密码:</label>
        <form:password path="confirmPassword" 
                       cssClass="form-control"
                       cssErrorClass="form-control error"/>
        <form:errors path="confirmPassword" cssClass="field-error"/>
    </div>
    
    <!-- 自定义错误样式 -->
    <style>
        .field-error {
            color: red;
            font-size: 14px;
            margin-top: 5px;
        }
        .global-error {
            background-color: #f8d7da;
            color: #721c24;
            padding: 15px;
            margin-bottom: 20px;
            border-radius: 5px;
        }
        .form-control.error {
            border-color: #dc3545;
            box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
        }
    </style>
</form:form>

JavaScript客户端验证

<!-- 内联JavaScript验证 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    const form = document.querySelector('form');
    const submitButton = form.querySelector('button[type="submit"]');
    
    // 实时验证
    const inputs = form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
        input.addEventListener('blur', function() {
            validateField(this);
        });
        
        input.addEventListener('input', function() {
            clearFieldError(this);
        });
    });
    
    // 表单提交验证
    form.addEventListener('submit', function(e) {
        let isValid = true;
        
        inputs.forEach(input => {
            if (!validateField(input单)) {
                isValid = false;
            }
        });
        
        if (!isValid) {
            e.preventDefault();
        }
    });
    
    function validateField(field) {
        const fieldName = field.name;
        const value = field.value.trim();
        
        // 清空之前的错误
        clearFieldError(field);
        
        // 验证逻辑
        let errorMessage = '';
        
        if (field.hasAttribute('required') && !value) {
            errorMessage = fieldName + '不能为空';
        } else if (field.type === 'email' && value && !isValidEmail(value)) {
            errorMessage = '邮箱格式不正确';
        } else if (field.name === 'username' && value && value.length < 3) {
            errorMessage = '用户名长度至少3个字符';
        } else if (field.name === 'password' && value && value.length < 6) {
            errorMessage = '密码长度至少6个字符';
        }
        
        if (errorMessage) {
            showFieldError(field, errorMessage);
            return false;
        }
        
        return true;
    }
    
    function clearFieldError(field) {
        const errorElement = field.parentNode.querySelector('.field-error');
        if (errorElement) {
            errorElement.remove();
        }
        field.classList.remove('error');
    }
    
    function showFieldError(field, message) {
        const errorElement = document.createElement('span');
        errorElement.className = 'field-error';
        errorElement.textContent = message;
        field.parentNode.appendChild(errorElement);
        field.classList.add('error');
    }
    
    function isValidEmail(email) {
        const emailRegex = /^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$/;
        return emailRegex.test(email);
    }
});
</script>

国际化错误消息

消息资源文件

# messages.properties
user.username.required=用户名不能为空
user.username.size=用户名长度必须在{2}和{1}个字符之间
user.username.pattern=用户名只能包含字母、数字和下划线
user.email.required=邮箱不能为空
user.email.format=邮箱格式不正确
user.password.required=密码不能为空
user.password.size=密码长度必须在{2}和{1}个字符之间
user.passwordConfirm.mismatch=两次输入的密码不一致

# messages_zh_CN.properties
user.username.required=用户名不能为空
user.username.size=用户名长度必须在{2}和{1}个字符之间
user.username.pattern=用户名只能包含字母、数字和下划线
user.email.required=邮箱不能为空
user.email.format=邮箱格式不正确
user.password.required=密码不能为空
user.password.size=密码长度必须在{2}和{1}个字符之间
user.passwordConfirm.mismatch=两次输入的密码不一致

配置国际化解析器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(3600);
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }
    
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }
    
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

这个教程已经完成了表单处理和验证模块的主要部分。由于篇幅限制和token限制,我会继续创建其他模块的教程。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小凯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值