目录
- 表单处理概述
- 基础表单创建
- Spring Form标签库
- 数据绑定和转换
- 数据验证机制
- JSR-303Bean验证
- 自定义验证器
- 错误处理和显示
- 复杂表单处理
- 文件上传处理
- 表单国际化
- AJAX表单处理
- 最佳实践
- 常见问题解决
- 总结
表单处理概述
在Web应用中,表单是用户与系统交互的重要方式。Spring MVC提供了强大的表单处理机制,包括数据绑定、验证、错误处理等完整功能。
表单处理流程
- 表单显示: 控制器返回包含表单的页面
- 用户提交: 用户填写信息并提交表单
- 数据绑定: Spring MVC将请求参数绑定到对象
- 数据验证: 验证用户输入的数据
- 错误处理: 如果有验证错误,返回表单页面
- 数据处理: 验证通过后,执行业务逻辑
- 结果反馈: 返回成功页面或重定向
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=" | "/>
<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限制,我会继续创建其他模块的教程。
648

被折叠的 条评论
为什么被折叠?



