全部笔记All notes

Spring MVC 数据绑定机制详解

阅读 15m 41s15m 41s read

概述

Spring MVC 数据绑定是将HTTP请求中的参数数据自动绑定到控制器方法参数的过程。这个机制极大地简化了Web开发,让开发者可以专注于业务逻辑而不是参数解析。

什么是数据绑定?

数据绑定是将请求消息数据和后台方法形参建立连接的过程。Spring MVC 提供了强大的数据绑定功能,可以自动将请求参数转换为Java对象。

请求数据来源

  1. URL参数:通过URL传递参数,如 localhost:8080/user?id=1
  2. 表单数据:通过HTML表单POST提交的数据
  3. 请求体:JSON、XML等格式的请求体数据
  4. 请求头:HTTP请求头中的数据
  5. Cookie:浏览器Cookie中的数据
  6. Session属性:会话中存储的数据

数据绑定分类

根据后台方法形参的类型,数据绑定可以分为:

  • 简单数据绑定:基本类型、String、默认支持的类型
  • 复杂数据绑定:数组、集合、Map、自定义对象等

数据绑定原理

Spring MVC 的数据绑定由 HandlerMethodArgumentResolver 接口实现,主要流程如下:

  1. 参数解析:解析HTTP请求,提取参数数据
  2. 类型转换:使用 ConversionService 将字符串转换为目标类型
  3. 数据验证:使用 Validator 进行数据验证
  4. 绑定结果:将转换后的值绑定到方法参数

简单数据绑定

默认数据类型

Spring MVC 默认支持以下几种类型的参数绑定:

  1. HttpServletRequest:获取原始的请求对象,可以获取请求参数、请求头等信息
  2. HttpServletResponse:获取响应对象,用于直接操作响应流
  3. HttpSession:获取会话对象,访问session中的数据
  4. Model/ModelMap:用于向视图传递数据
    • Model 是一个接口
    • ModelMap 是 Model 的实现类
    • 作用是将数据填充到request域中
  5. ModelAndView:同时包含模型数据和视图信息
  6. Locale:获取请求的区域信息
  7. InputStream/Reader:获取请求输入流
  8. OutputStream/Writer:获取响应输出流

下面以 HttpServletRequest 为例进行详细讲解:

1. 创建项目并导入依赖

在 pom.xml 中添加 Spring MVC 相关依赖:

<!-- Servlet API -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

<!-- Spring Web -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.23</version>
</dependency>

<!-- Spring WebMVC -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.23</version>
</dependency>

<!-- JSP 支持(可选) -->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
    <scope>provided</scope>
</dependency>

<!-- JSTL(可选) -->
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

2. 配置 web.xml

在 web.xml 中配置 DispatcherServlet 和字符编码过滤器:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 配置 Spring MVC 前端控制器 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <!-- 配置字符编码过滤器,解决中文乱码问题 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

3. 创建 Spring MVC 配置文件

在 src/main/resources 目录下创建 springmvc-config.xml:

4. 创建控制器类

创建使用 HttpServletRequest 的控制器示例:

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;

@Controller
@RequestMapping("/user")
public class UserController {
    
    /**
     * 使用 HttpServletRequest 获取请求参数
     */
    @RequestMapping("/selectUser")
    public String selectUser(HttpServletRequest request) {
        // 获取请求参数
        String id = request.getParameter("id");
        String name = request.getParameter("name");
        
        // 获取请求头信息
        String userAgent = request.getHeader("User-Agent");
        
        // 获取请求URL信息
        String requestURI = request.getRequestURI();
        
        System.out.println("id=" + id);
        System.out.println("name=" + name);
        System.out.println("User-Agent=" + userAgent);
        System.out.println("Request URI=" + requestURI);
        
        return "success";
    }
    
    /**
     * 使用多个默认类型参数
     */
    @RequestMapping("/defaultTypes")
    public String demonstrateDefaultTypes(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session,
            Model model,
            ModelMap modelMap) {
        
        // 使用 HttpServletRequest
        String param = request.getParameter("param");
        
        // 使用 HttpSession
        session.setAttribute("user", "John Doe");
        Object user = session.getAttribute("user");
        
        // 使用 Model
        model.addAttribute("message", "Hello from Model");
        
        // 使用 ModelMap
        modelMap.addAttribute("info", "Hello from ModelMap");
        
        // HttpServletResponse 可用于直接写入响应
        // response.setContentType("text/html;charset=UTF-8");
        
        return "success";
    }
}

5.创建前端页面 success.jsp

在 WEB-INF 文件夹下面创建 jsp 文件夹,然后创建前端页面

<body>
success
</body>

6. 配置 Spring MVC

完善 springmvc-config.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 开启组件扫描,扫描控制器 -->
    <context:component-scan base-package="com.example.controller"/>
    
    <!-- 开启 Spring MVC 注解支持 -->
    <mvc:annotation-driven/>
    
    <!-- 配置视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 视图前缀 -->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!-- 视图后缀 -->
        <property name="suffix" value=".jsp"/>
    </bean>
    
    <!-- 静态资源处理 -->
    <mvc:default-servlet-handler/>
</beans>

7. 部署和测试

  1. 配置 Tomcat 服务器

    • 在 IDE 中配置 Tomcat 服务器
    • 部署项目到 Tomcat
    • 启动服务器
  2. 测试默认类型绑定

    发送请求:

    http://localhost:8080/项目名/user/selectUser?id=1&name=John

    控制台输出:

    id=1
    name=John
    User-Agent=Mozilla/5.0...
    Request URI=/项目名/user/selectUser

image-20220506220115521

基本数据类型

