springmvc 数据校验

 

数据验证的必要性

对于任何一个应用而言,客户端做的数据有效性验证都不是安全有效的,而数据验证又是一个企业级项目架构上最为基础的功能模块,这时候就要求我们在服务端接收到数据的时候也对数据的有效性进行验证。比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉又比较麻烦:

验证代码繁琐,重复劳动
方法内代码显得冗长
每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码

hibernate validator 提供了一套比较完善、便捷的验证实现方式。
spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。

validator校验demo

先来看一个简单的demo,添加了Validator的注解

1

2

3

import javax.validation.Valid;

import javax.validation.constraints.Min;

import javax.validation.constraints.NotNull;

在需要验证的model属性上添加验证注解

@Data

public class SupplierBiz {

 

    private Long id;

 

    @NotNull(message = "供应商服务对象id不能为空")

    @Min(value = 1, message = "供应商服务对象id必须大于0")

    private Long branchId;

 

    @NotNull(message = "供应商类型不能为空")

    private Integer type;

 

    @NotNull(message = "供应商公司id不能为空")

    @Min(value = 1, message = "供应商公司id必须大于0")

    private Long supplierId;

 

    @NotNull(message = "合同开始时间不能为空")

    private Date contractStartTime;

 

 

}

要在做验证的Service接口上添加注解@Validated,在需要验证的接口方法实体参数前添加注解@Validated,代码如下:

import org.springframework.validation.annotation.Validated;

 

@Validated

public interface SupplierBizService {

    long create(@Validated SupplierBiz supplierBiz, List<SupplierBizContact> supplierBizContacts, long userId);

}

以上是对实体属性的验证,当然也可以直接对接口方法的参数添加注解,示例如下:

import javax.validation.constraints.NotNull;

import javax.validation.constraints.Min;

import org.springframework.validation.annotation.Validated;

 

@Validated

public interface SupplierBizService {

    int update(NotNull(message="id不能为空"@Min(value = 1, message = "id不能小于1") Long id, @Validated SupplierBiz supplierBiz, List<SupplierBizContact> supplierBizContacts, long userId);

}

验证标签含义

参数验证非常方便,字段上注解+验证不通过提示信息即可代替手写一大堆的非空和字段限制验证代码。当然除上述看到的验证注解,还有很多,我做的简单整理,如有遗漏,欢迎大家补充:

限制

说明

@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之间
@Past验证注解的元素值(日期类型)比当前时间早
@NotEmpty验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

数据验证进阶

以上是对数据验证的简单介绍,接下来咱们深入了解下参数校验的玩法。

个性化验证

需要依赖实体的其中几个属性去做判断验证。(如:添加供应商时,选择供应商属性"结算账期"为"授信账期"时,那么"授信账期天数"、"授信额度是否限制"、"授信额度"三个属性必填;反之,这三个属性不需验证是否填写),示例代码如下:

import org.hibernate.validator.constraints.ScriptAssert;

import lombok.Data;

 

@ScriptAssert(lang = "javascript", script = "com.leading.supplierservice.domain.supplierbiz.model.AbstractSupplierBiz.checkPaymentDaysType(_this.paymentDaysType,_this.paymentDays,_this.limited,_this.creditLine)", message = "结算账期为授信账期时,填写数据错误")

@Data

public class SupplierBiz {

    /**

     * 授信额度是否受限

     */

    private Boolean limited;

 

    /**

     * 账期天数

     */

    private Integer paymentDays;

 

    /**

     * 账期类型

     */

    private Integer paymentDaysType;

 

    /**

     * 授信额度

     */

    private BigDecimal creditLine;

 

 

    public static boolean checkPaymentDaysType(Integer paymentDaysType, Integer paymentDays, Boolean limited,

            BigDecimal creditLine) {

        SupplierBizPaymentDaysType supplierBizPaymentDaysType = SupplierBizPaymentDaysType.getByValue(paymentDaysType);

        if (SupplierBizPaymentDaysType.CREDIT_PERIOD.equals(supplierBizPaymentDaysType)) {

            if (null == paymentDays || null == limited || null == creditLine) {

                return false;

            }

        }

 

        return true;

    }

 

 

}

对上述代码做下简单说明:@ScriptAssert是脚本验证,script属性要填写你自己的验证方法,此方法必须是public static 修饰,message是验证未通过时的错误信息。对checkPaymentDaysType验证方法做下说明,验证通过时返回true,验证未通过返回false。

重复验证

相信大家肯定经常遇到一些重复性的验证,这是需要去查询数据库,并不能简单验证实体属性,以下即是对这种情况的示例,也是通过@ScriptAssert脚本注解实现,在需要验证的实体写法如下:

import org.hibernate.validator.constraints.ScriptAssert;

import lombok.Data;

 

@ScriptAssert(lang = "javascript", script = "com.leading.supplierservice.domain.supplierbiz.model.SupplierValidator.validateDuplicateName(_this.name,_this.socialCreditCode)", message = "供应商名称不可重复")

@Data

public class SupplierBiz {

 

