SpringMVC 数据绑定与验证
2025/9/17大约 9 分钟
SpringMVC 数据绑定与验证
学习目标
通过本教程,您将掌握:
- SpringMVC 数据绑定机制
- 自定义类型转换器
- Bean Validation 数据验证
- 自定义验证器的实现
- 国际化错误消息
数据绑定基础
什么是数据绑定?
数据绑定是将 HTTP 请求参数自动转换为 Java 对象的过程。SpringMVC 提供了强大的数据绑定功能,支持:
- 基本类型绑定:String、int、boolean 等
- 对象绑定:将表单数据绑定到 Java Bean
- 集合绑定:List、Set、Map 等集合类型
- 嵌套对象绑定:复杂对象的嵌套绑定
基本数据绑定
@Controller
@RequestMapping("/binding")
public class DataBindingController {
/**
* 基本类型绑定
*/
@GetMapping("/basic")
@ResponseBody
public String basicBinding(@RequestParam String name,
@RequestParam int age,
@RequestParam boolean active,
@RequestParam double salary) {
return String.format("姓名: %s, 年龄: %d, 激活: %b, 薪资: %.2f",
name, age, active, salary);
}
/**
* 日期类型绑定
*/
@GetMapping("/date")
@ResponseBody
public String dateBinding(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date createTime) {
return String.format("生日: %s, 创建时间: %s", birthday, createTime);
}
/**
* 数组绑定
*/
@GetMapping("/array")
@ResponseBody
public String arrayBinding(@RequestParam String[] hobbies,
@RequestParam int[] scores) {
return String.format("爱好: %s, 分数: %s",
Arrays.toString(hobbies), Arrays.toString(scores));
}
}
对象绑定
简单对象绑定
首先定义用户实体类:
package com.example.springmvc.model;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.*;
import java.util.Date;
import java.util.List;
@Data
public class User {
@NotNull(message = "ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 100, message = "年龄不能大于100岁")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Past(message = "生日必须是过去的日期")
private Date birthday;
private Address address; // 嵌套对象
private List<String> hobbies; // 集合属性
}
地址实体类:
package com.example.springmvc.model;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class Address {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
private String detail;
private String zipCode;
}
对象绑定控制器
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 显示用户表单
*/
@GetMapping("/form")
public String showForm(Model model) {
model.addAttribute("user", new User());
return "user-form";
}
/**
* 处理表单提交 - 对象绑定
*/
@PostMapping("/save")
public String saveUser(@Valid @ModelAttribute User user,
BindingResult result,
Model model) {
// 检查验证错误
if (result.hasErrors()) {
model.addAttribute("errors", result.getAllErrors());
return "user-form";
}
// 模拟保存用户
user.setId(System.currentTimeMillis());
model.addAttribute("user", user);
model.addAttribute("message", "用户保存成功!");
return "user-success";
}
/**
* JSON 格式的用户创建
*/
@PostMapping("/api/create")
@ResponseBody
public ResponseEntity<?> createUser(@Valid @RequestBody User user,
BindingResult result) {
if (result.hasErrors()) {
Map<String, String> errors = new HashMap<>();
result.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
user.setId(System.currentTimeMillis());
return ResponseEntity.ok(user);
}
}
类型转换
内置类型转换器
SpringMVC 内置了常用的类型转换器:
- String → 基本类型(int, long, boolean 等)
- String → 日期类型(Date, LocalDate 等)
- String → 枚举类型
- 数组和集合转换
自定义类型转换器
1. 实现 Converter 接口
package com.example.springmvc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 字符串转 LocalDate 转换器
*/
@Component
public class StringToLocalDateConverter implements Converter<String, LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDate convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
try {
return LocalDate.parse(source.trim(), FORMATTER);
} catch (Exception e) {
throw new IllegalArgumentException("日期格式错误,请使用 yyyy-MM-dd 格式");
}
}
}
2. 自定义枚举转换器
package com.example.springmvc.model;
public enum UserStatus {
ACTIVE("激活"),
INACTIVE("未激活"),
SUSPENDED("暂停"),
DELETED("已删除");
private final String description;
UserStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public static UserStatus fromString(String value) {
for (UserStatus status : values()) {
if (status.name().equalsIgnoreCase(value) ||
status.description.equals(value)) {
return status;
}
}
throw new IllegalArgumentException("无效的用户状态: " + value);
}
}
package com.example.springmvc.converter;
import com.example.springmvc.model.UserStatus;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
/**
* 字符串转用户状态枚举转换器
*/
@Component
public class StringToUserStatusConverter implements Converter<String, UserStatus> {
@Override
public UserStatus convert(String source) {
if (source == null || source.trim().isEmpty()) {
return null;
}
return UserStatus.fromString(source.trim());
}
}
3. 配置转换器
<!-- 在 spring-mvc.xml 中配置 -->
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.example.springmvc.converter.StringToLocalDateConverter"/>
<bean class="com.example.springmvc.converter.StringToUserStatusConverter"/>
</set>
</property>
</bean>
数据验证
Bean Validation 注解
常用验证注解
点击展开常用验证注解
package com.example.springmvc.model;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
import javax.validation.groups.Default;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
public class Product {
// 基础验证
@NotNull(message = "产品ID不能为空")
@Positive(message = "产品ID必须为正数")
private Long id;
@NotBlank(message = "产品名称不能为空")
@Size(min = 2, max = 50, message = "产品名称长度必须在2-50之间")
private String name;
@Size(max = 500, message = "产品描述不能超过500个字符")
private String description;
// 数值验证
@NotNull(message = "价格不能为空")
@DecimalMin(value = "0.01", message = "价格必须大于0.01")
@DecimalMax(value = "999999.99", message = "价格不能超过999999.99")
@Digits(integer = 6, fraction = 2, message = "价格格式不正确")
private BigDecimal price;
@Min(value = 0, message = "库存不能为负数")
@Max(value = 99999, message = "库存不能超过99999")
private Integer stock;
// 范围验证
@Range(min = 1, max = 5, message = "评分必须在1-5之间")
private Integer rating;
// 日期验证
@PastOrPresent(message = "创建日期不能是未来日期")
private LocalDate createDate;
@Future(message = "过期日期必须是未来日期")
private LocalDate expireDate;
// 正则表达式验证
@Pattern(regexp = "^[A-Z]{2,3}\\d{4,6}$", message = "产品编码格式不正确")
private String productCode;
// 自定义验证
@ValidCategory
private String category;
}
自定义验证注解
1. 定义验证注解
package com.example.springmvc.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 自定义分类验证注解
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CategoryValidator.class)
@Documented
public @interface ValidCategory {
String message() default "无效的产品分类";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2. 实现验证器
package com.example.springmvc.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
/**
* 分类验证器实现
*/
public class CategoryValidator implements ConstraintValidator<ValidCategory, String> {
private static final List<String> VALID_CATEGORIES = Arrays.asList(
"电子产品", "服装", "食品", "图书", "家居", "运动", "美妆", "汽车"
);
@Override
public void initialize(ValidCategory constraintAnnotation) {
// 初始化方法,可以获取注解参数
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return true; // 空值由 @NotBlank 等注解处理
}
boolean isValid = VALID_CATEGORIES.contains(value.trim());
if (!isValid) {
// 自定义错误消息
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
String.format("分类 '%s' 无效,有效分类: %s", value, VALID_CATEGORIES)
).addConstraintViolation();
}
return isValid;
}
}
验证组
1. 定义验证组
package com.example.springmvc.validation;
/**
* 验证组接口
*/
public interface ValidationGroups {
interface Create {}
interface Update {}
interface Delete {}
}
2. 使用验证组
@Data
public class User {
@NotNull(groups = ValidationGroups.Update.class, message = "更新时ID不能为空")
@Null(groups = ValidationGroups.Create.class, message = "创建时ID必须为空")
private Long id;
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class},
message = "用户名不能为空")
private String username;
@Email(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class},
message = "邮箱格式不正确")
private String email;
}
3. 控制器中使用验证组
@RestController
@RequestMapping("/api/users")
public class UserApiController {
/**
* 创建用户 - 使用创建验证组
*/
@PostMapping
public ResponseEntity<?> createUser(
@Validated(ValidationGroups.Create.class) @RequestBody User user,
BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(getErrorMap(result));
}
user.setId(System.currentTimeMillis());
return ResponseEntity.ok(user);
}
/**
* 更新用户 - 使用更新验证组
*/
@PutMapping("/{id}")
public ResponseEntity<?> updateUser(
@PathVariable Long id,
@Validated(ValidationGroups.Update.class) @RequestBody User user,
BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(getErrorMap(result));
}
user.setId(id);
return ResponseEntity.ok(user);
}
/**
* 提取验证错误信息
*/
private Map<String, String> getErrorMap(BindingResult result) {
Map<String, String> errors = new HashMap<>();
result.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return errors;
}
}
国际化错误消息
1. 配置消息源
<!-- 在 spring-mvc.xml 中配置消息源 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages.validation</value>
<value>messages.error</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="300"/>
</bean>
<!-- 配置验证器使用消息源 -->
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<mvc:annotation-driven validator="validator"/>
2. 创建消息文件
创建 src/main/resources/messages/validation_zh_CN.properties
:
# 用户验证消息
user.id.notnull=用户ID不能为空
user.username.notblank=用户名不能为空
user.username.size=用户名长度必须在{min}-{max}之间
user.email.email=邮箱格式不正确
user.email.notblank=邮箱不能为空
user.age.min=年龄不能小于{value}岁
user.age.max=年龄不能大于{value}岁
user.phone.pattern=手机号格式不正确
user.birthday.past=生日必须是过去的日期
# 产品验证消息
product.name.notblank=产品名称不能为空
product.price.notnull=价格不能为空
product.price.min=价格必须大于{value}
product.stock.min=库存不能为负数
product.category.invalid=无效的产品分类
创建 src/main/resources/messages/validation_en_US.properties
:
# User validation messages
user.id.notnull=User ID cannot be null
user.username.notblank=Username cannot be blank
user.username.size=Username length must be between {min} and {max}
user.email.email=Invalid email format
user.email.notblank=Email cannot be blank
user.age.min=Age cannot be less than {value}
user.age.max=Age cannot be greater than {value}
user.phone.pattern=Invalid phone number format
user.birthday.past=Birthday must be a past date
# Product validation messages
product.name.notblank=Product name cannot be blank
product.price.notnull=Price cannot be null
product.price.min=Price must be greater than {value}
product.stock.min=Stock cannot be negative
product.category.invalid=Invalid product category
3. 使用国际化消息
@Data
public class User {
@NotNull(message = "{user.id.notnull}")
private Long id;
@NotBlank(message = "{user.username.notblank}")
@Size(min = 2, max = 20, message = "{user.username.size}")
private String username;
@Email(message = "{user.email.email}")
@NotBlank(message = "{user.email.notblank}")
private String email;
@Min(value = 18, message = "{user.age.min}")
@Max(value = 100, message = "{user.age.max}")
private Integer age;
}
复杂验证场景
1. 跨字段验证
package com.example.springmvc.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 密码确认验证注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
@Documented
public @interface PasswordMatch {
String message() default "密码和确认密码不匹配";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String password();
String confirmPassword();
}
package com.example.springmvc.validation;
import org.springframework.beans.BeanWrapperImpl;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 密码匹配验证器
*/
public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object> {
private String passwordField;
private String confirmPasswordField;
@Override
public void initialize(PasswordMatch constraintAnnotation) {
this.passwordField = constraintAnnotation.password();
this.confirmPasswordField = constraintAnnotation.confirmPassword();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapperImpl wrapper = new BeanWrapperImpl(value);
Object password = wrapper.getPropertyValue(passwordField);
Object confirmPassword = wrapper.getPropertyValue(confirmPasswordField);
boolean isValid = (password == null && confirmPassword == null) ||
(password != null && password.equals(confirmPassword));
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(confirmPasswordField)
.addConstraintViolation();
}
return isValid;
}
}
2. 使用跨字段验证
@Data
@PasswordMatch(password = "password", confirmPassword = "confirmPassword",
message = "密码和确认密码不匹配")
public class UserRegistration {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
@Email(message = "邮箱格式不正确")
private String email;
}
最佳实践
1. 验证策略
验证原则
- 前端验证:提升用户体验,快速反馈
- 后端验证:确保数据安全,防止恶意请求
- 分层验证:Controller 层验证格式,Service 层验证业务规则
- 国际化支持:提供多语言错误消息
2. 错误处理统一化
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(
MethodArgumentNotValidException e) {
Map<String, Object> response = new HashMap<>();
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
response.put("success", false);
response.put("message", "数据验证失败");
response.put("errors", errors);
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.badRequest().body(response);
}
/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handleBindException(BindException e) {
Map<String, Object> response = new HashMap<>();
Map<String, String> errors = new HashMap<>();
e.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
response.put("success", false);
response.put("message", "参数绑定失败");
response.put("errors", errors);
return ResponseEntity.badRequest().body(response);
}
}
3. 性能优化
/**
* 验证配置优化
*/
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages.validation", "messages.error");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600); // 缓存1小时
return messageSource;
}
}
总结
本教程全面介绍了 SpringMVC 的数据绑定和验证机制:
- ✅ 数据绑定:基本类型、对象、集合的自动绑定
- ✅ 类型转换:内置转换器和自定义转换器
- ✅ Bean Validation:标准验证注解的使用
- ✅ 自定义验证:自定义验证注解和验证器
- ✅ 验证组:不同场景下的验证策略
- ✅ 国际化:多语言错误消息支持
- ✅ 最佳实践:统一错误处理和性能优化
下一步学习
- 学习 SpringMVC 视图解析与模板引擎
- 了解拦截器和过滤器的使用
- 掌握文件上传和下载处理
掌握了数据绑定和验证,您就能构建健壮、安全的 Web 应用了!