MVC架构模式下的web系统数据验证方案

概述

惯例交待下背景,为保证系统的稳定可靠运行,必须对输入的数据进行严格验证,防止一些非法的异常数据引发系统后续处理流程出错甚至崩溃。同时,对于验证失败的情况,需要输出明确的、友好的错误信息,降低系统的运维工作量。

今天要说的数据验证,特指使用MVC架构的web系统,技术栈是SSM,即对于从前端传到控制器层(SpringMVC)的请求参数或对象,进行的数据校验工作。前端的数据校验,由前端技术方案解决,不在讨论范围内。后端带业务逻辑的数据验证,如账号是否已存在,是服务层service该干的活,同样不在讨论范围内。

很明显,这是一个常见常用的典型场景,就不要重复造轮子了,应该考虑优先选择成熟稳定的功能组件。

数据校验是有规范的,JSR303是标准。JSR-349是其升级版本,添加了一些新特性,规定一些校验注解,位于javax.validation.constraints包下。注意,规范仅仅是规范,实现还得具体的功能组件来。

目前主流的功能验证组件是hibernate validator,需要注意的是,别一提hibernate就想到ORM框架,这里只是一个数据验证的功能组件。

这个功能组件可以直接使用,不过,贴心的Spring,在其基础上进行了二次开发与封装,称之为spring validation,整合进了整个Spring体系。

弄清楚JSR303/JSR-349,hibernate validation,spring validation之间的关系,那么,很明显,我们应该选择spring validation。

常用注解

以下注解均来自于javax.validation.constraints包。

非空验证

这是最常见的需求,即验证某个属性不能为空,如用户账号、部门名称等,对应的注解有三个,@NotNull 、@NotEmpty和@NotBlank。
这三个略有差异,@NotNull是验证不能为null,适用于任何类型;@NotEmpty 进了一步,不仅不能为null,也不能为空串,既适用于字符串,也适用于集合类对象;@NotBlank更进一步,不能为非空白字符(空白字符包括空格、回车、换行、tab等),只适用于字符串。

与之对应的有三个功能相反的注解,@Null 、@Empty和 @Blank,从数据验证的角度,适用的地方会极其苛刻,只能用在某些特定的场景了,实用性相当低。

范围类验证

@Min和@Max:验证最小值和最大值,含边界,适用于整数类型,如byte、short、int、long及其包装类型,以及BigDecimal和BigInteger,不适用于double和float。

@DecimalMin和@DecimalMax:名字上看上去专用于Decimal数据类型,实际跟上面@Min和@Max的适用范围完全一致。那既然有了@Max,为何还需要@DecimalMax?原因在于,如果某个数字特别大,超出了Long.maxValue(@Max注解value属性的类型是long),这时候,只能使用@DecimalMax。

@Size(min,max) 用于字符串和集合,验证长度或元素数在最小值与最大值范围之间。

@Digits (integer, fraction),两个参数代表整数位数和小数位数最大值,适用于整数类型,如byte、short、int、long及其包装类型,以及BigDecimal和BigInteger,此外也适用于字符串。这里适用于字符串,是要求这个字符串必须能转换为数值,否则验证肯定通不过。

日期类验证

@Past 适用于日期时间类型,要求必须是过去的时间
@Future 适用于日期时间类型,要求必须是将来的时间
与之相关的还有两个,包含当前日期,@PastOrPresent和@FutureOrPresent
某些场景还是能用上。

特定格式验证
@Email 验证电子邮件

正则表达式验证

@Pattern 自己来写正则,实用性可以。

其他验证

还有一些,实用性比较差,在此简单列一下,知道有即可,用的可能性不大。
@Negative 负数
@Positive 正数
@AssertTrue 必须为true,专用于布尔类型,包括boolean和Boolean
@AssertFalse 必须为false,专用于布尔类型,包括boolean和Boolean

如何使用

以下内容从实战角度说明如何来使用,需要注意的问题及坑点,不会面面俱到。

引入依赖

需要注意的是,当前网传的一些文章,说sping-boot-starer-web已经内置引用了Hibernate validator组件,实际是不准确的,在早期的SpringBoot版本,确实是引入了,但较新的SpringBoot版本,默认情况下是未引入的,例如我使用的2.3.0,仍需要引入以下包:

  <!--数据验证--> 
  <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.0.18.Final</version>
  </dependency>

如果不引入上面这个包,首先校验的注解无法识别,还需要引入下面的包。

 <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>2.0.1.Final</version>
  </dependency>

但如果只引入了validation-api包,不引入hibernate-validator包,项目编译与运行并不会报错,但是数据验证并不会触发与生效。

附加注解

为要进行数据验证的对象的属性添加注解,在我的系统设计中,使用VO(视图对象)作为前后端交互的对象,因此,注解添加在VO对象上,如下图使用的@NotBlank,其中message参数用于指定数据验证失败后的提示信息,下文会用到。

	@Data
    @EqualsAndHashCode(callSuper = true)
    @Accessors(chain = true)
    @ApiModel(value = "字典项对象")
    public class DictionaryItemVO extends BaseVO {

        private static final long serialVersionUID = 1L;

        @ApiModelProperty(value = "字典类型标识")
        private String typeId;


        @NotBlank(message = "名称不能为空")
        @ApiModelProperty(value = "名称")
        private String name;

        @ApiModelProperty(value = "编码")
        @NotBlank(message = "编码不能为空")
        private String code;

        @ApiModelProperty(value = "状态")
        private String status;

        @ApiModelProperty(value = "排序号")
        private String orderNo;


    }
