首页 > 技术文章 > 对象数据校验

yuanchuziwen 2022-01-21 14:26 原文

对象数据校验

当进行对象修改、对象保存等操作时,前端往往返回一个 JSON对象或者是 表单对象,通过 SpringMVC后一般都会封装为一个 Java对象;

我们针对这个 Java对象进行操作前,通常都要进行校验,可以使用 JSR303中定义的校验注解来简化

一、原始的写法

在每次请求中都写上校验,或者把校验抽取成一个方法,每次请求都执行;

/**
     * 添加重审专家
     *
     * @param sid
     * @param expId
     * @return
     */
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
@ResponseBody
@PostMapping("/arrange/review")
public Object arrangeReviewExp(Integer sid, Integer expId) {

    Sub sub = subService.getSubById(sid);
    if (sub == null) {
        return ResponseData.fail(WebConstant.RESPONSE_FAIL_SERVER_ERROR_500, SubConstant.SUB_MISS);
    }

    if (!(sub.getStatus() == 6 || sub.getStatus() == 7)) {
        return ResponseData.fail(WebConstant.RESPONSE_FAIL_SERVER_ERROR_500, SubConstant.SUB_NOT_AT_REARRANGEABLE);
    }

    User user = userService.getExpById(expId);
    if (user == null) {
        return ResponseData.fail(WebConstant.RESPONSE_FAIL_SERVER_ERROR_500, "该专家不存在");

    }

    if (!userService.checkExpAbility(user)) {
        return ResponseData
            .fail(WebConstant.RESPONSE_FAIL_SERVER_ERROR_500, "重审专家 " + user.getUsername() + " 没有审批资格");
    }

    if (subExpService.checkReviewSubExpExistence(sid)) {
        return ResponseData.fail(WebConstant.RESPONSE_FAIL_SERVER_ERROR_500, "重审专家已被分配,请勿重复提交");
    }

    // 真正的业务逻辑,之后才开始
}

二、简单校验

JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。

2.1、依赖

<!--jsr 303-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.2、常用注解

声明校验:

  • @Valid:常见用在方法,类中字段上进行校验,算是一种规范
  • @Validated:是spring提供的对@Valid的封装,常见用在方法上进行校验,算是对 @Valid的一种实现,更牛逼

其他常见注解如下:

  • @Null 限制只能为null
  • @AssertFalse 限制必须为false
  • @AssertTrue 限制必须为true
  • @DecimalMax(value) 限制必须为一个不大于指定值的数字
  • @DecimalMin(value) 限制必须为一个不小于指定值的数字
  • @Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
  • @Future 限制必须是一个将来的日期
  • @Max(value) 限制必须为一个不大于指定值的数字
  • @Min(value) 限制必须为一个不小于指定值的数字
  • @Past 限制必须是一个过去的日期
  • @Pattern(value) 限制必须符合指定的正则表达式,如 ^[a-zA-Z]$
  • @Size(max,min) 限制字符长度必须在min到max之间
  • @Past 验证注解的元素值(日期类型)比当前时间早
  • @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
  • @Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=, max=) 被注释的元素必须在合适的范围内
  • @URL(protocol=,host=, port=,regexp=, flags=) 被注释的字符串必须是一个有效的url
  • @NotNull 任何对象的 value不能为null
  • @NotEmpty 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
  • @NotBlank 只能用于字符串不为null,并且字符串trim()以后length要大于0

2.3、实体类

package com.zwb.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 品牌
 *
 * @author OliQ
 * @email yuanchuziwen@qq.com
 * @date 2022-01-16 13:49:02
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 品牌id
     */
    @TableId
    private Long brandId;

    /**
     * 品牌名
     *
     * @NotBlank 定义了使用 Validation包的注解校验方法
     * 会在使用 @Valid注解标注的 Controller方法中进行校验,
     * 可使用 message属性进行自定义反馈信息
     */
    @NotBlank(message = "品牌名不能为空")
    private String name;

    /**
     * 品牌logo地址
     */
    @NotEmpty
    @URL(message = "logo必须是一个合法的 URL地址")
    private String logo;

    /**
     * 介绍
     */
    private String descript;

    /**
     * 显示状态[0-不显示;1-显示]
     */
    private Integer showStatus;

    /**
     * 检索首字母
     */
    @NotEmpty
    @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
    private String firstLetter;

    /**
     * 排序
     */
    @NotNull
    @Min(value = 0, message = "排序必须是一个大于 0的整数")
    private Integer sort;

}

2.4、Controller

需要在被校验的对象前面加 @Valid注解,它后面可以紧随着 BindingResult对象,表示对该对象校验后的结果

/**
 * 保存
 */
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
    if (result.hasErrors()) {
        Map<String, String> errors = new HashMap<>(result.getFieldErrors().size());
        result.getFieldErrors().forEach(item -> {
            // 获取错误提示
            String message = item.getDefaultMessage();
            // 获取错误的属性字段
            String field = item.getField();
            errors.put(field, message);
        });

        return R.error().put("data", errors);
    }

    brandService.save(brand);
    return R.ok();
}

