1. 概述
通常在需要验证用户输入时,Spring MVC 提供了标准的预定义验证器。但当需要验证更特殊的输入类型时,我们完全可以创建自己的自定义验证逻辑。
本教程将带你实现两个场景:创建自定义验证器验证表单中的电话号码字段,以及实现跨多字段的自定义验证器。
⚠️ 本文聚焦 Spring MVC,关于 Spring Boot 中的自定义验证可参考《Spring Boot 中的验证》一文。
2. 环境准备
在 pom.xml
中添加核心依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.10.Final</version>
</dependency>
最新版本可查看 Maven 仓库。
✅ 若使用 Spring Boot,添加 spring-boot-starter-web
即可自动引入该依赖。
3. 自定义验证实现
创建自定义验证器需要两个核心步骤:
- 定义自定义注解
- 实现验证逻辑
下面我们以电话号码验证为例:要求必须是数字,长度在 8-11 位之间。
4. 创建自定义注解
定义注解接口 @ContactNumberConstraint
:
@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
关键点说明:
@Constraint
指定验证逻辑实现类message()
定义错误提示信息- 其他为 Spring 标准要求的样板代码
5. 实现验证器
创建验证器类 ContactNumberValidator
:
public class ContactNumberValidator implements
ConstraintValidator<ContactNumberConstraint, String> {
@Override
public void initialize(ContactNumberConstraint contactNumber) {
}
@Override
public boolean isValid(String contactField,
ConstraintValidatorContext cxt) {
return contactField != null && contactField.matches("[0-9]+")
&& (contactField.length() > 8) && (contactField.length() < 14);
}
}
核心要点:
- 实现
ConstraintValidator
接口 - 在
isValid()
方法中编写验证规则 - 当前规则:非空、纯数字、长度 9-13 位(实际业务中建议更严格的正则)
⚠️
ConstraintValidator
实现需满足:
- 目标对象必须是非参数化类型
- 泛型参数必须是无界通配符类型
6. 应用验证注解
在模型类中使用自定义注解:
@ContactNumberConstraint
private String phone;
控制器中处理验证逻辑:
@Controller
public class ValidatedPhoneController {
@GetMapping("/validatePhone")
public String loadFormPage(Model m) {
m.addAttribute("validatedPhone", new ValidatedPhone());
return "phoneHome";
}
@PostMapping("/addValidatePhone")
public String submitForm(@Valid ValidatedPhone validatedPhone,
BindingResult result, Model m) {
if(result.hasErrors()) {
return "phoneHome";
}
m.addAttribute("message", "Successfully saved phone: "
+ validatedPhone.toString());
return "phoneHome";
}
}
关键点:
@Valid
触发验证BindingResult
捕获验证错误- 验证失败时返回原表单页面
7. 视图层实现
JSP 表单页面 phoneHome.jsp
:
<form:form
action="/${pageContext.request.contextPath}/addValidatePhone"
modelAttribute="validatedPhone">
<label for="phoneInput">Phone: </label>
<form:input path="phone" id="phoneInput" />
<form:errors path="phone" cssClass="error" />
<input type="submit" value="Submit" />
</form:form>
核心元素:
form:input
绑定模型字段form:errors
显示验证错误信息
8. 测试验证
8.1 页面访问测试
@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
this.mockMvc.
perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}
8.2 验证逻辑测试
@Test
public void
givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() {
this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone").
accept(MediaType.TEXT_HTML).
param("phoneInput", "123")).
andExpect(model().attributeHasFieldErrorCode(
"validatedPhone","phone","ContactNumberConstraint")).
andExpect(view().name("phoneHome")).
andExpect(status().isOk()).
andDo(print());
}
测试要点:
- 提交无效数据 "123"(长度不足)
- 验证返回错误码
ContactNumberConstraint
- 确保页面正常渲染
9. 类级别自定义验证
当需要验证多个字段的关联关系时(如密码确认),可创建类级别验证器。
9.1 创建注解
定义 @FieldsValueMatch
注解:
@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {
String message() default "Fields values don't match!";
String field();
String fieldMatch();
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
FieldsValueMatch[] value();
}
}
关键设计:
- 支持指定两个比较字段名
- 内嵌
List
接口支持多重验证
9.2 实现验证器
public class FieldsValueMatchValidator
implements ConstraintValidator<FieldsValueMatch, Object> {
private String field;
private String fieldMatch;
public void initialize(FieldsValueMatch constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
public boolean isValid(Object value,
ConstraintValidatorContext context) {
Object fieldValue = new BeanWrapperImpl(value)
.getPropertyValue(field);
Object fieldMatchValue = new BeanWrapperImpl(value)
.getPropertyValue(fieldMatch);
if (fieldValue != null) {
return fieldValue.equals(fieldMatchValue);
} else {
return fieldMatchValue == null;
}
}
}
实现技巧:
- 使用
BeanWrapperImpl
动态获取字段值 - 处理 null 值的边界情况
9.3 应用注解
在用户注册表单中验证邮箱和密码:
@FieldsValueMatch.List({
@FieldsValueMatch(
field = "password",
fieldMatch = "verifyPassword",
message = "Passwords do not match!"
),
@FieldsValueMatch(
field = "email",
fieldMatch = "verifyEmail",
message = "Email addresses do not match!"
)
})
public class NewUserForm {
private String email;
private String verifyEmail;
private String password;
private String verifyPassword;
// 标准构造器、getter/setter
}
控制器实现:
@Controller
public class NewUserController {
@GetMapping("/user")
public String loadFormPage(Model model) {
model.addAttribute("newUserForm", new NewUserForm());
return "userHome";
}
@PostMapping("/user")
public String submitForm(@Valid NewUserForm newUserForm,
BindingResult result, Model model) {
if (result.hasErrors()) {
return "userHome";
}
model.addAttribute("message", "Valid form");
return "userHome";
}
}
9.4 测试类验证
有效数据测试
@Test
public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML).
param("email", "user@example.com")
.param("verifyEmail", "user@example.com")
.param("password", "pass")
.param("verifyPassword", "pass"))
.andExpect(model().errorCount(0))
.andExpect(status().isOk());
}
无效数据测试
@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML)
.param("email", "user@example.com")
.param("verifyEmail", "wrong@example.com")
.param("password", "pass")
.param("verifyPassword", "passsss"))
.andExpect(model().errorCount(2))
.andExpect(status().isOk());
}
测试要点:
- 有效数据:邮箱和密码均匹配
- 无效数据:故意制造不匹配
- 验证错误数量(预期2个错误)
10. 总结
本文系统介绍了 Spring MVC 中自定义验证的两种实现方式:
- 字段级验证:通过自定义注解验证单个字段
- 类级验证:通过类注解验证多字段关联关系
核心步骤总结:
- 创建注解定义验证规则
- 实现
ConstraintValidator
接口编写验证逻辑 - 在模型类中应用注解
- 控制器中使用
@Valid
触发验证 - 通过
BindingResult
处理验证结果
完整代码示例可在 GitHub 获取。