进行验证

对前后端交互,使用SpringMVC的情况下,后端接收参数的时候,需要附加@Validated注解来修饰指定参数。

    /**
     * 新增
     */
    @ApiOperation(value = "新增")
    @PostMapping("/")
    @SystemLog(value = "组织机构-新增")
    @PreAuthorize("hasPermission(null,'system:organization:add')")
    public ResponseEntity<Result> add(@Validated @RequestBody OrganizationVO vo) {
        Organization entity = convert2Entity(vo);
        organizationService.add(entity);
        OrganizationVO newVO = convert2VO(entity);
        return ResultUtil.success(newVO);
    }

如上图,对于组织机构新增的方法,增加了注解@Validated,这点很关键,不加该注解,则不会进行实际的数据验证。
加了该注解后,会自动进行数据验证,验证失败抛出MethodArgumentNotValidException的异常,这时候,我们就可以结合全局异常处理,在附加了@RestControllerAdvice的全局类中捕获异常,拿到错误提示信息,并给予友好提示了。

/**
 * 全局异常处理类
 * @author wqliu
 */
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {


    //常见Http异常
    /**
     * 参数异常 400
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public  ResponseEntity<Result> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String errorMessage = "";
        //bean validator验证部分
        BindingResult bindingResult = e.getBindingResult();
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        if (allErrors.size() > 0) {
            FieldError fieldError = (FieldError) allErrors.get(0);
            errorMessage = fieldError.getDefaultMessage();
        }
        return logAndGenerateResult(e,HttpStatus.BAD_REQUEST,errorMessage);
    }


    /**
     * 未授权,401
     */
    @ExceptionHandler(SessionExpiredException.class)
    public  ResponseEntity<Result> handleSessionExpiredException(SessionExpiredException e) {

        //会话超时为正常现象,虽然使用异常来处理,但不调用log.error
        return ResultUtil.error(e.getMessage(),HttpStatus.UNAUTHORIZED);

    }
	……
}

进阶使用

分组验证

对于同一业务实体,我们可能需要在不同场景下进行不同的数据验证工作,例如,通过组织机构数据维护,表单录入需要验证机构类型,而通过excel批量导入,只需要验证下机构名称不为空即可,机构类型在导入模板中干脆就不提供,让系统管理员导入后在系统中手工调整,这种情况下就用到分组验证。
Spring Validation提供了相应的分组验证方面的支持。

@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "Organization对象", description = "组织机构")
public class OrganizationVO extends BaseVO {

    private static final long serialVersionUID = 1L;

    /**
     * 表单录入
     */
    public interface FormInput{

    }

    /**
     * excel导入
     */
    public interface ExcelImport{

    }


    @ApiModelProperty(value = "父标识")
    private String parentId;


    @ApiModelProperty(value = "名称")
    @NotBlank(message = "名称不能为空",groups = {tech.popsoft.platform.system.vo.OrganizationVO.FormInput.class, tech.popsoft.platform.system.vo.OrganizationVO.ExcelImport.class})
    @ExcelProperty("名称")
    private String name;

    @ApiModelProperty(value = "类型")
    @NotBlank(message = "请选择类型",groups = {tech.popsoft.platform.system.vo.OrganizationVO.FormInput.class})
    @ExcelIgnore
    private String type;

	……
}

如上所示,在实体对象内部,以接口的方式,定义分组,然后在验证属性的注解上,如@NotBlank,附加groups属性,指定在分组,可指定多个。

需要注意的是,在controller接收数据的时候,需要在@Validated中指定分组名,如下所示。

public ResponseEntity<Result> add(@Validated(OrganizationVO.FormInput.class) @RequestBody OrganizationVO vo) {
        Organization entity = convert2Entity(vo);
        organizationService.add(entity);
        OrganizationVO newVO = convert2VO(entity);
        return ResultUtil.success(newVO);
    }

服务层使用

上面说的都是与SpringMVC做了整合的情况下,如果服务层,也想使用数据验证框架,如对Controller层传过来的数据,或Service层之间的调用数据,使用dto对象,同样可以使用。给要验证的对象添加注解跟前面是完全一样的,就是进行验证,需要调用validator对象的方法,进行手工触发。

 private void validate(ApiRequest apiRequest) {
        Validator validator= Validation.byProvider(HibernateValidator.class).configure()
                .failFast(true).buildValidatorFactory().getValidator();

        Set<ConstraintViolation<ApiRequest>> set = validator.validate(apiRequest);
        if(set.size()>0){
            throw new ApiException("S00", set.iterator().next().getMessage());
        }
    }

以上实际是直接使用HibernateValidator的方式,理论上,使用Spring validation也是可行的,在服务层中通过@Autowired直接注入private Validator validator,然后调用validator.validate方法即可,但这个方法返回是void,通过第二个参数Errors来接收错误,怎么构造出来这个参数,尚未找到资料,留个悬念,日后解决了补充进来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学海无涯,行者无疆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值