2.5、异常统一处理

如果有多个方法进行 Validation校验,那么就得写很多遍代码,可以统一抽取出来;

package com.zwb.gulimall.product.exception;


import com.zwb.common.exception.BizCodeEnum;
import com.zwb.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * @author:OliQ
 * @date: Created on 2022-01-20 21:00
 * @description:
 */
@Slf4j
@RestControllerAdvice(basePackages = {"com.zwb.gulimall.product.controller"}) // 可以设置处理的异常来源
public class GulimallExceptionHandler {

    @ExceptionHandler(value = MethodArgumentNotValidException.class) // 异常匹配是 小异常优先的
    public R handleValidException(MethodArgumentNotValidException e) {
        log.error("数据校验出现问题 {}, 异常类型 {}", e.getMessage(), e.getClass());

        BindingResult result = e.getBindingResult();
        Map<String, String> errors = new HashMap<>(result.getFieldErrors().size());
        result.getFieldErrors().forEach(fieldError -> {
            errors.put(fieldError.getField(), fieldError.getDefaultMessage());
        });

        return R.error(BizCodeEnum.VALID_EXCEPTION).put("data", errors);
    }
}

三、分组校验

同一个实体类,其中的诸多属性在不同的请求中,可能需要不同的校验方式

因此,可以构造分组校验,以满足上述需求

3.1、分组声明

用一个空接口即可

package com.zwb.common.valid;

/**
 * @author :OliQ
 * @date :Created on 2022-01-20 22:30
 */
public interface AddGroup {
}

package com.zwb.common.valid;

/**
 * @author :OliQ
 * @date :Created on 2022-01-20 22:30
 */
public interface UpdateGroup {
}

3.2、实体类

在每一个约束注解中添加 groups属性,表示该注解在哪一种情况下生效;

如果 groups属性没有声明,就表示在 Default.class分组下

package com.zwb.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zwb.common.valid.AddGroup;
import com.zwb.common.valid.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 品牌
 *
 * @author OliQ
 * @email yuanchuziwen@qq.com
 * @date 2022-01-16 13:49:02
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 品牌id
     */
    @TableId
    @NotNull(message = "修改必须指定 ID", groups = {UpdateGroup.class})
    @Null(message = "新增不能指定 ID", groups = {AddGroup.class})
    private Long brandId;

    /**
     * 品牌名
     *
     * @NotBlank 定义了使用 Validation包的注解校验方法
     * 会在使用 @Valid注解标注的 Controller方法中进行校验,
     * 可使用 message属性进行自定义反馈信息
     */
    @NotBlank(message = "品牌名不能为空", groups = {AddGroup.class})
    private String name;

    /**
     * 品牌logo地址
     */
    @NotEmpty
    @URL(message = "logo必须是一个合法的 URL地址")
    private String logo;

    /**
     * 介绍
     */
    private String descript;

    /**
     * 显示状态[0-不显示;1-显示]
     */
    private Integer showStatus;

    /**
     * 检索首字母
     */
    @NotEmpty
    @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
    private String firstLetter;

    /**
     * 排序
     */
    @NotNull
    @Min(value = 0, message = "排序必须是一个大于 0的整数")
    private Integer sort;

}

3.3、Controller类

需要使用 @Validated注解,代替 @Valid注解

并且声明 value属性,指明当前校验是在哪些分组情况下进行的

注意:如果在 value属性中声明了具体的接口,那么 default.class接口,也必须显示声明,就像类的空参和有参构造一样,如果不声明,那么处在 default.class组中的约束(就是没刻意分组的那些约束)都不会再生效