Spring MVC 支持自动绑定基本数据类型和包装类型:

  • 基本类型:int、long、float、double、boolean、char 等
  • 包装类型:Integer、Long、Float、Double、Boolean、Character 等
  • 字符串类型:String
  • 日期类型:Date(需要格式化)

1. 基本类型绑定示例

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;

@Controller
@RequestMapping("/data")
public class DataBindingController {
    
    /**
     * 基本类型自动绑定
     */
    @RequestMapping("/getUser")
    public String getUser(int id, String name) {
        System.out.println("id=" + id);
        System.out.println("name=" + name);
        return "success";
    }
    
    /**
     * 使用包装类型(推荐)
     * 包装类型可以接收 null 值,避免类型转换异常
     */
    @RequestMapping("/getUserSafe")
    public String getUserSafe(Integer id, String name) {
        if (id != null) {
            System.out.println("id=" + id);
        } else {
            System.out.println("id 参数未提供");
        }
        System.out.println("name=" + name);
        return "success";
    }
    
    /**
     * 多种基本类型绑定
     */
    @RequestMapping("/multiTypes")
    public String multipleTypes(
            Integer id,
            String name,
            Double salary,
            Boolean active,
            @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthDate) {
        
        System.out.println("ID: " + id);
        System.out.println("Name: " + name);
        System.out.println("Salary: " + salary);
        System.out.println("Active: " + active);
        System.out.println("Birth Date: " + birthDate);
        
        return "success";
    }
}

2. 测试请求

发送请求测试基本类型绑定:

http://localhost:8080/项目名/data/getUser?id=4&name=John
http://localhost:8080/项目名/data/getUserSafe?name=John  // id为null,不会报错
http://localhost:8080/项目名/data/multiTypes?id=1&name=John&salary=5000.50&active=true&birthDate=1990-01-01

3. @RequestParam 注解

当请求参数名与方法参数名不一致时,使用 @RequestParam 注解进行映射:

/**
 * 使用 @RequestParam 注解映射参数
 */
@RequestMapping("/paramMapping")
public String paramMapping(
        @RequestParam("user_id") Integer userId,  // 将 user_id 映射到 userId
        @RequestParam(value = "user_name", required = false) String userName,  // 可选参数
        @RequestParam(defaultValue = "10") int pageSize) {  // 带默认值的参数
    
    System.out.println("User ID: " + userId);
    System.out.println("User Name: " + userName);
    System.out.println("Page Size: " + pageSize);
    
    return "success";
}

@RequestParam 属性说明:

  • value/name:指定请求参数名称
  • required:是否必须,默认为 true
  • defaultValue:参数默认值

image-20220506222128788

POJO绑定

POJO(Plain Old Java Object)是普通的Java对象。Spring MVC 可以自动将请求参数绑定到POJO的属性中,这种方式在处理表单数据时非常方便。

绑定规则

  • 请求参数名必须与POJO的属性名一致
  • 支持级联属性绑定(如 user.name)
  • 自动进行类型转换

下面以用户注册为例进行说明:

1. 创建实体类 User

package com.example.domain;

import java.util.Date;

/**
 * 用户实体类
 */
public class User {
    private Integer id;
    private String username;
    private String password;
    private String email;
    private Date birthDate;
    private Boolean active;
    
    // 默认构造函数
    public User() {}
    
    // 带参构造函数
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    // Getter 和 Setter 方法
    public Integer getId() {
        return id;
    }
    
    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public Date getBirthDate() {
        return birthDate;
    }
    
    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }
    
    public Boolean getActive() {
        return active;
    }
    
    public void setActive(Boolean active) {
        this.active = active;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'\'' +
                ", password='" + password + '\'\'' +
                ", email='" + email + '\'\'' +
                ", birthDate=" + birthDate +
                ", active=" + active +
                '}';
    }
}

2. 创建注册页面 register.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户注册</title>
    <style>
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: inline-block;
            width: 100px;
        }
    </style>
</head>
<body>
    <h2>用户注册</h2>
    <form action="${pageContext.request.contextPath}/user/registerUser" method="post">
        <div class="form-group">
            <label>用户名:</label>
            <input type="text" name="username" required/>
        </div>
        <div class="form-group">
            <label>密码:</label>
            <input type="password" name="password" required/>
        </div>
        <div class="form-group">
            <label>邮箱:</label>
            <input type="email" name="email"/>
        </div>
        <div class="form-group">
            <label>生日:</label>
            <input type="date" name="birthDate"/>
        </div>
        <div class="form-group">
            <label>是否激活:</label>
            <input type="checkbox" name="active" value="true"/>
        </div>
        <div class="form-group">
            <input type="submit" value="注册"/>
            <input type="reset" value="重置"/>
        </div>
    </form>
</body>
</html>

3. 创建控制器方法

package com.example.controller;

import com.example.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.ui.Model;

@Controller
@RequestMapping("/user")
public class UserController {
    
    /**
     * 跳转到注册页面
     */
    @RequestMapping("/toRegister")
    public String toRegister() {
        return "register";
    }
    
    /**
     * 处理用户注册
     * Spring MVC 会自动将表单数据绑定到 User 对象的属性上
     */
    @RequestMapping(value = "/registerUser", method = RequestMethod.POST)
    public String registerUser(User user, Model model) {
        // 打印接收到的用户信息
        System.out.println("注册用户信息:" + user);
        
        // 模拟保存用户
        // userService.save(user);
        
        // 将用户信息传递给成功页面
        model.addAttribute("user", user);
        model.addAttribute("message", "注册成功!");
        
        return "success";
    }
    