    /**

     * 供应商公司名称

     */

    private String name;

 

    /**

     * 统一社会信用代码

     */

    private String socialCreditCode;

 

}

验证名称重复的组件

@Component

public class SupplierValidator {

 

    private static SupplierMapper supplierMapper;

 

    @Autowired

    public void setSupplierMapper(SupplierMapper supplierMapper) {

        SupplierValidator.supplierMapper = supplierMapper;

    }

 

    public static boolean validateDuplicateName(String name, String socialCreditCode) {

        Supplier supplier = supplierMapper.checkSupplierNameAndSocialCreditCode(name, socialCreditCode);

 

        // 已存在返回false

        if (null != supplier) {

            return false;

        }

 

        return true;

    }

 

}

借助于验证组件完成名称重复的验证。

对象级联校验

以上验证都是对实体单个属性,如果实体属性是个自定义对象,同样需要对属性对象做验证时,怎么实现级联验证呢?很简单,在需要验证的属性上添加@Valid注解即可:

import javax.validation.Valid;

import javax.validation.constraints.NotNull;

import lombok.Data;

 

@Data

public class SupplierBiz {

 

    private Long id;

 

    @NotNull(message = "供应商服务对象id不能为空")

    @Min(value = 1, message = "供应商服务对象id必须大于0")

    private Long branchId;

 

    @NotNull(message = "供应商联系人不能为空")

    @Valid

    private SupplierBizContact supplierBizContact;

 

 

}

这样就能实现对象级联验证,不仅对实体本身属性做验证,也会对复杂对象属性进行验证。

分组校验

经过以上介绍,大家已经能基本使用validator进行验证。但如果针对同一个实体的同一个属性,方法A需要验证不能为空;方法B不需要进行验证,这种情况,如果不再新建一个实体的情况下,怎么来解决呢,下面我们所说的分组即可解决此场景问题。

举例:供应商实体,在添加时,id属性可以为空;但是在修改时,id属性不能为空,针对这种情况,做下代码实现:

验证实体

import javax.validation.Valid;

import javax.validation.constraints.Min;

import javax.validation.constraints.NotNull;

import lombok.Data;

 

@Data

public class SupplierBiz {

    public interface Create {

    }

    public interface Update {

    }

 

 

    /**

     * 供应商业务信息id

     */

    @NotNull(message = "供应商业务信息id不能为空", groups = Update.class)

    private Long id;

 

    /**

     * 供应商服务对象id(branch表id)

     */

    @NotNull(message = "供应商服务对象id不能为空", groups = Create.class)

    @Min(value = 1, message = "供应商服务对象id必须大于0")

    private Long branchId;

 

    /**

     * 供应商类型

     */

    @NotNull(message = "供应商类型不能为空", groups = { Create.class, Update.class })

    private Integer type;

 

    /**

     * 供应商公司id(supplier表id)

     */

    @NotNull(message = "供应商公司id不能为空")

    @Min(value = 1, message = "供应商公司id必须大于0")

    private Long supplierId;

 

    /**

     * 合同开始时间

     */

    @NotNull(message = "合同开始时间不能为空")

    private Date contractStartTime;

 

 

}

接下来在Service中的代码如下:

分组验证Service

import org.springframework.validation.annotation.Validated;

import com.leading.supplierservice.domain.supplierbiz.model.AbstractSupplierBiz.Create;

import com.leading.supplierservice.domain.supplierbiz.model.AbstractSupplierBiz.Update;

import javax.validation.groups.Default;

 

@Validated

public interface SupplierBizService {

 

    long create(@Validated({ Create.class, Default.class}) SupplierBiz supplierBiz, List<SupplierBizContact> supplierBizContacts, long userId);

 

