Appearance
参数绑定与数据校验
Spring MVC 提供了强大的参数绑定和数据校验机制,能够自动将 HTTP 请求参数转换为控制器方法的参数,并对这些参数进行验证。本文详细介绍这些功能的使用方法。
参数绑定基础
参数绑定是将 HTTP 请求中的数据映射到控制器方法参数的过程。Spring MVC 支持多种类型的参数绑定。
控制器方法支持的参数类型
Spring MVC 控制器方法可以接受多种参数类型:
- 基本数据类型和包装类:int、long、Integer、Long 等
- 字符串类型:String
- 模型相关:Model、ModelMap、Map
- Servlet API 类型:HttpServletRequest、HttpServletResponse、HttpSession 等
- 特殊类型:RedirectAttributes、Errors、BindingResult 等
- 自定义对象:POJO 类
- 集合类型:List、Set、Map 等
请求参数到方法参数的映射
@RequestParam
用于从请求参数中提取值,支持基本类型和字符串:
java
@GetMapping("/search")
public String search(
@RequestParam String keyword, // 必需参数
@RequestParam(required = false) String category, // 可选参数
@RequestParam(defaultValue = "1") int page, // 带默认值的参数
@RequestParam(name = "size") int pageSize, // 参数名不匹配方法参数名
@RequestParam List<String> tags, // 集合类型 (多值参数)
Model model
) {
// 处理逻辑
return "searchResults";
}
@PathVariable
用于从 URL 路径中提取变量值:
java
@GetMapping("/users/{userId}/posts/{postId}")
public String getPost(
@PathVariable Long userId,
@PathVariable("postId") Long id, // 当路径变量名与方法参数名不同时
@PathVariable(required = false) Optional<String> version, // 可选路径变量
Model model
) {
// 处理逻辑
return "postDetail";
}
@RequestHeader
用于获取请求头信息:
java
@GetMapping("/greeting")
public String greeting(
@RequestHeader("User-Agent") String userAgent,
@RequestHeader(value = "Accept-Language", defaultValue = "en") String language,
@RequestHeader Map<String, String> headers, // 获取所有请求头
Model model
) {
// 处理逻辑
return "greeting";
}
@CookieValue
用于获取 Cookie 值:
java
@GetMapping("/dashboard")
public String dashboard(
@CookieValue(value = "sessionId", required = false) String sessionId,
@CookieValue("JSESSIONID") Cookie cookie, // 直接获取 Cookie 对象
Model model
) {
// 处理逻辑
return "dashboard";
}
@RequestBody
用于接收 HTTP 请求体中的 JSON/XML 数据并转换为 Java 对象:
java
@PostMapping("/api/users")
@ResponseBody
public User createUser(@RequestBody User user) {
// 处理逻辑
return userService.save(user);
}
@ModelAttribute
用于将请求参数绑定到对象,或从模型中获取已有属性:
java
// 方法参数上的 @ModelAttribute
@PostMapping("/users/update")
public String updateUser(@ModelAttribute("user") User user) {
userService.update(user);
return "redirect:/users";
}
// 方法级别的 @ModelAttribute,在所有处理器方法执行前填充模型
@ModelAttribute("categories")
public List<Category> populateCategories() {
return categoryService.findAll();
}
@SessionAttributes
在会话中存储模型属性:
java
@Controller
@SessionAttributes({"user", "shoppingCart"})
public class CheckoutController {
@GetMapping("/checkout/address")
public String address(Model model) {
model.addAttribute("user", new User()); // 会存储在会话中
return "checkout/address";
}
@PostMapping("/checkout/address")
public String processAddress(@ModelAttribute("user") User user) {
// user 来自会话
return "redirect:/checkout/payment";
}
}
@RequestPart
用于处理 multipart/form-data 请求,特别是文件上传:
java
@PostMapping("/upload")
public String handleFileUpload(
@RequestPart("file") MultipartFile file,
@RequestPart("userData") User user
) {
// 处理上传文件和用户数据
return "uploadSuccess";
}
数据绑定机制
Spring MVC 的数据绑定过程分为以下几个步骤:
- WebDataBinder 创建:为请求创建一个数据绑定器
- 数据类型转换:将请求参数(字符串)转换为目标类型
- 数据格式化:根据配置的格式化器格式化数据
- 数据校验:如果配置了验证器,执行数据校验
类型转换
内置转换器
Spring 内置了多种转换器,支持基本类型、集合等的转换:
- StringToNumberConverterFactory:字符串到数字
- StringToBooleanConverter:字符串到布尔值
- StringToEnumConverterFactory:字符串到枚举
- CollectionToCollectionConverter:集合之间的转换
- StringToArrayConverter:字符串到数组
- 等等
自定义转换器
可以实现 Converter 接口创建自定义转换器:
java
public class StringToDateConverter implements Converter<String, Date> {
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
@Override
public Date convert(String source) {
try {
return dateFormat.parse(source);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid date format. Use yyyy-MM-dd");
}
}
}
// 注册转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToDateConverter());
}
}
数据格式化
内置格式化器
Spring 提供了多种格式化器:
- NumberFormatter:数字格式化
- CurrencyFormatter:货币格式化
- PercentFormatter:百分比格式化
- DateFormatter:日期格式化
使用注解配置格式化
可以使用以下注解配置字段格式:
java
public class Product {
private String name;
@NumberFormat(pattern = "#,###.##")
private BigDecimal price;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date releaseDate;
// getters and setters
}
数据校验
Spring MVC 支持两种主要的验证方式:
- 基于 JSR-303/JSR-380 的 Bean Validation
- Spring 的 Validator 接口
Bean Validation
使用 Bean Validation 需要添加依赖(如 Hibernate Validator)并使用相关注解:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用验证注解
java
public class User {
@NotBlank(message = "姓名不能为空")
private String name;
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄必须大于或等于18")
@Max(value = 120, message = "年龄必须小于或等于120")
private Integer age;
@Past(message = "生日必须是过去的日期")
private Date birthDate;
@Size(min = 6, max = 20, message = "密码长度必须在6到20之间")
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).*$",
message = "密码必须包含数字、小写字母和大写字母")
private String password;
@Valid // 级联验证
private Address address;
// getters and setters
}
public class Address {
@NotBlank(message = "街道不能为空")
private String street;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "邮政编码不能为空")
@Pattern(regexp = "\\d{6}", message = "邮政编码必须是6位数字")
private String zipCode;
// getters and setters
}
在控制器中使用验证
java
@PostMapping("/users")
public String createUser(
@Valid @ModelAttribute User user,
BindingResult result,
Model model
) {
if (result.hasErrors()) {
return "user/form"; // 返回表单页面,显示错误信息
}
userService.save(user);
return "redirect:/users";
}
自定义验证注解
可以创建自定义验证注解:
java
@Documented
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatches {
String message() default "密码不匹配";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
@Override
public void initialize(PasswordMatches constraintAnnotation) {
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context) {
UserRegistrationDto dto = (UserRegistrationDto) obj;
return dto.getPassword().equals(dto.getConfirmPassword());
}
}
@PasswordMatches
public class UserRegistrationDto {
// 字段和方法
}
分组验证
可以定义验证组以在不同场景使用不同的验证规则:
java
// 定义验证组
public interface Create {}
public interface Update {}
public class User {
@NotNull(groups = Update.class)
private Long id;
@NotBlank(groups = {Create.class, Update.class})
private String name;
@NotBlank(groups = Create.class)
@Null(groups = Update.class)
private String password;
// getters and setters
}
// 在控制器中使用
@PostMapping("/users")
public String createUser(
@Validated(Create.class) @ModelAttribute User user,
BindingResult result
) {
// ...
}
@PutMapping("/users/{id}")
public String updateUser(
@PathVariable Long id,
@Validated(Update.class) @ModelAttribute User user,
BindingResult result
) {
// ...
}
Spring Validator 接口
除了 Bean Validation,还可以实现 Spring 的 Validator 接口:
java
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;
if (user.getName() == null || user.getName().trim().isEmpty()) {
errors.rejectValue("name", "name.empty", "Name is required");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
errors.rejectValue("email", "email.invalid", "Invalid email format");
}
}
}
// 在控制器中使用
@Controller
public class UserController {
private final UserValidator userValidator;
@Autowired
public UserController(UserValidator userValidator) {
this.userValidator = userValidator;
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(userValidator);
}
@PostMapping("/users")
public String createUser(@ModelAttribute User user, BindingResult result) {
// 此处不需要显式调用验证器,因为已通过 @InitBinder 注册
if (result.hasErrors()) {
return "user/form";
}
return "redirect:/users";
}
}
错误处理与显示
在控制器中处理错误
java
@PostMapping("/users")
public String createUser(@Valid @ModelAttribute User user, BindingResult result) {
if (result.hasErrors()) {
// 获取所有错误
List<ObjectError> globalErrors = result.getGlobalErrors();
List<FieldError> fieldErrors = result.getFieldErrors();
// 获取特定字段的错误
FieldError emailError = result.getFieldError("email");
if (emailError != null) {
String errorMessage = emailError.getDefaultMessage();
// 处理特定字段错误
}
// 手动添加错误
result.rejectValue("username", "username.exists", "用户名已存在");
result.reject("form.global.error", "表单包含错误");
return "user/form";
}
userService.save(user);
return "redirect:/users";
}
在视图中显示错误(Thymeleaf)
html
<form th:action="@{/users}" th:object="${user}" method="post">
<!-- 显示全局错误 -->
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}" class="error"></p>
</div>
<!-- 名称字段 -->
<div>
<label for="name">姓名:</label>
<input type="text" id="name" th:field="*{name}" />
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error"></span>
</div>
<!-- 邮箱字段 -->
<div>
<label for="email">邮箱:</label>
<input type="text" id="email" th:field="*{email}" />
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></span>
</div>
<!-- 年龄字段 -->
<div>
<label for="age">年龄:</label>
<input type="number" id="age" th:field="*{age}" />
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" class="error"></span>
</div>
<button type="submit">提交</button>
</form>
在 JSP 中显示错误
jsp
<form:form modelAttribute="user" method="post">
<!-- 显示全局错误 -->
<form:errors path="*" cssClass="error" element="div" />
<!-- 名称字段 -->
<div>
<label for="name">姓名:</label>
<form:input path="name" id="name" />
<form:errors path="name" cssClass="error" />
</div>
<!-- 邮箱字段 -->
<div>
<label for="email">邮箱:</label>
<form:input path="email" id="email" />
<form:errors path="email" cssClass="error" />
</div>
<!-- 年龄字段 -->
<div>
<label for="age">年龄:</label>
<form:input path="age" id="age" type="number" />
<form:errors path="age" cssClass="error" />
</div>
<button type="submit">提交</button>
</form:form>
实战示例
完整的表单处理示例
java
@Controller
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
private final CategoryService categoryService;
@Autowired
public ProductController(ProductService productService, CategoryService categoryService) {
this.productService = productService;
this.categoryService = categoryService;
}
// 提供所有控制器方法共享的数据
@ModelAttribute("categories")
public List<Category> populateCategories() {
return categoryService.findAll();
}
// 显示创建表单
@GetMapping("/new")
public String showCreateForm(Model model) {
model.addAttribute("product", new ProductDto());
return "product/form";
}
// 处理表单提交
@PostMapping
public String createProduct(
@Valid @ModelAttribute("product") ProductDto productDto,
BindingResult result,
RedirectAttributes redirectAttributes) {
// 自定义验证逻辑
if (productDto.getPrice() != null && productDto.getDiscountPrice() != null
&& productDto.getDiscountPrice().compareTo(productDto.getPrice()) > 0) {
result.rejectValue("discountPrice", "discountPrice.invalid",
"折扣价不能高于原价");
}
if (result.hasErrors()) {
return "product/form";
}
// 保存产品
Product savedProduct = productService.save(convertToEntity(productDto));
// 添加成功消息
redirectAttributes.addFlashAttribute("message", "产品创建成功!");
redirectAttributes.addAttribute("id", savedProduct.getId());
return "redirect:/products/{id}";
}
// 显示编辑表单
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) {
Product product = productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
model.addAttribute("product", convertToDto(product));
return "product/form";
}
// 处理更新请求
@PostMapping("/{id}")
public String updateProduct(
@PathVariable Long id,
@Valid @ModelAttribute("product") ProductDto productDto,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (!productService.existsById(id)) {
throw new ResourceNotFoundException("Product not found");
}
if (result.hasErrors()) {
return "product/form";
}
productDto.setId(id);
productService.save(convertToEntity(productDto));
redirectAttributes.addFlashAttribute("message", "产品更新成功!");
return "redirect:/products/{id}";
}
// Helper methods for DTO conversion
private ProductDto convertToDto(Product product) {
ProductDto dto = new ProductDto();
dto.setId(product.getId());
dto.setName(product.getName());
dto.setDescription(product.getDescription());
dto.setPrice(product.getPrice());
dto.setDiscountPrice(product.getDiscountPrice());
dto.setCategoryId(product.getCategory().getId());
dto.setAvailable(product.isAvailable());
return dto;
}
private Product convertToEntity(ProductDto dto) {
Product product = new Product();
if (dto.getId() != null) {
product = productService.findById(dto.getId())
.orElse(new Product());
}
product.setName(dto.getName());
product.setDescription(dto.getDescription());
product.setPrice(dto.getPrice());
product.setDiscountPrice(dto.getDiscountPrice());
product.setAvailable(dto.isAvailable());
if (dto.getCategoryId() != null) {
Category category = categoryService.findById(dto.getCategoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category not found"));
product.setCategory(category);
}
return product;
}
}
@Data
public class ProductDto {
private Long id;
@NotBlank(message = "产品名称不能为空")
@Size(min = 2, max = 100, message = "产品名称长度必须在2-100个字符之间")
private String name;
@Size(max = 500, message = "产品描述不能超过500个字符")
private String description;
@NotNull(message = "价格不能为空")
@DecimalMin(value = "0.01", message = "价格必须大于0")
private BigDecimal price;
private BigDecimal discountPrice;
@NotNull(message = "分类不能为空")
private Long categoryId;
private boolean available = true;
}
高级表单示例(多对多关系)
java
@Controller
@RequestMapping("/courses")
public class CourseController {
private final CourseService courseService;
private final TeacherService teacherService;
@Autowired
public CourseController(CourseService courseService, TeacherService teacherService) {
this.courseService = courseService;
this.teacherService = teacherService;
}
@ModelAttribute("allTeachers")
public List<Teacher> populateTeachers() {
return teacherService.findAll();
}
@GetMapping("/new")
public String showCreateForm(Model model) {
CourseDto courseDto = new CourseDto();
model.addAttribute("course", courseDto);
return "course/form";
}
@PostMapping
public String createCourse(
@Valid @ModelAttribute("course") CourseDto courseDto,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
return "course/form";
}
Course savedCourse = courseService.save(convertToEntity(courseDto));
redirectAttributes.addFlashAttribute("message", "课程创建成功!");
return "redirect:/courses";
}
private Course convertToEntity(CourseDto dto) {
Course course = new Course();
course.setTitle(dto.getTitle());
course.setDescription(dto.getDescription());
course.setCredits(dto.getCredits());
if (dto.getTeacherIds() != null && !dto.getTeacherIds().isEmpty()) {
Set<Teacher> teachers = dto.getTeacherIds().stream()
.map(id -> teacherService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Teacher not found")))
.collect(Collectors.toSet());
course.setTeachers(teachers);
}
return course;
}
}
@Data
public class CourseDto {
private Long id;
@NotBlank(message = "课程标题不能为空")
@Size(min = 3, max = 200, message = "课程标题长度必须在3-200个字符之间")
private String title;
@Size(max = 1000, message = "课程描述不能超过1000个字符")
private String description;
@NotNull(message = "学分不能为空")
@Min(value = 1, message = "学分必须大于等于1")
@Max(value = 10, message = "学分不能超过10")
private Integer credits;
@NotEmpty(message = "请至少选择一位教师")
private List<Long> teacherIds;
}
在 Thymeleaf 模板中展示多选表单字段:
html
<form th:action="@{/courses}" th:object="${course}" method="post">
<!-- 其他字段 -->
<div>
<label>教师:</label>
<select th:field="*{teacherIds}" multiple>
<option th:each="teacher : ${allTeachers}"
th:value="${teacher.id}"
th:text="${teacher.name}">教师姓名</option>
</select>
<span th:if="${#fields.hasErrors('teacherIds')}" th:errors="*{teacherIds}" class="error"></span>
</div>
<!-- 其他字段 -->
<button type="submit">保存</button>
</form>