    /**
     * 使用 @ModelAttribute 注解
     * 可以自定义绑定的属性名
     */
    @RequestMapping(value = "/registerWithModelAttribute", method = RequestMethod.POST)
    public String registerWithModelAttribute(
            @ModelAttribute("newUser") User user, 
            Model model) {
        
        System.out.println("使用@ModelAttribute注册:" + user);
        
        // 在视图中可以通过 ${newUser} 访问
        model.addAttribute("message", "注册成功!");
        
        return "success";
    }
}

4. 创建成功页面 success.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>注册成功</title>
</head>
<body>
    <h2>${message}</h2>
    <h3>用户信息:</h3>
    <p>用户名:${user.username}</p>
    <p>邮箱:${user.email}</p>
    <p>生日:${user.birthDate}</p>
    <p>状态:${user.active ? '已激活' : '未激活'}</p>
</body>
</html>

5. 测试 POJO 绑定

  1. 访问注册页面:http://localhost:8080/项目名/user/toRegister
  2. 填写表单并提交
  3. 查看控制台输出和成功页面

注意事项:

  • 确保表单的 name 属性与 POJO 的属性名一致
  • 日期格式需要使用 @DateTimeFormat 注解指定格式
  • 布尔类型的 checkbox,选中时值为 true
  • 中文乱码问题已通过字符编码过滤器解决

image-20220506223113983

包装POJO类型

包装POJO类型用于处理复杂的嵌套对象绑定。当请求参数涉及多个相关对象时,可以使用包装类将它们组织在一起。

应用场景

  • 查询条件包含多个对象的属性
  • 表单数据涉及主从关系
  • 需要同时提交多个相关实体的数据

绑定规则

  • 使用点号(.)访问嵌套属性
  • 支持多层嵌套
  • 自动创建嵌套对象

1. 创建订单实体类

package com.example.domain;

import java.util.Date;

/**
 * 订单实体类
 */
public class Order {
    private Integer orderId;
    private String orderNumber;
    private Date orderDate;
    private Double totalAmount;
    private User user;  // 关联的用户对象
    private Address shippingAddress;  // 配送地址
    
    // Getter 和 Setter 方法
    public Integer getOrderId() {
        return orderId;
    }
    
    public void setOrderId(Integer orderId) {
        this.orderId = orderId;
    }
    
    public String getOrderNumber() {
        return orderNumber;
    }
    
    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }
    
    public Date getOrderDate() {
        return orderDate;
    }
    
    public void setOrderDate(Date orderDate) {
        this.orderDate = orderDate;
    }
    
    public Double getTotalAmount() {
        return totalAmount;
    }
    
    public void setTotalAmount(Double totalAmount) {
        this.totalAmount = totalAmount;
    }
    
    public User getUser() {
        return user;
    }
    
    public void setUser(User user) {
        this.user = user;
    }
    
    public Address getShippingAddress() {
        return shippingAddress;
    }
    
    public void setShippingAddress(Address shippingAddress) {
        this.shippingAddress = shippingAddress;
    }
    
    @Override
    public String toString() {
        return "Order{" +
                "orderId=" + orderId +
                ", orderNumber='" + orderNumber + '\'\'' +
                ", orderDate=" + orderDate +
                ", totalAmount=" + totalAmount +
                ", user=" + user +
                ", shippingAddress=" + shippingAddress +
                '}';
    }
}

/**
 * 地址实体类
 */
class Address {
    private String street;
    private String city;
    private String zipCode;
    
    // Getter 和 Setter 方法
    public String getStreet() {
        return street;
    }
    
    public void setStreet(String street) {
        this.street = street;
    }
    
    public String getCity() {
        return city;
    }
    
    public void setCity(String city) {
        this.city = city;
    }
    
    public String getZipCode() {
        return zipCode;
    }
    
    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }
    
    @Override
    public String toString() {
        return "Address{" +
                "street='" + street + '\'\'' +
                ", city='" + city + '\'\'' +
                ", zipCode='" + zipCode + '\'\'' +
                '}';
    }
}

2. 创建订单控制器

package com.example.controller;

import com.example.domain.Order;
import com.example.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.ui.Model;

@Controller
@RequestMapping("/order")
public class OrderController {
    
    /**
     * 跳转到订单查询页面
     */
    @RequestMapping("/toFindOrderWithUser")
    public String toFindOrderWithUser() {
        return "orders";
    }
    
    /**
     * 查询订单及用户信息
     * 演示嵌套对象的数据绑定
     */
    @RequestMapping(value = "/findOrderWithUser", method = RequestMethod.POST)
    public String findOrderWithUser(Order order, Model model) {
        // 获取订单信息
        System.out.println("订单编号:" + order.getOrderNumber());
        System.out.println("订单金额:" + order.getTotalAmount());
        
        // 获取嵌套的用户信息
        if (order.getUser() != null) {
            System.out.println("下单用户:" + order.getUser().getUsername());
            System.out.println("用户邮箱:" + order.getUser().getEmail());
        }
        
        // 获取嵌套的地址信息
        if (order.getShippingAddress() != null) {
            System.out.println("配送地址:" + order.getShippingAddress());
        }
        
        // 传递到视图
        model.addAttribute("order", order);
        model.addAttribute("message", "订单查询成功");
        
        return "orderDetail";
    }
    
    /**
     * 创建新订单
     * 演示更复杂的嵌套绑定
     */
    @RequestMapping("/toCreateOrder")
    public String toCreateOrder() {
        return "createOrder";
    }
    
    @RequestMapping(value = "/createOrder", method = RequestMethod.POST)
    public String createOrder(Order order, Model model) {
        System.out.println("创建订单:" + order);
        
        // 模拟保存订单
        // orderService.save(order);
        
        model.addAttribute("order", order);
        model.addAttribute("message", "订单创建成功");
        
        return "orderDetail";
    }
}

