当前位置: 代码网 > it编程>编程语言>Java > SpringBoot如何使用validator框架优雅地校验参数

SpringBoot如何使用validator框架优雅地校验参数

2025年02月14日 Java 我要评论
1、为什么要校验参数?在日常的开发中,为了防止非法参数对业务造成影响,需要对接口的参数进行校验,以便正确性地入库。例如:登录时,就需要判断用户名、密码等信息是否为空。虽然前端也有校验,但为了接口的安全

1、为什么要校验参数?

在日常的开发中,为了防止非法参数对业务造成影响,需要对接口的参数进行校验,以便正确性地入库。

例如:登录时,就需要判断用户名、密码等信息是否为空。虽然前端也有校验,但为了接口的安全性,后端接口还是有必要进行参数校验的。

同时,为了校验参数更加优雅,这里就介绍了 spring validation 方式。

  • java api 规范(jsr303:java ee 6 中的一项子规范,叫做 bean validation)定义了 bean 校验的标准 validation-api,但没有提供实现。
  • hibernate validation 是对这个规范的实现,并增加了校验注解。如:@email、@length。

jsr 官网

hibernate validator 官网

spring validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

2、引入依赖

如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于等于 2.3.x,则需要手动引入依赖。

<dependency>
    <groupid>org.hibernate.validator</groupid>
    <artifactid>hibernate-validator</artifactid>
    <version>8.0.0.final</version>
</dependency>

对于 web 服务来说,为防止非法参数对业务造成影响,在 controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  • post、put 请求,使用 @requestbody 接收参数
  • get 请求,使用 @requestparam、@pathvariable 接收参数

3、@requestbody 参数校验

对于 post、put 请求,后端一般会使用 @requestbody + 对象 接收参数。此时,只需要给对象添加 @validated 或 @valid 注解,即可轻松实现自动校验参数。如果校验失败,会抛出 methodargumentnotvalidexception 异常。

uservo :添加校验注解

@data
public class uservo {

	private long id;

    @notnull
    @length(min = 2, max = 10)
    private string username;

    @notnull
    @length(min = 6, max = 20)
    private string account;

    @notnull
    @length(min = 6, max = 20)
    private string password;
}

usercontroller :

@restcontroller
@requestmapping("/user")
public class usercontroller {

    @postmapping("/adduser")
    public string adduser(@requestbody @valid uservo uservo) {
        return "adduser";
    }
}

或者使用 @validated 注解:

@postmapping("/adduser")
public string adduser(@requestbody @validated uservo uservo) {
    return "adduser";
}

4、@requestparam、@pathvariable 参数校验

get 请求一般会使用 @requestparam、@pathvariable 注解接收参数。如果参数比较多(比如超过 5 个),还是推荐使用对象接收。否则,推荐将一个个参数平铺到方法入参中。

在这种情况下,必须在 controller 类上标注 @validated 注解,并在入参上声明约束注解(如:@min )。如果校验失败,会抛出 constraintviolationexception 异常

@restcontroller
@requestmapping("/user")
@validated
public class usercontroller {

    @getmapping("/getuser")
    public string getuser(@min(1l) long id) {
        return "getuser";
    }
}

5、统一异常处理

如果校验失败,会抛出 methodargumentnotvalidexception 或者 constraintviolationexception 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示

@restcontrolleradvice
public class exceptioncontrolleradvice {

    @exceptionhandler({methodargumentnotvalidexception.class})
    @responsestatus(httpstatus.ok)
    public string handlemethodargumentnotvalidexception(methodargumentnotvalidexception ex) {
        bindingresult bindingresult = ex.getbindingresult();
        stringbuilder sb = new stringbuilder("校验失败:");
        for (fielderror fielderror : bindingresult.getfielderrors()) {
            sb.append(fielderror.getfield()).append(":").append(fielderror.getdefaultmessage()).append(", ");
        }
        string msg = sb.tostring();
        return "参数校验失败" + msg;
    }

    @exceptionhandler({constraintviolationexception.class})
    public string handleconstraintviolationexception(constraintviolationexception ex) {
        return "参数校验失败" + ex;
    }

}

6、分组校验

在实际项目中,可能多个方法需要使用同一个类对象来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。

如:保存 user 的时候,userid 是可空的,但是更新 user 的时候,userid 的值必须 >= 1l;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

