Spring MVC 表单数据校验

Spring MVC 表单数据校验

在实际工作中,获取数据后的首要步骤是校验数据正确性。前端虽可通过 JS 进行验证,但存在多种绕过方式,无法保障数据安全。因此,服务器端数据校验必不可少,Spring 提供的验证器(Validator)规则可满足这一需求,且验证器会由 Spring MVC 自动加载。本文将采用 JSR 303 校验规范,结合 Hibernate 提供的数据校验器实现数据校验。

JSR 303 注解说明

Spring 支持对 Bean 的功能校验,通过 @Valid 注解标明需要启用注解式验证的 Bean。在 javax.validation.constraints.* 中定义了一系列 JSR 303 规范注解,具体如下:

注解 限制说明
@Null 限制只能为 null
@NotNull 限制必须不为 null
@AssertFalse 限制必须为 false
@AssertTrue 限制必须为 true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer, fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max, min) 限制字符长度必须在 min 到 max 之间
@Email 限制为邮箱类型
@NotEmpty 限制集合不为空
@NotBlank 限制不为空字符串
@Positive 限制为正数
@PositiveOrZero 限制为正数或 0
@Negative 限制为负数
@NegativeOrZero 限制为负数或 0
@PastOrPresent 限制为过去或者现在日期
@FutureOrPresent 限制为将来或者现在日期

依赖配置

使用 JSR 303 校验需添加以下依赖项:

implementation 'jakarta.validation:jakarta.validation-api:3.1.1'
implementation 'org.hibernate.validator:hibernate-validator:9.1.0.Final'

表单页面实现(JSP)

创建添加员工表单页面 add_emp.jsp,用于接收用户输入:

<%--
  Created by IntelliJ IDEA.
  User: Jing61
  Date: 2025/12/25
  Time: 09:39
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>添加员工</title>
</head>
<body>
    <form action="/emp/save" method="post">
        <div>
            <label>username:</label>
            <input type="text" name="username" placeholder="用户名,长度在4-12位之间" />
        </div>
        <div>
            <label>password:</label>
            <input type="password" name="password" />
        </div>
        <div>
            <label>email:</label>
            <input type="email" name="email" placeholder="电子邮箱"/>
        </div>
        <div>
            <label>phone:</label>
            <input type="text" name="phone" placeholder="手机号码"/>
        </div>
        <div>
            <label>birthday:</label>
            <input type="date" name="birthday" placeholder="生日"/>
        </div>
        <div>
            <label>hire date:</label>
            <input type="date" name="hireDate" placeholder="入职日期"/>
        </div>

        <div>
            <label>salary:</label>
            <input type="number" name="salary" placeholder="薪水"/>
        </div>
        <div>
            <label>intro:</label> <br/>
            <textarea name="intro" rows="5" cols="40" placeholder="员工简介"></textarea>
        </div>
<div>
    <input type="submit" />
</div>

    </form>
</body>
</html>

校验规则定义

基础校验规则