3. 创建订单查询页面 orders.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>订单查询</title>
    <style>
        .form-group {
            margin-bottom: 10px;
        }
        label {
            display: inline-block;
            width: 120px;
        }
        fieldset {
            margin-bottom: 20px;
            padding: 10px;
        }
    </style>
</head>
<body>
    <h2>订单查询</h2>
    <form action="${pageContext.request.contextPath}/order/findOrderWithUser" method="post">
        <fieldset>
            <legend>订单信息</legend>
            <div class="form-group">
                <label>订单编号:</label>
                <input type="text" name="orderNumber" />
            </div>
            <div class="form-group">
                <label>订单金额:</label>
                <input type="number" step="0.01" name="totalAmount" />
            </div>
        </fieldset>
        
        <fieldset>
            <legend>用户信息</legend>
            <div class="form-group">
                <label>用户名:</label>
                <input type="text" name="user.username" />
            </div>
            <div class="form-group">
                <label>用户邮箱:</label>
                <input type="email" name="user.email" />
            </div>
        </fieldset>
        
        <fieldset>
            <legend>配送地址</legend>
            <div class="form-group">
                <label>街道地址:</label>
                <input type="text" name="shippingAddress.street" />
            </div>
            <div class="form-group">
                <label>城市:</label>
                <input type="text" name="shippingAddress.city" />
            </div>
            <div class="form-group">
                <label>邮政编码:</label>
                <input type="text" name="shippingAddress.zipCode" />
            </div>
        </fieldset>
        
        <div class="form-group">
            <input type="submit" value="查询" />
        </div>
    </form>
</body>
</html>

4. 创建订单详情页面 orderDetail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>订单详情</title>
</head>
<body>
    <h2>${message}</h2>
    <h3>订单信息</h3>
    <p>订单编号:${order.orderNumber}</p>
    <p>订单金额:${order.totalAmount}</p>
    
    <h3>用户信息</h3>
    <p>用户名:${order.user.username}</p>
    <p>邮箱:${order.user.email}</p>
    
    <h3>配送地址</h3>
    <p>街道:${order.shippingAddress.street}</p>
    <p>城市:${order.shippingAddress.city}</p>
    <p>邮编:${order.shippingAddress.zipCode}</p>
</body>
</html>

5. 测试包装类型绑定

  1. 访问订单查询页面:http://localhost:8080/项目名/order/toFindOrderWithUser
  2. 填写表单数据:
    • 订单编号:ORDER001
    • 订单金额:999.99
    • 用户名:John Doe
    • 用户邮箱:john@example.com
    • 街道地址:123 Main St
    • 城市:New York
    • 邮编:10001
  3. 提交表单,查看控制台输出和订单详情页面

注意事项:

  • 嵌套属性使用点号(.)分隔
  • Spring MVC 会自动创建嵌套对象
  • 确保嵌套类有无参构造函数

image-20220507091032381

复杂数据绑定

数组绑定

数组绑定常用于批量操作,如批量删除、批量更新等场景。Spring MVC 可以自动将同名的多个参数值绑定到数组中。