    int update(long id, @Validated({ Update.class, Create.class }) SupplierBiz supplierBiz,

            List<SupplierBizContact> supplierBizContacts, long userId);

}

对以上代码做下解释:首先我们能在实体看到验证注解中多了一个groups属性,它的值是接口的集合(必须是接口),这样就定义了分组,同样可以定义多个分组(如:{ Create.class, Update.class })或者没有分组;那么Service方法在做验证的时候就需要去添加分组的属性。此处对分组验证范围说下:

如果Service验证注解添加了分组,那么就只会对实体上有该分组的属性;

如果Service验证注解没有分组,那么就只对实体上没有分组设置的属性进行验证;

针对以上示例,我做了个表格,有助于理解:

Service方法

验证注解

验证实体属性

create
@Validated({ Create.class, Default.class})
SupplierBiz.branchId (groups = Create.class)
SupplierBiz.type (groups = { Create.class, Update.class })
SupplierBiz.supplierId (groups 无)
SupplierBiz.contractStartTime (groups 无)
update
@Validated({ Update.class, Create.class })
SupplierBiz.id (groups = Update.class)
SupplierBiz.branchId (groups = Create.class)
SupplierBiz.type (groups = { Create.class, Update.class })

此处注意:没有设置groups分组的,默认即为Default分组。

到此Validator就基本介绍完毕,希望对各位在开发时有所帮助。

注:集合校验需要加@Valid注解

扩展

Spring Validator已经提供了比较完善的验证注解,但有些场景可能还是不能够满足,所以可以自定义编写验证注解,以下是一个自定义注解,代码片段如下:

/**

 * <p>

 * Title: NotRepeatable

 * </p>

 * <p>

 * Description: list集合中对象或对象属性不能重复

 * </p>

 *

 * @author zhanghua

 * @date   2019年1月25日

 */

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ ElementType.FIELD, ElementType.PARAMETER })

@Constraint(validatedBy = { NotRepeatableValidator.class })  //验证逻辑编写类

@Repeatable(NotRepeatable.List.class)

public @interface NotRepeatable {

 

    /*

     * 对于复杂对象元素,填写若干个属性字段,如果不同元素的设置的属性值全部一样,则为重复,返回false

     * 对于基础数据类型,不需要设置该值,设置也会被忽略

     */

    String[] value() default {};

 

    String message() default "集合中不能存在重复元素";

 

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

 

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

 

    @Documented

    @Retention(RetentionPolicy.RUNTIME)

    @Target({ ElementType.FIELD, ElementType.PARAMETER })

    public @interface List {

        NotRepeatable[] value();

    }

 

}

 

@Slf4j

@Component

public class NotRepeatableValidator implements ConstraintValidator<NotRepeatable, List<?>> {

 

    /**

     * 基本数据类型

     */

    private final static Class<?> BASIC_DATA_TYPE[] = { Integer.class, Short.class, Long.class, Float.class,

            Double.class, Boolean.class, Character.class, Byte.class, String.class };

 

    private String[] value;

 

    private Class<?> classType;

 

    @Override

    public void initialize(NotRepeatable constraintAnnotation) {

        this.value = constraintAnnotation.value();

    }

 

    @Override

    public boolean isValid(List<?> values, ConstraintValidatorContext context) {

        if (null == values || values.size() == 0) {

            return true;

        }

        this.classType = values.get(0).getClass();

 

        Set<String> set = new HashSet<>();

        if (this.isBasicDataType() || (null == this.value || this.value.length == 0)) {

            for (int i = 0; i < values.size(); i++) {

                set.add(values.get(i).toString());

            }

            if (set.size() == values.size()) {

                return true;

            }

        else {

            if (null == this.value || this.value.length == 0) {

                throw new RuntimeException("集合非重复元素验证传入参数有误");

            }

            for (int i = 0; i < values.size(); i++) {

                Object object = values.get(i);

                List<String> elementList = new ArrayList<>();

                for (String string : this.value) {

                    try {

                        Method method = this.classType.getMethod("get" + toUpcase(string));

                        elementList.add(method.invoke(object).toString());

                    catch (Exception e) {

                        log.error("集合非重复元素验证有误", e);

                        throw new RuntimeException("集合非重复元素验证有误");

                    }

                }

                set.add(StringUtils.join(elementList, "-"));

            }

            if (set.size() == values.size()) {

                return true;

            }

        }

 

        return false;

    }

 

    /**

     * <p>

     * Description: 判断是否为基础数据类型

     * </p>

     *

     * @author zhanghua

     * @return

     */

    private boolean isBasicDataType() {

        for (Class<?> class1 : BASIC_DATA_TYPE) {

            if (class1 == this.classType) {

                return true;

            }

        }

 

        return false;

    }

 

    /**

     * <p>

     * Description: 转换第一个字母为大写

     * </p>

     *

     * @author   zhanghua

     * @param  s

     * @return

     */

    private static String toUpcase(String s) {

        return s.substring(01).toUpperCase() + s.substring(1, s.length());

    }

 

}

 

使用方法如下:

long create(@NotRepeatable({ "id""name" }) List<SupplierBizContact> supplierBizContacts, long userId){

}

@NotRepeatable 注解的value为选填,针对复杂对象,填写了若干属性,会针对填写的属性进行重复验证;如果不填写,则调用对象的toString方法进行重复验证。

如上代码:会验证id和name,如果集合中不同的元素存在id和name都一样的情况,则任务元素重复;如果不填写value,则会调用SupplierBizContact.toString()方法验重。

long create(@NotRepeatable List<Long> userIdList){

}

如果是基础数据类型的List集合,不需要设置value,设置value也会忽略。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值