SpringMVC 数据校验(Validation):JSR 303/349 与 `Hibernate Validator` 集成

好的,没问题!咱们这就来聊聊 SpringMVC 数据校验这档子事儿,保证让你看完之后,腰不酸了,腿不疼了,数据校验也不再是难题了!

SpringMVC 数据校验:让你的数据不再“裸奔”

各位看官,在互联网这个花花世界里,数据就像我们的小秘密,要好好保护起来。如果数据随随便便就能被篡改、注入,那还得了?轻则显示错误,重则系统崩溃,甚至用户信息泄露,想想都可怕!所以,数据校验就显得尤为重要,它就像我们程序的一道安全防线,拦截那些不靠谱的数据,确保数据的完整性和安全性。

SpringMVC 作为 Web 开发的利器,自然也提供了强大的数据校验功能。今天,我们就来深入了解一下 SpringMVC 如何集成 JSR 303/349 规范和 Hibernate Validator,让我们的数据不再“裸奔”。

一、什么是 JSR 303/349?

JSR (Java Specification Requests) 是 Java 规范请求的缩写,简单来说,就是 Java 官方制定的一些标准。JSR 303 和 JSR 349 都是关于 Bean Validation 的规范,它们定义了一套标准的注解,用于声明 Java Bean 的属性约束。

  • JSR 303 (Bean Validation 1.0): 这是最初的 Bean Validation 规范,定义了一些基本的约束注解,例如 @NotNull@Size@Min 等。

  • JSR 349 (Bean Validation 1.1): 这是 JSR 303 的升级版,增加了一些新的特性,例如分组校验、容器元素校验等。

你可以把 JSR 303/349 想象成一套“数据校验说明书”,里面详细规定了各种数据应该符合什么样的规则。

二、Hibernate Validator:JSR 303/349 的得力干将

Hibernate Validator 是 Bean Validation 规范的一个具体实现。简单来说,它就是按照 JSR 303/349 的“说明书”来实现数据校验功能的。Hibernate Validator 提供了丰富的校验注解,并且可以方便地进行扩展,满足各种复杂的校验需求。

你可以把 Hibernate Validator 想象成一个“数据校验执行官”,它会严格按照 JSR 303/349 的规定,对数据进行校验。

三、SpringMVC 如何集成 JSR 303/349 和 Hibernate Validator?

SpringMVC 提供了非常方便的方式来集成 JSR 303/349 和 Hibernate Validator。下面我们通过一个简单的例子来说明如何实现:

1. 添加依赖

首先,我们需要在 pom.xml 文件中添加 Hibernate Validator 的依赖:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

注意: Spring Boot 2.3+ 默认集成了 hibernate-validator,可以省略此步骤。如果使用更早的版本,建议添加依赖。

2. 开启 SpringMVC 的校验功能

在 SpringMVC 的配置文件中(例如 spring-mvc.xml),我们需要配置 LocalValidatorFactoryBean 来开启校验功能:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <!-- 配置校验器,这里我们使用 Hibernate Validator -->
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <!-- 配置快速失败模式,如果有一个校验不通过,立即返回错误 -->
    <property name="validationMessageSource" ref="messageSource"/>
</bean>

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:messages"/>
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

这里解释一下:

  • LocalValidatorFactoryBean:这是 Spring 提供的 Bean,用于集成 Bean Validation 规范。
  • providerClass:指定 Bean Validation 的实现类,这里我们使用 Hibernate Validator。
  • validationMessageSource: 指定国际化消息文件,用于显示校验错误信息。

3. 在 Controller 中使用校验

现在,我们就可以在 Controller 中使用校验功能了。首先,我们需要在需要校验的 Bean 参数上添加 @Valid 注解,然后在 Controller 方法中注入 BindingResult 对象,用于获取校验结果。

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.validation.Valid;

@Controller
public class UserController {

    @GetMapping("/register")
    public String registerForm(Model model) {
        model.addAttribute("user", new User());
        return "register";
    }