1. 创建用户列表页面 user.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户管理</title>
    <style>
        table {
            border-collapse: collapse;
            margin: 20px 0;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
    </style>
</head>
<body>
    <h2>批量删除用户</h2>
    <form method="post" action="${pageContext.request.contextPath}/user/batchDelete">
        <table>
            <tr>
                <th>选择</th>
                <th>ID</th>
                <th>用户名</th>
                <th>邮箱</th>
                <th>状态</th>
            </tr>
            <tr>
                <td><input type="checkbox" name="ids" value="1"/></td>
                <td>1</td>
                <td>Tom</td>
                <td>tom@example.com</td>
                <td>激活</td>
            </tr>
            <tr>
                <td><input type="checkbox" name="ids" value="2"/></td>
                <td>2</td>
                <td>Jack</td>
                <td>jack@example.com</td>
                <td>激活</td>
            </tr>
            <tr>
                <td><input type="checkbox" name="ids" value="3"/></td>
                <td>3</td>
                <td>Mary</td>
                <td>mary@example.com</td>
                <td>未激活</td>
            </tr>
            <tr>
                <td><input type="checkbox" name="ids" value="4"/></td>
                <td>4</td>
                <td>John</td>
                <td>john@example.com</td>
                <td>激活</td>
            </tr>
        </table>
        <input type="submit" value="批量删除" onclick="return confirm('确定要删除选中的用户吗?')"/>
    </form>
    
    <h2>其他数组绑定示例</h2>
    <form method="post" action="${pageContext.request.contextPath}/user/updateStatus">
        <p>选择要更新状态的用户ID(多选):</p>
        <select name="userIds" multiple size="4">
            <option value="1">用户1</option>
            <option value="2">用户2</option>
            <option value="3">用户3</option>
            <option value="4">用户4</option>
        </select>
        <p>新状态:
            <select name="status">
                <option value="active">激活</option>
                <option value="inactive">未激活</option>
            </select>
        </p>
        <input type="submit" value="更新状态"/>
    </form>
</body>
</html>

2. 编写数组绑定的控制器方法

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;
import java.util.Arrays;

@Controller
@RequestMapping("/user")
public class UserBatchController {
    
    /**
     * 跳转到用户管理页面
     */
    @RequestMapping("/toUserList")
    public String toUserList() {
        return "user";
    }
    
    /**
     * 批量删除用户 - 基本数组绑定
     */
    @RequestMapping(value = "/batchDelete", method = RequestMethod.POST)
    public String batchDeleteUsers(int[] ids, Model model) {
        if (ids != null && ids.length > 0) {
            System.out.println("准备删除的用户ID:" + Arrays.toString(ids));
            
            // 模拟批量删除操作
            for (int id : ids) {
                System.out.println("删除了ID为 " + id + " 的用户");
                // userService.delete(id);
            }
            
            model.addAttribute("message", "成功删除 " + ids.length + " 个用户");
        } else {
            System.out.println("未选择任何用户");
            model.addAttribute("message", "请至少选择一个用户");
        }
        
        return "success";
    }
    
    /**
     * 批量删除用户 - 使用包装类型数组(推荐)
     */
    @RequestMapping(value = "/batchDeleteSafe", method = RequestMethod.POST)
    public String batchDeleteUsersSafe(Integer[] ids, Model model) {
        if (ids != null && ids.length > 0) {
            System.out.println("删除用户ID:" + Arrays.toString(ids));
            model.addAttribute("message", "成功删除 " + ids.length + " 个用户");
        } else {
            model.addAttribute("message", "未选择任何用户");
        }
        return "success";
    }
    
    /**
     * 批量更新用户状态
     * 演示多个数组参数的绑定
     */
    @RequestMapping(value = "/updateStatus", method = RequestMethod.POST)
    public String updateUserStatus(
            @RequestParam("userIds") Long[] userIds,
            @RequestParam("status") String status,
            Model model) {
        
        if (userIds != null && userIds.length > 0) {
            System.out.println("更新用户ID:" + Arrays.toString(userIds));
            System.out.println("新状态:" + status);
            
            // 模拟更新操作
            for (Long userId : userIds) {
                System.out.println("将用户 " + userId + " 的状态更新为:" + status);
                // userService.updateStatus(userId, status);
            }
            
            model.addAttribute("message", 
                "成功将 " + userIds.length + " 个用户的状态更新为:" + status);
        } else {
            model.addAttribute("message", "请选择要更新的用户");
        }
        
        return "success";
    }
    
    /**
     * 处理不同类型的数组
     */
    @RequestMapping("/multiArrayTypes")
    public String handleMultipleArrayTypes(
            String[] tags,      // 字符串数组
            Double[] prices,    // 浮点数数组
            Boolean[] flags) {  // 布尔数组
        
        System.out.println("Tags: " + Arrays.toString(tags));
        System.out.println("Prices: " + Arrays.toString(prices));
        System.out.println("Flags: " + Arrays.toString(flags));
        
        return "success";
    }
}

3. 测试数组绑定

  1. 访问用户管理页面:http://localhost:8080/项目名/user/toUserList
  2. 选择多个用户并点击”批量删除”
  3. 查看控制台输出

测试结果示例:

  • 选择用户1、3、4进行删除
  • 控制台输出:
    准备删除的用户ID:[1, 3, 4]
    删除了ID为 1 的用户
    删除了ID为 3 的用户
    删除了ID为 4 的用户

注意事项:

  • checkbox 未选中时不会提交值
  • 使用包装类型数组(Integer[])比基本类型数组(int[])更安全
  • 可以使用 @RequestParam 指定参数名和是否必需

image-20220506224513695

集合绑定

集合绑定用于处理动态数量的对象数据,常用于批量编辑、动态表单等场景。Spring MVC 不能直接绑定集合类型的参数,需要使用包装类。

1. 创建集合包装类

package com.example.vo;

import com.example.domain.User;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 用户集合包装类
 * 用于批量操作用户数据
 */
public class UserVO {
    private List<User> users;  // 用户列表
    private Set<Integer> selectedIds;  // 选中的ID集合
    private Map<Integer, String> statusMap;  // ID与状态的映射
    
    // 默认构造函数
    public UserVO() {}
    
    // Getter 和 Setter 方法
    public List<User> getUsers() {
        return users;
    }
    
    public void setUsers(List<User> users) {
        this.users = users;
    }
    
    public Set<Integer> getSelectedIds() {
        return selectedIds;
    }
    
    public void setSelectedIds(Set<Integer> selectedIds) {
        this.selectedIds = selectedIds;
    }
    
    public Map<Integer, String> getStatusMap() {
        return statusMap;
    }
    
    public void setStatusMap(Map<Integer, String> statusMap) {
        this.statusMap = statusMap;
    }
}

/**
 * 订单集合包装类
 */
public class OrderVO {
    private List<Order> orders;
    private List<String> orderNumbers;
    private Map<String, Double> priceMap;
    
    // Getter 和 Setter 方法
    public List<Order> getOrders() {
        return orders;
    }
    
    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }
    
    public List<String> getOrderNumbers() {
        return orderNumbers;
    }
    
    public void setOrderNumbers(List<String> orderNumbers) {
        this.orderNumbers = orderNumbers;
    }
    
    public Map<String, Double> getPriceMap() {
        return priceMap;
    }
    
    public void setPriceMap(Map<String, Double> priceMap) {
        this.priceMap = priceMap;
    }
}

2. 编写集合绑定的控制器方法

package com.example.controller;

import com.example.domain.User;
import com.example.vo.UserVO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.ui.Model;
import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/user")
public class UserCollectionController {
    
    /**
     * 跳转到批量编辑页面
     * 预加载用户数据
     */
    @RequestMapping("/toUserEdit")
    public String toUserEdit(Model model) {
        // 模拟从数据库加载用户列表
        List<User> userList = new ArrayList<>();
        
        User user1 = new User();
        user1.setId(1);
        user1.setUsername("Tom");
        user1.setEmail("tom@example.com");
        user1.setActive(true);
        userList.add(user1);
        
        User user2 = new User();
        user2.setId(2);
        user2.setUsername("Jack");
        user2.setEmail("jack@example.com");
        user2.setActive(true);
        userList.add(user2);
        
        User user3 = new User();
        user3.setId(3);
        user3.setUsername("Mary");
        user3.setEmail("mary@example.com");
        user3.setActive(false);
        userList.add(user3);
        
        // 创建VO对象
        UserVO userVO = new UserVO();
        userVO.setUsers(userList);
        
        model.addAttribute("userVO", userVO);
        
        return "user_edit";
    }
    