约束注解上声明适用的分组信息 groups

6.1、定义分组接口

public interface validgroup extends default {
	// 添加操作
    interface save extends validgroup {}
    // 更新操作
    interface update extends validgroup {}
	// ...
}

为什么要继承 default ?下文有。

6.2、给需要校验的字段分配分组

@data
public class uservo {
	
    @null(groups = validgroup.save.class, message = "id要为空")
    @notnull(groups = validgroup.update.class, message = "id不能为空")
    private long id;

    @notblank(groups = validgroup.save.class, message = "用户名不能为空")
    @length(min = 2, max = 10)
    private string username;

    @email
    @notnull
    private string email;
}

根据校验字段看:

  • id:分配分组:save、update。添加时,一定为 null;更新时,一定不为 null
  • username:分配分组:save。添加时,一定不能为空
  • email:分配分组:无。即:使用默认的分组

6.3、给需要校验的参数指定分组

@restcontroller
@requestmapping("/user")
public class usercontroller {

    @postmapping("/adduser")
    public string adduser(@requestbody @validated(validgroup.save.class) uservo uservo) {
        return "adduser";
    }

    @postmapping("/updateuser")
    public string updateuser(@requestbody @validated(validgroup.update.class) uservo uservo) {
        return "updateuser";
    }
}

测试校验。

6.4、默认分组

如果 validgroup 接口 不继承 default 接口,那么,将无法校验 email 字段(未分配分组);

继承后,validgroup 就属于 default 类型,即:默认分组/所以,可以对 email 校验

7、嵌套校验

必须要用 @valid 注解

@data
public class uservo {

	@notnull(groups = {validgroup.save.class, validgroup.update.class})
    @valid
    private address address;
}

8、自定义校验

8.1、案例一、自定义校验 加密id

假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步:

8.1.1、自定义约束注解

@target({method, field, annotation_type, constructor, parameter})
@retention(runtime)
@documented
@constraint(validatedby = {encryptidvalidator.class}) // 自定义验证器
public @interface encryptid {

    // 默认错误消息
    string message() default "加密id格式错误";
    // 分组
    class<?>[] groups() default {};
    // 负载
    class<? extends payload>[] payload() default {};
}

8.1.2、编写约束校验器

public class encryptidvalidator implements constraintvalidator<encryptid, string> {

    private static final pattern pattern = pattern.compile("^[a-f\\d]{32,256}$");