    @PostMapping("/register")
    public String registerSubmit(@Valid User user, BindingResult result, Model model) {
        if (result.hasErrors()) {
            // 如果有校验错误,返回注册页面,显示错误信息
            return "register";
        }

        // 如果校验通过,处理注册逻辑
        System.out.println("注册成功:" + user);
        return "success";
    }
}

在这个例子中,我们定义了一个 User 类,并在 registerSubmit 方法中使用 @Valid 注解来校验 User 对象。如果校验不通过,BindingResult 对象会包含错误信息,我们可以在页面上显示这些错误信息。

4. 定义需要校验的 Bean

接下来,我们需要定义 User 类,并在其属性上添加校验注解:

import javax.validation.constraints.*;

public class User {

    @NotEmpty(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在 3 到 20 之间")
    private String username;

    @NotEmpty(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在 6 到 20 之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Min(value = 18, message = "必须年满 18 岁才能注册")
    private int age;

    // 省略 getter 和 setter 方法

    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 int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + ''' +
                ", password='" + password + ''' +
                ", email='" + email + ''' +
                ", age=" + age +
                '}';
    }
}

在这个例子中,我们使用了以下校验注解:

  • @NotEmpty:验证字符串不为空。
  • @Size:验证字符串的长度在指定范围内。
  • @Email:验证字符串是否为有效的邮箱地址。
  • @Min:验证数字的最小值。

5. 显示错误信息

最后,我们需要在页面上显示错误信息。我们可以使用 Spring 的 form 标签库来实现:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE html>
<html>
<head>
    <title>注册</title>
</head>
<body>
    <h1>注册</h1>
    <form:form modelAttribute="user" method="post" action="/register">
        <p>
            用户名:<form:input path="username"/><form:errors path="username"/>
        </p>
        <p>
            密码:<form:password path="password"/><form:errors path="password"/>
        </p>
        <p>
            邮箱:<form:input path="email"/><form:errors path="email"/>
        </p>
        <p>
            年龄:<form:input path="age"/><form:errors path="age"/>
        </p>
        <button type="submit">注册</button>
    </form:form>
</body>
</html>

在这个例子中,我们使用 <form:errors> 标签来显示每个字段的错误信息。

四、常用的校验注解

Hibernate Validator 提供了丰富的校验注解,可以满足各种常见的校验需求。下面是一些常用的校验注解:

注解 说明
@Null 验证对象是否为 null
@NotNull 验证对象是否不为 null
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
@Min(value) 验证 Number 或 String 对象是否大等于指定的值。
@Max(value) 验证 Number 或 String 对象是否小等于指定的值。
@DecimalMin(value) 验证 Number 或 String 对象是否大等于指定的值,value 是字符串形式。
@DecimalMax(value) 验证 Number 或 String 对象是否小等于指定的值,value 是字符串形式。
@Size(min, max) 验证 String、Collection、Map 或 Array 对象是否在指定大小之间。
@Digits(integer, fraction) 验证 Number 或 String 对象是否为一个合法的数字,integer 指定整数部分的位数,fraction 指定小数部分的位数。
@Past 验证 Date 或 Calendar 对象是否在当前时间之前。
@Future 验证 Date 或 Calendar 对象是否在当前时间之后。
@Pattern(regexp) 验证 String 对象是否符合指定的正则表达式。
@Email 验证 String 对象是否为有效的邮箱地址。
@NotEmpty 验证 String、Collection、Map 或 Array 对象是否不为空。
@NotBlank 验证 String 对象是否不为空,并且去除前后空格后长度大于 0。
@Range(min, max) 验证 Number 或 String 对象是否在指定范围内 (Hibernate Validator 扩展)。
@URL 验证 String 对象是否为有效的 URL (Hibernate Validator 扩展)。

五、分组校验

有时候,我们需要对同一个 Bean 在不同的场景下进行不同的校验。例如,在注册时,我们需要校验密码是否为空,而在修改密码时,我们还需要校验旧密码是否正确。这时,我们就可以使用分组校验来实现。

首先,我们需要定义一个或多个分组接口:

public interface RegisterGroup {}

public interface UpdateGroup {}

然后,在校验注解中指定分组:

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class User {

    @NotEmpty(message = "用户名不能为空", groups = {RegisterGroup.class})
    @Size(min = 3, max = 20, message = "用户名长度必须在 3 到 20 之间", groups = {RegisterGroup.class, UpdateGroup.class})
    private String username;

    @NotEmpty(message = "密码不能为空", groups = {RegisterGroup.class})
    @Size(min = 6, max = 20, message = "密码长度必须在 6 到 20 之间", groups = {RegisterGroup.class, UpdateGroup.class})
    private String password;

    // 省略 getter 和 setter 方法
}

在这个例子中,usernamepassword 字段在 RegisterGroupUpdateGroup 两个分组下都需要进行校验。

最后,在 Controller 中指定需要校验的分组:

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class UserController {

    @PostMapping("/register")
    public String registerSubmit(@Validated({RegisterGroup.class}) User user, BindingResult result) {
        // ...
    }

    @PostMapping("/update")
    public String updateSubmit(@Validated({UpdateGroup.class}) User user, BindingResult result) {
        // ...
    }
}

在这个例子中,registerSubmit 方法只校验 RegisterGroup 分组下的字段,而 updateSubmit 方法只校验 UpdateGroup 分组下的字段。

六、自定义校验

除了使用 Hibernate Validator 提供的校验注解之外,我们还可以自定义校验器来实现更复杂的校验逻辑。

首先,我们需要创建一个自定义的校验注解:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = {PasswordStrengthValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordStrength {

    String message() default "密码强度不够";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

然后,我们需要创建一个自定义的校验器,实现 ConstraintValidator 接口:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PasswordStrengthValidator implements ConstraintValidator<PasswordStrength, String> {

    @Override
    public void initialize(PasswordStrength constraintAnnotation) {
        // 初始化逻辑,可以获取注解中的参数
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 校验逻辑
        if (value == null || value.length() < 8) {
            return false;
        }
        // 校验密码是否包含数字和字母
        boolean hasDigit = false;
        boolean hasLetter = false;
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            if (Character.isDigit(c)) {
                hasDigit = true;
            } else if (Character.isLetter(c)) {
                hasLetter = true;
            }
        }
        return hasDigit && hasLetter;
    }
}

在这个例子中,PasswordStrengthValidator 校验器校验密码的长度是否大于等于 8,并且是否包含数字和字母。

最后,在需要校验的字段上添加自定义的校验注解:

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class User {

    @NotEmpty(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在 3 到 20 之间")
    private String username;

    @PasswordStrength(message = "密码强度不够")
    private String password;

    // 省略 getter 和 setter 方法
}

七、国际化

我们可以使用国际化来显示不同语言的错误信息。首先,我们需要创建一个或多个国际化消息文件,例如 messages.propertiesmessages_zh_CN.propertiesmessages_en_US.properties

messages.properties 文件中,我们可以定义默认的错误信息:

javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.Size.message = 长度必须在 {min} 到 {max} 之间
com.example.PasswordStrength.message = 密码强度不够

messages_zh_CN.properties 文件中,我们可以定义中文的错误信息:

javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.Size.message = 长度必须在 {min} 到 {max} 之间
com.example.PasswordStrength.message = 密码强度不够

messages_en_US.properties 文件中,我们可以定义英文的错误信息:

javax.validation.constraints.NotEmpty.message = cannot be empty
javax.validation.constraints.Size.message = length must be between {min} and {max}
com.example.PasswordStrength.message = password strength is not enough

然后,在 SpringMVC 的配置文件中配置 messageSource Bean:

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:messages"/>
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

这样,SpringMVC 就会根据当前的 Locale 来选择合适的国际化消息文件,显示对应语言的错误信息。

八、总结

SpringMVC 集成 JSR 303/349 和 Hibernate Validator 可以让我们轻松地实现数据校验功能,保护我们的数据安全。通过本文的介绍,相信你已经掌握了 SpringMVC 数据校验的基本用法,可以灵活地运用到实际项目中。

记住,数据校验就像给你的程序穿上一件防弹衣,让你的数据不再“裸奔”,安全可靠!

希望这篇文章对你有所帮助!如果你还有其他问题,欢迎随时提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注