    /**
     * 批量编辑用户
     * 处理List集合绑定
     */
    @RequestMapping(value = "/editUsers", method = RequestMethod.POST)
    public String editUsers(@ModelAttribute UserVO userVO, Model model) {
        // 获取用户列表
        List<User> users = userVO.getUsers();
        
        if (users != null && !users.isEmpty()) {
            System.out.println("接收到 " + users.size() + " 个用户数据");
            
            // 处理每个用户的修改
            for (int i = 0; i < users.size(); i++) {
                User user = users.get(i);
                if (user.getId() != null) {
                    System.out.println("用户[" + i + "] - ID: " + user.getId() + 
                                     ", 用户名: " + user.getUsername() + 
                                     ", 邮箱: " + user.getEmail() + 
                                     ", 状态: " + user.getActive());
                    
                    // 模拟更新操作
                    // userService.update(user);
                }
            }
            
            model.addAttribute("message", "成功更新 " + users.size() + " 个用户信息");
        } else {
            model.addAttribute("message", "没有接收到用户数据");
        }
        
        return "success";
    }
    
    /**
     * 动态添加用户
     * 演示动态表单的集合绑定
     */
    @RequestMapping("/toDynamicUserForm")
    public String toDynamicUserForm(Model model) {
        // 初始化空的VO对象
        UserVO userVO = new UserVO();
        userVO.setUsers(new ArrayList<>());
        model.addAttribute("userVO", userVO);
        
        return "user_dynamic";
    }
    
    @RequestMapping(value = "/addDynamicUsers", method = RequestMethod.POST)
    public String addDynamicUsers(@ModelAttribute UserVO userVO, Model model) {
        List<User> users = userVO.getUsers();
        
        if (users != null) {
            // 过滤掉空的用户对象
            List<User> validUsers = new ArrayList<>();
            for (User user : users) {
                if (user.getUsername() != null && !user.getUsername().trim().isEmpty()) {
                    validUsers.add(user);
                }
            }
            
            System.out.println("接收到 " + validUsers.size() + " 个有效用户");
            for (User user : validUsers) {
                System.out.println("新用户: " + user);
                // userService.save(user);
            }
            
            model.addAttribute("message", "成功添加 " + validUsers.size() + " 个用户");
        }
        
        return "success";
    }
}

3. 创建批量编辑页面 user_edit.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>批量编辑用户</title>
    <style>
        table {
            border-collapse: collapse;
            margin: 20px 0;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
        }
        th {
            background-color: #f2f2f2;
        }
        input[type="text"], input[type="email"] {
            width: 150px;
        }
    </style>
</head>
<body>
    <h2>批量编辑用户信息</h2>
    <form action="${pageContext.request.contextPath}/user/editUsers" method="post">
        <table>
            <tr>
                <th>ID</th>
                <th>用户名</th>
                <th>邮箱</th>
                <th>状态</th>
            </tr>
            <c:forEach items="${userVO.users}" var="user" varStatus="status">
                <tr>
                    <td>
                        <input type="hidden" name="users[${status.index}].id" value="${user.id}"/>
                        ${user.id}
                    </td>
                    <td>
                        <input type="text" name="users[${status.index}].username" value="${user.username}"/>
                    </td>
                    <td>
                        <input type="email" name="users[${status.index}].email" value="${user.email}"/>
                    </td>
                    <td>
                        <input type="checkbox" name="users[${status.index}].active" 
                               value="true" ${user.active ? 'checked' : ''}/>
                        激活
                    </td>
                </tr>
            </c:forEach>
        </table>
        <input type="submit" value="保存修改"/>
        <input type="button" value="取消" onclick="history.back()"/>
    </form>
    
    <h3>集合绑定说明</h3>
    <ul>
        <li>使用 users[index].property 格式绑定List集合元素</li>
        <li>index 必须从0开始且连续</li>
        <li>Spring MVC 会自动创建List并填充数据</li>
    </ul>
</body>
</html>

4. 创建动态表单页面 user_dynamic.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>动态添加用户</title>
    <script>
        var userIndex = 0;
        
        function addUserRow() {
            var tbody = document.getElementById('userTableBody');
            var newRow = tbody.insertRow();
            
            newRow.innerHTML = `
                <td>
                    <input type="text" name="users[${userIndex}].username" placeholder="用户名" required/>
                </td>
                <td>
                    <input type="email" name="users[${userIndex}].email" placeholder="邮箱"/>
                </td>
                <td>
                    <input type="password" name="users[${userIndex}].password" placeholder="密码" required/>
                </td>
                <td>
                    <button type="button" onclick="removeRow(this)">删除</button>
                </td>
            `;
            
            userIndex++;
        }
        
        function removeRow(button) {
            button.parentNode.parentNode.remove();
        }
    </script>
</head>
<body>
    <h2>动态添加用户</h2>
    <form action="${pageContext.request.contextPath}/user/addDynamicUsers" method="post">
        <table>
            <thead>
                <tr>
                    <th>用户名</th>
                    <th>邮箱</th>
                    <th>密码</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody id="userTableBody">
                <!-- 动态添加的行将插入这里 -->
            </tbody>
        </table>
        <button type="button" onclick="addUserRow()">添加用户</button>
        <input type="submit" value="提交所有用户"/>
    </form>
</body>
</html>