package com.zwb.gulimall.product.controller;import com.zwb.common.utils.PageUtils;import com.zwb.common.utils.R;import com.zwb.common.valid.AddGroup;import com.zwb.common.valid.UpdateGroup;import com.zwb.gulimall.product.entity.BrandEntity;import com.zwb.gulimall.product.service.BrandService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;import javax.validation.Validator;import javax.validation.groups.Default;import java.util.Arrays;import java.util.HashMap;import java.util.Map;/** * 品牌 * * @author OliQ * @email yuanchuziwen@qq.com * @date 2022-01-16 13:49:02 */@RestController@RequestMapping("product/brand")public class BrandController {    @Autowired    private BrandService brandService;    /**     * 列表     */    @RequestMapping("/list")    // @RequiresPermissions("product:brand:list")    public R list(@RequestParam Map<String, Object> params) {        PageUtils page = brandService.queryPage(params);        return R.ok().put("page", page);    }    /**     * 信息     */    @RequestMapping("/info/{brandId}")    // @RequiresPermissions("product:brand:info")    public R info(@PathVariable("brandId") Long brandId) {        BrandEntity brand = brandService.getById(brandId);        return R.ok().put("brand", brand);    }    /**     * 保存     */    @RequestMapping("/save")    // @RequiresPermissions("product:brand:save")    public R save(@Validated({AddGroup.class, Default.class}) @RequestBody BrandEntity brand) {        brandService.save(brand);        return R.ok();    }    /**     * 修改     */    @RequestMapping("/update")    // @RequiresPermissions("product:brand:update")    public R update(@Validated({UpdateGroup.class, Default.class}) @RequestBody BrandEntity brand) {        brandService.updateById(brand);        return R.ok();    }    /**     * 删除     */    @RequestMapping("/delete")    // @RequiresPermissions("product:brand:delete")    public R delete(@RequestBody Long[] brandIds) {        brandService.removeByIds(Arrays.asList(brandIds));        return R.ok();    }}

四、自定义校验

4.1、编写自定义校验注解

仿造其他的那些约束注解,自己写一个

必须有 groups, payload, message属性

message表示默认提示从哪里获取,需要自己在 resource路径下,新建 ValidationMessages.properties文件

package com.zwb.common.valid;import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.Documented;import java.lang.annotation.Retention;import java.lang.annotation.Target;import static java.lang.annotation.ElementType.*;import static java.lang.annotation.RetentionPolicy.RUNTIME;/** * @author :OliQ * @date :Created on 2022-01-20 22:57 */@Documented@Constraint(validatedBy = {ListValueConstraintValidator.class})@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})@Retention(RUNTIME)public @interface ListValue {    String message() default "{com.zwb.common.ListValue.message}";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};    int[] vals() default {};}

ValidationMessages.properties

注意中文乱码问题,修改后如果还有问题,就把这个配置文件删掉重写一遍

com.zwb.common.ListValue.message=必须提交指定的值

实体类中的使用

package com.zwb.gulimall.product.entity;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import com.zwb.common.valid.AddGroup;import com.zwb.common.valid.ListValue;import com.zwb.common.valid.UpdateGroup;import lombok.Data;import org.hibernate.validator.constraints.URL;import javax.validation.constraints.*;import java.io.Serializable;/** * 品牌 * * @author OliQ * @email yuanchuziwen@qq.com * @date 2022-01-16 13:49:02 */@Data@TableName("pms_brand")public class BrandEntity implements Serializable {    /**     * 显示状态[0-不显示;1-显示]     */    @ListValue(vals={0, 1}, message = "必须提交指定的值")    private Integer showStatus;}

4.2、编写自定义的校验器

自己写一个类,实现 ConstraintValidator<Anno, Type>接口

  • Anno: 为注解名称
  • Type:为数据的类型
package com.zwb.common.valid;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import java.util.HashSet;import java.util.Set;/** * @author:OliQ * @date: Created on 2022-01-21 12:44 * @description: */public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {    private Set<Integer> set = new HashSet<>();    /**     * 初始化方法     *     * @param constraintAnnotation     */    @Override    public void initialize(ListValue constraintAnnotation) {        int[] vals = constraintAnnotation.vals();        for (int val : vals) {            set.add(val);        }    }    /**     * 判断校验规则     *     * @param value 被校验的值     * @param context     * @return     */    @Override    public boolean isValid(Integer value, ConstraintValidatorContext context) {        return set.contains(value);    }}

4.3、将二者关联

在自定义注解上加上元注解 @Constrains(validatedBy={xxx.class})

package com.zwb.common.valid;@Documented@Constraint(validatedBy = {ListValueConstraintValidator.class})@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})@Retention(RUNTIME)public @interface ListValue {    String message() default "{com.zwb.common.ListValue.message}";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};    int[] vals() default {};}

五、其他

5.1、异常状态的声明

针对多种可能的异常,使用枚举会比常量类更好

package com.zwb.common.exception;

/**
 * @author:OliQ
 * @date: Created on 2022-01-20 21:42
 * @description:
 */
public enum BizCodeEnum {
    VALID_EXCEPTION(10001,"参数格式校验失败"),

    UNKNOWN_EXCEPTION(10000,"系统未知异常");

    private Integer code;
    private String msg;

    BizCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

5.2、用于返回的结果对象

/**
 * Copyright (c) 2016-2019 人人开源 All rights reserved.
 * <p>
 * https://www.renren.io
 * <p>
 * 版权所有,侵权必究!
 */

package com.zwb.common.utils;

import com.zwb.common.exception.BizCodeEnum;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
 * 返回数据
 *
 * @author Mark sunlightcs@gmail.com
 */
public class R extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    public R() {
        put("code", 0);
        put("msg", "success");
    }

    public static R error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
    }

    /*public static R error(BizCodeEnum bizCodeEnum) {
        return error(bizCodeEnum.getCode(), bizCodeEnum.getMessage());
    }*/

    public static R error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static R error(BizCodeEnum bizCodeEnum) {
        return error(bizCodeEnum.getCode(), bizCodeEnum.getMsg());
    }

    public static R error(int code, String msg) {
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put("msg", msg);
        return r;
    }

    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R ok() {
        return new R();
    }

    @Override
    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}

推荐阅读