字段 校验规则
用户名 1. 不允许为空;2. 长度在 4-12 位之间
密码 不允许为空或空串
电子邮箱 1. 不允许为空;2. 必须满足电子邮箱格式
手机号 符合正确的手机号码格式(正则:^1[358]\\d{9}$
生日 必须是一个过去的日期
入职时间 必须是现在或者将来的日期
工资 自定义校验规则:不能低于职位的最低工资,不能高于职位的最高工资
职位描述 长度不超过 200 个字符

POJO 类定义(结合注解)

创建 Employee 类,通过 JSR 303 注解定义校验规则:

package com.pojo;

import com.enums.Position;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;

@Data
public class Employee {
    private Integer id;

    /**
     * 用户名:
     * 1. 不允许为空
     * 2. 长度在4-12位之间
     */
    @NotNull(message = "用户名不能为空")
    @Size(min = 4, max = 12, message = "用户名长度必须在4-12位之间")
    private String username;

    /**
     * 密码:不允许为空或空串
     */
    @NotBlank(message = "密码不能为空")
    private String password;

    /**
     * 电子邮箱
     * 1. 不允许为空
     * 2. 必须满足电子邮箱格式
     */
    @NotNull(message = "电子邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    /**
     * 手机号:正确的手机号码
     * "^1[358]\\d{9}$":手机号格式:1 开头,第二位是 3、5、8,后面是 9 个数字
     */
    @Pattern(regexp = "^1[358]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    /**
     * 生日:必须是一个过去的日期
     */
    @Past(message = "生日必须是一个过去的日期")
    private LocalDate birthday;

    /**
     * 入职时间:必须一个现在或者将来的日期
     */
    @FutureOrPresent(message = "入职时间必须一个现在或者将来的日期")
    private LocalDate hireDate;

    /**
     * 工资:自定义校验规则:不能低于职位的最低工资,不能高于职位的最高工资
     */
    private int salary;

    /**
     * 职位
     */
    private Position job = Position.CLERK;

    /**
     * 职位描述:长度不超过200个字符
     */
    @Size(max = 200, message = "职位描述长度不能超过200个字符")
    private String intro;
}

控制器实现(校验逻辑处理)

创建 EmployeeController,通过 @Validated 注解启用校验,BindingResult 接收校验错误信息:

package com.controller;

import com.pojo.Employee;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/emp")
public class EmployeeController {

    /**
     * 注解 @Validated 表示使用JSR303校验
     * @param employee 表单数据
     * @param result 校验不通过时,错误信息会填充进该对象
     * @return 逻辑视图名称
     */
    @RequestMapping("/save")
    public String save(@Validated @ModelAttribute Employee employee, BindingResult result) {
        // 校验不通过
        if (result.hasErrors()) {
            result.getAllErrors().forEach(e -> {
                System.out.println("code:" + e.toString() + ";object name" + e.getObjectName() + ";message" + e.getDefaultMessage());
                System.out.println(e);
                System.out.println("========================================================");
            });
            // 返回添加员工视图
            return "add_emp";
        } else {
            // 保存员工信息
            return "employees";
        }
    }

    @RequestMapping("/add")
    public String saveEmployeeUI() {
        return "add_emp"; // 跳转到添加员工页面
    }
}

Spring MVC 配置(ServletConfig)

配置视图解析器、静态资源、跨域:

package com.config;

import org.hibernate.validator.HibernateValidator;
import org.jspecify.annotations.Nullable;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@ComponentScan(basePackages = "com.controller")
@EnableWebMvc // 以注解方式驱动 Spring MVC
public class ServletConfig implements WebMvcConfigurer {
    private final MessageSource messageSource;

    public ServletConfig(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * 配置静态资源不被拦截
     * @param registry 资源处理器
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") // 静态资源URL:根路径下
                .addResourceLocations("/public/"); // 静态资源存放位置
    }

    /**
     * 全局跨域配置(局部跨域使用@CrossOrigin注解)
     * @param registry 跨域处理器
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许所有请求跨域
                .allowedOrigins("http://localhost:5173") // 跨域来源(携带Cookie时不可设为*)
                .allowedHeaders("*") // 允许所有请求头
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD") // 允许的请求方式
                .allowCredentials(true) // 允许携带Cookie
                .maxAge(3600); // 跨域缓存时间(1小时)
    }

    /**
     * 配置视图解析器
     * @param registry 视图解析器
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(
                new InternalResourceViewResolver("/WEB-INF/pages/", ".jsp")
        );
    }
}

员工列表页面(employees.jsp)

校验通过后跳转的页面:

<%--
  Created by IntelliJ IDEA.
  User: Jing61
  Date: 2025/12/25
  Time: 10:20
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
员工列表
</body>
</html>

测试

image
直接提交,控制台自行查看输出,会出现 System.out.println 打印的错误信息,且再次跳转到这个页面。

页面错误信息显示

jsp 页面

通过 Spring MVC 提供的标签库 <mvc:errors> 在页面显示校验错误信息,修改 add_emp.jsp

<%@ taglib prefix="mvc" uri="http://www.springframework.org/tags/form" %>
<%--
  Created by IntelliJ IDEA.
  User: Jing61
  Date: 2025/12/25
  Time: 09:39
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>添加员工</title>
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>
    <mvc:form action="/emp/save" method="post" modelAttribute="employee">
        <div>
            <label>username:</label>
            <input type="text" name="username" placeholder="用户名,长度在4-12位之间" value="${employee.username}"/>
            <span class="error">
                <mvc:errors path="username" />
            </span>
        </div>
        <div>
            <label>password:</label>
            <input type="password" name="password" value="${employee.password}"/>
            <span class="error">
                <mvc:errors path="password" />
            </span>
        </div>
        <div>
            <label>email:</label>
            <input type="email" name="email" placeholder="电子邮箱" value="${employee.email}"/>
            <span class="error">
                <mvc:errors path="email" />
            </span>
        </div>
        <div>
            <label>phone:</label>
            <input type="text" name="phone" placeholder="手机号码" value="${employee.phone}"/>
            <span class="error">
                <mvc:errors path="phone" />
            </span>
        </div>
        <div>
            <label>birthday:</label>
            <input type="date" name="birthday" placeholder="生日" value="${employee.birthday}"/>
            <span class="error">
                <mvc:errors path="birthday" />
            </span>
        </div>
        <div>
            <label>hire date:</label>
            <input type="date" name="hireDate" placeholder="入职日期" value="${employee.hireDate}"/>
            <span class="error">
                <mvc:errors path="hireDate" />
            </span>
        </div>

        <div>
            <label>salary:</label>
            <input type="number" name="salary" placeholder="薪水" value="${employee.salary}"/>
            <span class="error">
                <mvc:errors path="salary" />
            </span>
        </div>
        <div>
            <label>intro:</label> <br/>
            <textarea name="intro" rows="5" cols="40" placeholder="员工简介" >${employee.intro}</textarea>
            <span class="error">
                <mvc:errors path="intro" />
            </span>
        </div>
        <div>
            <input type="submit" />
        </div>
    </mvc:form>
</body>
</html>

控制器

通过注解 @ModelAttribute 将表单数据封装成 Employee 对象。

package com.controller;

import com.pojo.Employee;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/emp")
public class EmployeeController {

    /**
     * 注解 @Validated 表示使用JSR303校验,这个Bean将会被校验表单数据(是否满足定义的校验规则)
     * 注解 @ModelAttribute 表示将表单数据封装成 Employee 对象
     * @param employee 表单数据
     * @param result 校验不通过会将错误信息填充进该对象中
     * @return 逻辑视图名称
     */
    @RequestMapping("/save")
    public String save(@Validated @ModelAttribute Employee employee, BindingResult result) {
        // 校验不通过
        if (result.hasErrors()) {
            result.getAllErrors().forEach(e -> {
                System.out.println("code:" + e.toString() + ";object name" + e.getObjectName() + ";message" + e.getDefaultMessage());
                System.out.println(e);
                System.out.println("========================================================");
            });
            // 返回新添加员工视图
            return "add_emp";
        } else {
            // 保存员工信息
            return "employees";
        }
    }

    @RequestMapping("/add")
    public String saveEmployeeUI() {
        return "add_emp"; // 跳转到添加员工页面
    }
}

测试

访问 http://localhost:8080/emp/add,点击提交:
image

输入以下内容:
image

输入正确校验规则后提交:
image

错误信息可配置化(避免硬编码)

看起来似乎非常不错,但是我们的message错误提示信息是硬编码在pojo身上,为了避免其硬编码而实现可配置,我们在src/main/resource下新建validation.properties文件。

创建配置文件

src/main/resource 下新建 validation.properties

employee.username.null = 用户名不能为空
employee.username.size = 用户名长度必须在4-12位之间
employee.password.null = 密码不能为空
employee.email.null = 电子邮箱不能为空
employee.email.pattern = 邮箱格式不正确
employee.phone.pattern = 手机号格式不正确
employee.birthday = 生日必须是一个过去的日期
employee.hire_date = 入职时间必须一个现在或者将来的日期
# 自定义,未实现
employee.salary = 工资必须...
employee.intro = 职位描述长度不能超过200个字符

修改 POJO 注解及配置数据校验器

通过 {key} 引用配置文件中的错误信息:

@NotNull(message = "{employee.username.null}")
@Size(min = 4, max = 12, message = "{employee.username.size}")
private String username;

采用{}表达式对配置文件的key加以读取其对应的value。其它字段同理,到这里还不能读取指定配置文件,需要告诉spring的检验器加载什么样的配置文件,配置代码如下:

package com.config;

import org.hibernate.validator.HibernateValidator;
import org.jspecify.annotations.Nullable;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * Spring 为了方便 spring mvc 相关配置,创建了 WebMvcConfigurer 接口
 */
@Configuration
@ComponentScan(basePackages = "com.controller")
@EnableWebMvc // 以注解的方式驱动 Spring MVC(其中包含了注解映射的HandlerMapping以及HandlerAdapter等相关主键)
public class ServletConfig implements WebMvcConfigurer {
    private final MessageSource messageSource;

    public ServletConfig(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    // 实现 WebMvcConfig 接口

    /**
     * 配置静态资源不被拦截
     * 静态资源:css, js, 图片, 视频, 音频, 文档
     * @param registry 资源处理器
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") // 静态资源的url:根路径下
                .addResourceLocations("/public/"); // 静态资源存放的位置(访问链接就不用public)
    }

    /**
     * 处理跨域问题(全局),局部跨域跨域使用@CrossOrigin注解
     * @param registry 跨域处理器
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许所有的请求跨域
                .allowedOrigins("http://localhost:5173") // 跨域来源为 http://localhost:5173,如果要携带cookie凭证,allowedOrigins不能设为*
                .allowedHeaders("*") // 允许所有的请求头
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD") // 允许的请求方式
                .allowCredentials(true) // 允许携带 cookie
                .maxAge(3600); // 1 个小时内都允许跨域访问
    }

    /**
     * 配置视图解析器
     * @param registry 视图解析器
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(
                new InternalResourceViewResolver("/WEB-INF/pages/", ".jsp")
        );
    }

    /**
     * 配置数据校验器
     */
    @Override
    public @Nullable Validator getValidator() {
        var validator = new LocalValidatorFactoryBean();
        validator.setProviderClass(HibernateValidator.class);
        validator.setValidationMessageSource(messageSource());
        return validator;
    }

    private MessageSource messageSource() {
        var messageSource = new ReloadableResourceBundleMessageSource();
        // 加载资源文件(默认为 classpath:message)
        messageSource.setBasename("classpath:validation");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(60); // 缓存时间, 60秒
        return messageSource;
    }

}

分组校验

当不同场景需要不同校验规则时,可使用分组校验。

定义分组接口

// 分组校验1
public interface ValidationGroup1 {
}

// 分组校验2
public interface ValidationGroup2 {
}

POJO 注解添加分组

@NotNull(message = "{employee.username.null}", groups = {ValidationGroup1.class})
@Size(min = 4, max = 12, message = "{employee.username.size}", groups = {ValidationGroup2.class})
private String username;

控制器指定分组

@RequestMapping("/save")
public String save(@Validated(ValidationGroup2.class) @ModelAttribute Employee employee, BindingResult result) {
    // 仅校验 ValidationGroup2 分组的规则(如用户名长度)
    if (result.hasErrors()) {
        // 错误处理逻辑
        return "add_emp";
    } else {
        return "employees";
    }
}

此时就只会校验用户名(分组不是ValidationGroup2.class都会不进行校验)是否满足长度要求

自定义校验(业务逻辑校验)

有时候除了简单的输入格式、非空性等校验,也需要一定的业务校验,Spring提供了Validator接口来实现校验,它将在进入控制器逻辑之前对参数的合法性进行校验。例如上面的薪水,比如:不低于该岗位最低薪水。

创建职位枚举

package com.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor // 全参构造函数
public enum Position {
    CLERK(2000,"业务员"),
    SALESMAN(2000,"销售员"),
    ANALYST(5000,"分析员"),
    SEARCHER(3000,"搜索员"),
    MANAGER(6000,"经理"),
    ;

    private float lowSalary; // 职位最低工资
    private String value;
}

实现自定义校验器

package com.validation;

import com.pojo.Employee;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class EmployeeValidator implements Validator {

    /**
     * 判断当前校验器是否用于检验指定类型的 POJO
     * @param clazz POJO 类型
     * @return true:启用校验;false:不校验
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz == Employee.class;
    }

    @Override
    public void validate(Object target, Errors errors) {
        var emp = (Employee) target;
        // 校验工资不低于当前职位最低工资
        if (emp.getSalary() < emp.getJob().getLowSalary()) {
            errors.rejectValue("salary", "LowSalary.employee.salary", 
                               "当前职位的最低工资为:" + emp.getJob().getLowSalary());
        }
    }
}

绑定校验器到控制器

通过 @InitBinder 注解将自定义校验器与控制器绑定:

package com.controller;

import com.pojo.Employee;
import com.validation.EmployeeValidator;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/emp")
public class EmployeeController {

    /**
     * 初始化数据绑定,添加自定义校验器
     * @param binder 数据绑定器
     */
    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.addValidators(new EmployeeValidator());
    }

    @RequestMapping("/save")
    public String save(@Validated @ModelAttribute Employee employee, BindingResult result) {
        if (result.hasErrors()) {
            // 错误处理
            return "add_emp";
        } else {
            return "employees";
        }
    }

    @RequestMapping("/add")
    public String saveEmployeeUI() {
        return "add_emp";
    }
}
posted @ 2025-12-25 14:42  Jing61  阅读(21)  评论(0)    收藏  举报