5. 测试集合绑定

  1. 测试批量编辑:

    • 访问:http://localhost:8080/项目名/user/toUserEdit
    • 修改用户信息并提交
    • 查看控制台输出
  2. 测试动态表单:

    • 访问:http://localhost:8080/项目名/user/toDynamicUserForm
    • 点击”添加用户”按钮动态添加多个用户
    • 填写信息并提交

注意事项:

  • List集合必须通过包装类传递
  • 索引必须从0开始且连续
  • 动态表单需要JavaScript配合维护索引
  • 使用JSTL标签库简化JSP开发

image-20220507121847120

Map绑定

Map绑定用于处理键值对数据,适用于动态属性、配置项等场景。

1. Map绑定示例

/**
 * Map绑定示例
 */
@RequestMapping("/mapBinding")
public String demonstrateMapBinding(
        @RequestParam Map<String, String> params,  // 绑定所有请求参数
        @RequestParam("config") Map<String, String> configMap) {  // 绑定特定格式的参数
    
    System.out.println("所有参数:" + params);
    System.out.println("配置参数:" + configMap);
    
    return "success";
}

/**
 * 使用包装类进行Map绑定
 */
public class ConfigVO {
    private Map<String, String> settings;
    private Map<Integer, User> userMap;
    
    // Getter和Setter方法
}

@RequestMapping(value = "/saveConfig", method = RequestMethod.POST)
public String saveConfiguration(ConfigVO configVO) {
    Map<String, String> settings = configVO.getSettings();
    if (settings != null) {
        for (Map.Entry<String, String> entry : settings.entrySet()) {
            System.out.println("设置项:" + entry.getKey() + " = " + entry.getValue());
        }
    }
    return "success";
}

2. Map绑定的表单

<form action="${pageContext.request.contextPath}/config/saveConfig" method="post">
    <h3>系统配置</h3>
    <p>
        站点名称:<input type="text" name="settings['siteName']" value="My Site"/>
    </p>
    <p>
        站点URL:<input type="text" name="settings['siteUrl']" value="http://example.com"/>
    </p>
    <p>
        每页显示:<input type="number" name="settings['pageSize']" value="20"/>
    </p>
    <p>
        启用缓存:<input type="checkbox" name="settings['enableCache']" value="true"/>
    </p>
    <input type="submit" value="保存配置"/>
</form>

数据类型转换

内置类型转换器

Spring MVC 内置了大量的类型转换器,可以自动处理常见的类型转换:

  1. 基本类型和包装类:String → Integer、Long、Double、Boolean等
  2. 日期时间类型:String → Date、LocalDate、LocalDateTime
  3. 集合类型:String[] → List、Set
  4. 文件类型:MultipartFile
  5. 其他类型:Locale、Currency等

日期类型转换示例

/**
 * 日期类型绑定
 */
@RequestMapping("/dateBinding")
public String handleDateBinding(
        @DateTimeFormat(pattern = "yyyy-MM-dd") Date date,
        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate localDate,
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime dateTime) {
    
    System.out.println("Date: " + date);
    System.out.println("LocalDate: " + localDate);
    System.out.println("LocalDateTime: " + dateTime);
    
    return "success";
}

自定义类型转换器

当内置的类型转换器不能满足需求时,可以创建自定义类型转换器。

1. 实现 Converter 接口

package com.example.converter;

import org.springframework.core.convert.converter.Converter;
import com.example.domain.User;

/**
 * 自定义字符串到用户对象的转换器
 * 格式:"id,username,email"
 */
public class StringToUserConverter implements Converter<String, User> {
    
    @Override
    public User convert(String source) {
        if (source == null || source.isEmpty()) {
            return null;
        }
        
        String[] parts = source.split(",");
        if (parts.length < 3) {
            throw new IllegalArgumentException("Invalid user format: " + source);
        }
        
        User user = new User();
        user.setId(Integer.parseInt(parts[0].trim()));
        user.setUsername(parts[1].trim());
        user.setEmail(parts[2].trim());
        
        return user;
    }
}

2. 注册自定义转换器

在 Spring 配置文件中注册转换器:

<!-- 配置转换服务 -->
<bean id="conversionService" 
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.example.converter.StringToUserConverter"/>
        </set>
    </property>
</bean>

<!-- 将转换服务注册到 Spring MVC -->
<mvc:annotation-driven conversion-service="conversionService"/>

3. 使用自定义转换器

@RequestMapping("/customConverter")
public String testCustomConverter(
        @RequestParam("user") User user) {  // 自动使用 StringToUserConverter
    
    System.out.println("转换后的用户:" + user);
    return "success";
}

// 请求示例:/customConverter?user=1,john,john@example.com

数据验证

JSR-303验证

Spring MVC 支持 JSR-303(Bean Validation)标准,可以使用注解进行数据验证。

1. 添加验证依赖

<!-- Hibernate Validator (JSR-303 实现) -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

2. 在实体类添加验证注解

package com.example.domain;

import javax.validation.constraints.*;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;

/**
 * 带验证注解的用户实体
 */
public class User {
    
    private Integer id;
    
    @NotNull(message = "用户名不能为空")
    @Length(min = 3, max = 20, message = "用户名长度必须在3-20之间")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z]).{6,20}$", 
             message = "密码必须包含字母和数字,长度6-20")
    private String password;
    
    @Email(message = "邮箱格式不正确")
    @NotEmpty(message = "邮箱不能为空")
    private String email;
    
    @Min(value = 18, message = "年龄必须大于等于18")
    @Max(value = 100, message = "年龄必须小于等于100")
    private Integer age;
    
    @Past(message = "生日必须是过去的日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthDate;
    
    @AssertTrue(message = "必须同意服务条款")
    private Boolean agreeTerms;
    
    // Getter和Setter方法...
}

3. 在控制器中使用验证