    @override
    public boolean isvalid(string value, constraintvalidatorcontext constraintvalidatorcontext) {
        if (value != null) {
            matcher matcher = pattern.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

8.1.3、使用

@data
public class uservo {

    @encryptid
    private string id;
}

8.2、案例二、自定义校验 性别只允许两个值

uservo 类中的 sex 性别属性,只允许前端传递传 m,f 这2个枚举值,如何实现呢?

8.2.1、自定义约束注解

@target({method, field, annotation_type, constructor, parameter})
@retention(runtime)
@documented
@constraint(validatedby = {sexvalidator.class})
public @interface sexvalid {


    // 默认错误消息
    string message() default "value not in enum values";

    // 分组
    class<?>[] groups() default {};

    // 负载
    class<? extends payload>[] payload() default {};

    string[] value();
}

8.2.2、编写约束校验器

public class sexvalidator implements constraintvalidator<sexvalid, string> {

    private list<string> sexs;

    @override
    public void initialize(sexvalid constraintannotation) {
        sexs = arrays.aslist(constraintannotation.value());
    }

    @override
    public boolean isvalid(string value, constraintvalidatorcontext constraintvalidatorcontext) {
        if (stringutils.isempty(value)) {
            return true;
        }
        return sexs.contains(value);
    }

}

8.2.3、使用

@data
public class uservo {

    @sexvalid(value = {"f", "m"}, message = "性别只允许为f或m")
    private string sex;

}
```### 8.2.4、测试
```java
@getmapping("/get")
private string get(@requestbody @validated uservo uservo) {
    return "get";
}

9、实现校验业务规则

业务规则校验 指 接口需要满足某些特定的业务规则。举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。 这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。

最优雅的实现方法应该是参考 bean validation 的标准方式,借助自定义校验注解完成业务规则校验。

9.1、自定义约束注解

首先我们需要创建两个自定义注解,用于业务规则校验:

  • uniqueuser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
  • notconflictuser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
@documented
@retention(runtime)
@target({field, method, parameter, type})
@constraint(validatedby = uservalidator.uniqueuservalidator.class)
public @interface uniqueuser {

    string message() default "用户名、手机号码、邮箱不允许与现存用户重复";
    class<?>[] groups() default {};
    class<? extends payload>[] payload() default {};

}
@documented
@retention(runtime)
@target({field, method, parameter, type})
@constraint(validatedby = uservalidator.notconflictuservalidator.class)
public @interface notconflictuser {

    string message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    class<?>[] groups() default {};
    class<? extends payload>[] payload() default {};

}

9.2、编写约束校验器

想让自定义验证注解生效,需要实现 constraintvalidator 接口。接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类,因为需要校验多个参数,我们直接传入用户对象。 需要提到的一点是 constraintvalidator 接口的实现类无需添加 @component 它在启动的时候就已经被加载到容器中了。

public class uservalidator<t extends annotation> implements constraintvalidator<t, uservo> {

    protected predicate<uservo> predicate = c -> true;

    @override
    public boolean isvalid(uservo uservo, constraintvalidatorcontext constraintvalidatorcontext) {
        return predicate.test(uservo);
    }

    public static class uniqueuservalidator extends uservalidator<uniqueuser>{
        @override
        public void initialize(uniqueuser uniqueuser) {
            userdao userdao = applicationcontextholder.getbean(userdao.class);
            predicate = c -> !userdao.existsbyusernameoremailortelphone(c.getusername(),c.getemail(),c.gettelphone());
        }
    }

    public static class notconflictuservalidator extends uservalidator<notconflictuser>{
        @override
        public void initialize(notconflictuser notconflictuser) {
            predicate = c -> {
                userdao userdao = applicationcontextholder.getbean(userdao.class);
                collection<uservo> collection = userdao.findbyusernameoremailortelphone(c.getusername(), c.getemail(), c.gettelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isempty() || (collection.size() == 1 && collection.iterator().next().getid().equals(c.getid()));
            };
        }
    }

}
@component
public class applicationcontextholder implements applicationcontextaware {

    private static applicationcontext context;

    @override
    public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {
        context = applicationcontext;
    }

    public static applicationcontext getcontext() {
        return context;
    }

    public static object getbean(string name) {
        return context != null ? context.getbean(name) : null;
    }

    public static <t> t getbean(class<t> clz) {
        return context != null ? context.getbean(clz) : null;
    }

    public static <t> t getbean(string name, class<t> clz) {
        return context != null ? context.getbean(name, clz) : null;
    }

    public static void addapplicationlistenerbean(string listenerbeanname) {
        if (context != null) {
            applicationeventmulticaster applicationeventmulticaster = (applicationeventmulticaster)context.getbean(applicationeventmulticaster.class);
            applicationeventmulticaster.addapplicationlistenerbean(listenerbeanname);
        }
    }

}

9.3、测试

@restcontroller
@requestmapping("/user")
public class usercontroller {

    @postmapping("/adduser")
    public string adduser(@requestbody @uniqueuser uservo uservo) {
        return "adduser";
    }

    @postmapping("/updateuser")
    public string updateuser(@requestbody @notconflictuser uservo uservo) {
        return "updateuser";
    }
}

10、@valid 和 @validated 的区别

区别如下:

11、常用注解

bean validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

注解详细信息
@null任意类型。被注释的元素必须为 null
@notnull任意类型。被注释的元素不为 null
@min(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@max(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@decimalmin(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
@decimalmax(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
@size(max, min)字符串、集合、map、数组类型。被注释的元素的大小(长度)必须在指定的范围内
@digits (integer, fraction)数值类型、数值型字符串类型。其值必须在可接受的范围内。 integer:整数精度;fraction:小数精度
@past日期类型。被注释的元素必须是一个过去的日期
@future日期类型。被注释的元素必须是一个将来的日期
@pattern(value)字符串类型。被注释的元素必须符合指定的正则表达式

hibernate validator 在原有的基础上也内嵌了几个注解,如下:

注解详细信息
@email字符串类型。被注释的元素必须是电子邮箱地址
@length字符串类型。被注释的字符串的长度必须在指定的范围内
@notempty字符串、集合、map、数组类型。 被注释的元素的长度必须非空
@range数值类型、字符串类型。 被注释的元素必须在合适的范围内

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com