@Controller
@RequestMapping("/validation")
public class ValidationController {
    
    /**
     * 显示注册表单
     */
    @RequestMapping("/toRegister")
    public String toRegister(Model model) {
        model.addAttribute("user", new User());
        return "validation/register";
    }
    
    /**
     * 处理注册请求,带数据验证
     */
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(
            @Valid @ModelAttribute User user,  // @Valid 触发验证
            BindingResult bindingResult,       // 验证结果
            Model model) {
        
        // 检查是否有验证错误
        if (bindingResult.hasErrors()) {
            // 获取所有错误信息
            List<FieldError> errors = bindingResult.getFieldErrors();
            for (FieldError error : errors) {
                System.out.println(error.getField() + ": " + error.getDefaultMessage());
            }
            
            // 返回注册页面,显示错误信息
            return "validation/register";
        }
        
        // 验证通过,处理注册逻辑
        System.out.println("注册用户:" + user);
        model.addAttribute("message", "注册成功!");
        
        return "success";
    }
    
    /**
     * 分组验证示例
     */
    public interface CreateGroup {}
    public interface UpdateGroup {}
    
    @RequestMapping("/groupValidation")
    public String groupValidation(
            @Validated(CreateGroup.class) User user,
            BindingResult result) {
        
        if (result.hasErrors()) {
            return "validation/form";
        }
        
        return "success";
    }
}

4. 显示验证错误的JSP页面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户注册</title>
    <style>
        .error {
            color: red;
            font-size: 12px;
        }
        .form-group {
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
    <h2>用户注册(带验证)</h2>
    
    <form:form modelAttribute="user" 
               action="${pageContext.request.contextPath}/validation/register" 
               method="post">
        <div class="form-group">
            <label>用户名:</label>
            <form:input path="username"/>
            <form:errors path="username" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <label>密码:</label>
            <form:password path="password"/>
            <form:errors path="password" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <label>邮箱:</label>
            <form:input path="email"/>
            <form:errors path="email" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <label>年龄:</label>
            <form:input path="age" type="number"/>
            <form:errors path="age" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <label>生日:</label>
            <form:input path="birthDate" type="date"/>
            <form:errors path="birthDate" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <form:checkbox path="agreeTerms"/>同意服务条款
            <form:errors path="agreeTerms" cssClass="error"/>
        </div>
        
        <div class="form-group">
            <input type="submit" value="注册"/>
        </div>
        
        <!-- 显示全局错误 -->
        <c:if test="${not empty errors}">
            <div class="error">
                <c:forEach items="${errors}" var="error">
                    <p>${error.defaultMessage}</p>
                </c:forEach>
            </div>
        </c:if>
    </form:form>
</body>
</html>

自定义验证器

创建自定义验证注解和验证器:

/**
 * 自定义验证注解:手机号验证
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

/**
 * 手机号验证器实现
 */
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    
    private static final String PHONE_PATTERN = "^1[3-9]\\d{9}$";
    
    @Override
    public void initialize(PhoneNumber constraintAnnotation) {}
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return true;  // 让 @NotNull 处理空值
        }
        return value.matches(PHONE_PATTERN);
    }
}

最佳实践

  1. 使用包装类型而非基本类型

    • 包装类型可以处理 null 值,避免类型转换异常
    • 示例:使用 Integer 而不是 int
  2. 合理使用 @RequestParam 注解

    • 明确指定参数名称、是否必需、默认值
    • 提高代码可读性和健壮性
  3. 统一日期格式处理

    • 使用 @DateTimeFormat 注解指定日期格式
    • 考虑全局配置日期格式转换器
  4. 数据验证分层

    • 前端验证:提高用户体验
    • 后端验证:确保数据安全和完整性
    • 数据库约束:最后一道防线
  5. 合理的错误处理

    • 使用 BindingResult 获取验证错误
    • 提供友好的错误信息
    • 考虑国际化错误消息
  6. 避免过度嵌套

    • 嵌套层级不宜过深
    • 复杂场景考虑使用DTO模式

常见问题

1. 日期绑定失败

问题:日期格式不匹配导致绑定失败

解决方案:

// 使用 @DateTimeFormat 注解
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthDate;

// 或全局配置
@InitBinder
public void initBinder(WebDataBinder binder) {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}

2. 中文乱码问题

问题:表单提交的中文数据出现乱码

解决方案:

  • 配置字符编码过滤器(见前文 web.xml 配置)
  • 确保JSP页面编码设置正确
  • 检查数据库连接编码

3. 复选框绑定问题

问题:复选框未选中时不提交值

解决方案:

// 使用隐藏域技巧
<input type="hidden" name="_active" value="false"/>
<input type="checkbox" name="active" value="true"/>

// 或在实体类中设置默认值
private Boolean active = false;

4. 文件上传绑定

配置 MultipartResolver:

<bean id="multipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="10485760"/> <!-- 10MB -->
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

控制器处理:

@RequestMapping("/upload")
public String handleFileUpload(
        @RequestParam("file") MultipartFile file,
        @RequestParam("description") String description) {
    
    if (!file.isEmpty()) {
        String fileName = file.getOriginalFilename();
        long fileSize = file.getSize();
        // 保存文件逻辑
    }
    
    return "success";
}

5. RESTful 风格参数绑定

@RequestMapping("/user/{id}")
public String getUser(@PathVariable("id") Integer userId) {
    // 处理路径变量
    return "user";
}

@RequestMapping("/users/{userId}/orders/{orderId}")
public String getOrder(
        @PathVariable Integer userId,
        @PathVariable Integer orderId) {
    // 处理多个路径变量
    return "order";
}

相关文章

前后章节导航

Spring MVC 相关

数据访问与事务