欲加入创客群请加微信:cool-smiler ,备注:入群

JavaWeb校验框架Hibernate Validator最佳实践

今日限免 JackLeon 3个月前 (05-22) 119次浏览 0个评论 扫描二维码

JavaWeb 校验框架 Hibernate Validator 实践

数据校验是任何一个应用程序都会用到的功能,无论是显示层还是持久层. 通常,相同的校验逻辑会分散在各个层中, 这样,不仅浪费了时间还会导致错误的发生(译注: 重复代码). 为了避免重复, 开发人员经常会把这些校验逻辑直接写在领域模型里面, 但是这样又把领域模型代码和校验代码混杂在了一起, 而这些校验逻辑更应该是描述领域模型的元数据.


JSR 303 – Bean Validation – 为实体验证定义了元数据模型和 API. 默认的元数据模型是通过 Annotations 来描述的,但是也可以使用 XML 来重载或者扩展. Bean Validation API 并不局限于应用程序的某一层或者哪种编程模型, 例如,如图所示, Bean Validation 可以被用在任何一层, 或者是像类似 Swing 的富客户端程序中.而 hibernate validator 则实现了这样一套规范。

JavaWeb校验框架Hibernate Validator最佳实践

1.编写全局工具类:

package com.souche.sc.logic;

import com.alibaba.fastjson.JSON;
import com.souche.optimus.exception.system.OptimusParamValidationException;
import org.hibernate.validator.HibernateValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.*;
import java.util.Set;

/**
 * @author xiewenlin@frtauto.com
 * @ClassName GlobalExceptionHandler
 * @Description
 * @Date 2019/5/21 11:14
 * @Version V1.0.0
 */
public class GlobalValidation {
    private static Logger logger = LoggerFactory.getLogger(GlobalValidation.class);

    /**
     * 校验实体类
     * @param entity 实体类实例
     * @param methodName 方法名称,用于定位排查问题
     * @param groups 用于满足指定部分校验分组
     */
    public static void validate(Object entity,String methodName,Class... groups) {
        StringBuffer errorInfo=new StringBuffer();
        if(null==entity){
            errorInfo.append("方法名:"+methodName+"入参对象不能为空");
            throw new OptimusParamValidationException(errorInfo.toString());
        }
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .failFast( true )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        Set<ConstraintViolation<Object>> constraintViolations=null;
        if(null==groups){
            constraintViolations=validator.validate(entity);
        }else{
            constraintViolations=validator.validate(entity,groups);
        }

        if(constraintViolations.size()>0){
            for (ConstraintViolation<Object> constraintViolation : constraintViolations){
                errorInfo.append(constraintViolation.getMessageTemplate());
            }
            logger.error("方法名:"+methodName+"入参参数验证失败:{}",JSON.toJSONString(entity));
            throw new OptimusParamValidationException(errorInfo.toString());
        }
    }
}

2.通用配置方法

步骤 1:通过注解配置需要校验的实体

package com.souche.sc.model.dto.purOrder;

import com.wordnik.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NonNull;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

/**
 * @Auther: yangqinglong
 * @Date: 2018/11/9 14:52
 * @Description: 列表查询条件
 */
@Data
public class PurOrderConditionDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "当前页",required = true)
    @Min(value = 1,message="当前页必须大于等于 1")
    private Integer index;

    @ApiModelProperty(value = "每页显示的条数",required = true)
    @Min(value = 0,message="每页显示的条数必须大于等于 0")
    private Integer pageSize;

    @ApiModelProperty(value = "业务单号")
    @NotBlank(message="业务单号不能为空")
    private String bizOrderNo;

    @ApiModelProperty(value = "供应商")
    private String supplierId;

    @ApiModelProperty(value = "单据类型编码")
    private String orderTypeCode;

    @ApiModelProperty(value = "未开完发票")
    private Boolean notTicket = false;
}

步骤 2:使用对比

优化前:

@Rest(value = "pagePurOrderDet", method = OptimusRequestMethod.GET,desc = "查询采购明细单获取业务数据")
    @JsonEx(serializerFeature ={SerializerFeature.WriteNullStringAsEmpty})
    public Result<Page<PurOrderRespVO>> pagePurOrderDet(@ModelAttribute @Params PurOrderConditionDTO purOrderConditionDTO){

        Page<PurOrderRespVO> pagePurOrder;
        try {
            if (null == purOrderConditionDTO.getIndex()) {
                throw new OptimusParamValidationException("当前页不得为空");
            }
            if (null == purOrderConditionDTO.getPageSize()) {
                throw new OptimusParamValidationException("每页条数不得为空");
            }
            pagePurOrder = purOrderService.pagePurOrderDet(purOrderConditionDTO);
        } catch (OptimusExceptionBase e) {
            return Result.fail(e.getErrCode(), e.getMessage());
        } catch (Exception e) {
            logger.info("查询采购明细单获取业务数据失败:{}", e.getMessage(), e);
            return Result.fail(ErrorCodes.UNKNOW_EXCEPTION.getErrorCode(), "查询采购明细单获取业务数据失败");
        }
        return Result.success(pagePurOrder);

    }

优化后:

    @Autowired
    private LoginHandleService loginHandleService;
    @Rest(value = "pagePurOrderDetX", method = OptimusRequestMethod.GET,desc = "查询采购明细单获取业务数据")
    @JsonEx(serializerFeature ={SerializerFeature.WriteNullStringAsEmpty})
    public Result<Page<PurOrderRespVO>> pagePurOrderDetX(@ModelAttribute @Params PurOrderConditionDTO purOrderConditionDTO){
        Page<PurOrderRespVO> pagePurOrder;
        GlobalValidation.validate(purOrderConditionDTO,"pagePurOrderDetX", null);
        try {
            pagePurOrder = purOrderService.pagePurOrderDet(purOrderConditionDTO);
        } catch (OptimusExceptionBase e) {
            return Result.fail(ResultEnum.SYSTEM_ERROR.getRtCode(), "查询采购明细单获取业务数据失败"+ResultEnum.SYSTEM_ERROR.getMsg());
        } catch (Exception e) {
            logger.error("查询采购明细单获取业务数据失败:{}", e.getMessage()+"查询采购明细单获取业务数据-入参:"+ JSON.toJSONString(purOrderConditionDTO), e);
            return Result.fail(ResultEnum.SYSTEM_ERROR.getRtCode(), "查询采购明细单获取业务数据失败"+ResultEnum.SYSTEM_ERROR.getMsg());
        }
        return Result.success(pagePurOrder);
    }

3.约束分组(GROUP)

约束分组用来实现部分校验的功能,例如我们在PurOrderConditionDTO的 fields 上添加了较多约束,但是在有些场景中我们只需要验证分页相关的属性,虽然这种场景的使用应不多,但我们如何实现这种功能呢?


每一个约束中都包含一个 groups 的属性,返回 class 数组,Validator 的 validate 方法也提供一个输入 groups 的参数,我想大家都明白 groups 是怎么用的了。示例如下:

目标:只校验PurOrderConditionDTO 里的index 和pageSize 属性

步骤 1:首先定义一个 groups 接口

package com.souche.sc.logic;

/**
 * @author xiewenlin@frtauto.com
 * @ClassName OnlyPageInfoCheck
 * @Description
 * @Date 2019/5/21 17:37
 * @Version V1.0.0
 */
public interface OnlyPageInfoCheck {
}

步骤 2:在 PurOrderConditionDTO 中添加 groups 属性

package com.souche.sc.model.dto.purOrder;

import com.souche.sc.logic.OnlyPageInfoCheck;
import com.wordnik.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
 * @Auther: yangqinglong
 * @Date: 2018/11/9 14:52
 * @Description: 列表查询条件
 */
@Data
public class PurOrderConditionDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "当前页",required = true)
    @Min(value = 1,message="当前页必须大于等于 1",groups = OnlyPageInfoCheck.class)
    private Integer index;

    @ApiModelProperty(value = "每页显示的条数",required = true)
    @Min(value = 0,message="每页显示的条数必须大于等于 0",groups = OnlyPageInfoCheck.class)
    private Integer pageSize;

    @NotBlank(message = "业务单号不能为空")
    @ApiModelProperty(value = "业务单号")
    private String bizOrderNo;

    @ApiModelProperty(value = "供应商")
    private String supplierId;

    @ApiModelProperty(value = "单据类型编码")
    private String orderTypeCode;

    @ApiModelProperty(value = "未开完发票")
    private Boolean notTicket = false;
}

步骤 3:使用

@Rest(value = "pagePurOrderDetX", method = OptimusRequestMethod.GET,desc = "查询采购明细单获取业务数据")
    @JsonEx(serializerFeature ={SerializerFeature.WriteNullStringAsEmpty})
    public Result<Page<PurOrderRespVO>> pagePurOrderDetX(@ModelAttribute @Params PurOrderConditionDTO purOrderConditionDTO){
        Page<PurOrderRespVO> pagePurOrder;
        GlobalValidation.validate(purOrderConditionDTO,"pagePurOrderDetX", OnlyPageInfoCheck.class);
        try {
            pagePurOrder = purOrderService.pagePurOrderDet(purOrderConditionDTO);
        } catch (OptimusExceptionBase e) {
            return Result.fail(ResultEnum.SYSTEM_ERROR.getRtCode(), "查询采购明细单获取业务数据失败"+ResultEnum.SYSTEM_ERROR.getMsg());
        } catch (Exception e) {
            logger.error("查询采购明细单获取业务数据失败:{}", e.getMessage()+"查询采购明细单获取业务数据-入参:"+ JSON.toJSONString(purOrderConditionDTO), e);
            return Result.fail(ResultEnum.SYSTEM_ERROR.getRtCode(), "查询采购明细单获取业务数据失败"+ResultEnum.SYSTEM_ERROR.getMsg());
        }
        return Result.success(pagePurOrder);

    }

注意默认通用配置方法(groups 传 null 相当于Default.class)只适用于校验不带 groups 的属性。如果既需要校验默认组又需要校验指定组,则传递多个组类即可,比如:

        GlobalValidation.validate(purOrderConditionDTO,"pagePurOrderDetX", Default.class,OnlyPageInfoCheck.class);

4.使用场景实战

优化前的代码如下:

@Rest(value = "insertPurOrderDet", method = OptimusRequestMethod.POST,desc = "添加采购单明细数据")
    public Result<String> insertPurOrderDet(@Json("@requestBody") PurOrderDetDTO purOrderDetDTO){
        if(StringUtil.isEmpty(purOrderDetDTO.getPurOrderMstrId())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请采购单主单 ID");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getGoodsId())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品 ID");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getGoodsName())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品名称");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL1CategoryId())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品一级类别 ID");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL1CategoryCode())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品一级类别编码");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL1CategoryName())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品一级类别名称");
        }

        // 整车 四级必填
        if(InvoiceEnum.CAR.getCode().equals(purOrderDetDTO.getGoodsL1CategoryCode())){
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL2CategoryId())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品二级类别 ID");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL2CategoryCode())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品二级类别编码");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL2CategoryName())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品二级类别名称");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL3CategoryId())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品三级类别 ID");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL3CategoryCode())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品三级类别编码");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL3CategoryName())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品三级类别名称");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL4CategoryId())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品四级类别 ID");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL4CategoryCode())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品四级类别编码");
            }
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsL4CategoryName())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品四级类别名称");
            }
        }else{
            if(StringUtil.isEmpty(purOrderDetDTO.getGoodsNo())){
                return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写商品编码");
            }
        }
        if(purOrderDetDTO.getPurPrice() == null){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写采购单价");
        }
        if(purOrderDetDTO.getPurQty() == null){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写采购数量");
        }
        if(purOrderDetDTO.getPurAmount() == null){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写采购金额");
        }
        if(StringUtil.isEmpty(purOrderDetDTO.getBizPurOrderItemId())){
            return Result.fail(ResultEnum.ERR_PARAM.getRtCode(),"请填写业务采购单明细 ID");
        }
        try{
//            purOrderService.insertPurOrderDet(purOrderDetDTO);
            List<com.souche.sc.hessian.model.dto.PurOrderDetDTO> purOrderDetDTOList = new ArrayList<>();
            com.souche.sc.hessian.model.dto.PurOrderDetDTO purOrderDetDTO1 = new com.souche.sc.hessian.model.dto.PurOrderDetDTO();
            BeanUtils.copyProperties(purOrderDetDTO,purOrderDetDTO1);
            purOrderDetDTOList.add(purOrderDetDTO1);
            purOrderApiService.insertBatchPurOrderDet(purOrderDetDTOList);
        }catch (Exception e){
            logger.error("新增采购单明细数据失败:{}",e.getMessage(),e);
            return Result.fail(ErrorCodes.UNKNOW_EXCEPTION.getErrorCode(), "新增采购单明细数据失败");
        }
        return Result.success("Ok");
    }

优化后的代码如下:

@Rest(value = "insertPurOrderDetX", method = OptimusRequestMethod.POST,desc = "添加采购单明细数据")
    public Result<String> insertPurOrderDetX(@Json("@requestBody") PurOrderDetDTO purOrderDetDTO){
        GlobalValidation.validate(purOrderDetDTO,"insertPurOrderDetX", Default.class);
        // 整车 四级必填
        if(InvoiceEnum.CAR.getCode().equals(purOrderDetDTO.getGoodsL1CategoryCode())){
            GlobalValidation.validate(purOrderDetDTO,"insertPurOrderDetX", PurOrderJsonPurOrderDetDTOGoodsCategory.class);
        }else{
            GlobalValidation.validate(purOrderDetDTO,"insertPurOrderDetX", PurOrderJsonPurOrderDetDTOGoodsCategoryElse.class);
        }
        try{
//            purOrderService.insertPurOrderDet(purOrderDetDTO);
            List<com.souche.sc.hessian.model.dto.PurOrderDetDTO> purOrderDetDTOList = new ArrayList<>();
            com.souche.sc.hessian.model.dto.PurOrderDetDTO purOrderDetDTO1 = new com.souche.sc.hessian.model.dto.PurOrderDetDTO();
            BeanUtils.copyProperties(purOrderDetDTO,purOrderDetDTO1);
            purOrderDetDTOList.add(purOrderDetDTO1);
            purOrderApiService.insertBatchPurOrderDet(purOrderDetDTOList);
        }catch (Exception e){
            logger.error("新增采购单明细数据失败:{}",e.getMessage(),e);
            return Result.fail(ErrorCodes.UNKNOW_EXCEPTION.getErrorCode(), "新增采购单明细数据失败");
        }
        return Result.success("Ok");
    }

配置规则文档

Bean Validation constraints

Annotation Supported data types 作用 Hibernate metadata impact
@AssertFalse Booleanboolean Checks that the annotated element is false. 没有
@AssertTrue Booleanboolean Checks that the annotated element is true. 没有
@DecimalMax BigDecimalBigIntegerStringbyteshortintlong and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number. 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示. 没有
@DecimalMin BigDecimalBigIntegerStringbyteshortintlong and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number. 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示. 没有
@Digits(integer=, fraction=) BigDecimalBigIntegerStringbyteshortintlong and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number. Checks whether the annoted value is a number having up to integer digits and fractionfractional digits. 对应的数据库表字段会被设置精度(precision)和准度(scale).
@Future java.util.Datejava.util.Calendar; Additionally supported by HV, if the Joda Time date/time API is on the class path: any implementations of ReadablePartial and ReadableInstant. 检查给定的日期是否比现在晚. 没有
@Max BigDecimalBigIntegerbyteshortintlongand the respective wrappers of the primitive types. Additionally supported by HV: String(the numeric value represented by a String is evaluated), any sub-type of Number. 检查该值是否小于或等于约束条件中指定的最大值. 会给对应的数据库表字段添加一个 check 的约束条件.
@Min BigDecimalBigIntegerbyteshortintlongand the respective wrappers of the primitive types. Additionally supported by HV: String(the numeric value represented by a String is evaluated), any sub-type of Number. 检查该值是否大于或等于约束条件中规定的最小值. 会给对应的数据库表字段添加一个 check 的约束条件.
@NotNull Any type Checks that the annotated value is not null. 对应的表字段不允许为 null.
@Null Any type Checks that the annotated value is null. 没有
@Past java.util.Datejava.util.Calendar; Additionally supported by HV, if the Joda Time date/time API is on the class path: any implementations of ReadablePartial and ReadableInstant. 检查标注对象中的值表示的日期比当前早. 没有
@Pattern(regex=, flag=) String 检查该字符串是否能够在match指定的情况下被regex定义的正则表达式匹配. 没有
@Size(min=, max=) StringCollectionMap and arrays Checks if the annotated element’s size is between min and max (inclusive). 对应的数据库表字段的长度会被设置成约束中定义的最大值.
@Valid Any non-primitive type 递归的对关联对象进行校验, 如果关联对象是个集合或者数组, 那么对其中的元素进行递归校验,如果是一个 map,则对其中的值部分进行校验. 没有

Custom constraints provided by Hibernate Validator

Annotation Supported data types 作用 Hibernate metadata impact
@CreditCardNumber String Checks that the annotated string passes the Luhn checksum test. Note, this validation aims to check for user mistakes, not credit card validity! See also Anatomy of Credit Card Numbers. 没有
@Email String Checks whether the specified string is a valid email address. 没有
@Length(min=, max=) String Validates that the annotated string is between min and max included. 对应的数据库表字段的长度会被设置成约束中定义的最大值.
@NotBlank String Checks that the annotated string is not null and the trimmed length is greater than 0. The difference to @NotEmpty is that this constraint can only be applied on strings and that trailing whitespaces are ignored. 没有
@NotEmpty StringCollectionMap and arrays Checks whether the annotated element is not null nor empty. 没有
@Range(min=, max=) BigDecimalBigIntegerStringbyteshortintlong and the respective wrappers of the primitive types Checks whether the annotated value lies between (inclusive) the specified minimum and maximum. 没有
@SafeHtml(whitelistType=, additionalTags=) CharSequence Checks whether the annotated value contains potentially malicious fragments such as <script/>. In order to use this constraint, the jsoup library must be part of the class path. With the whitelistType attribute predefined whitelist types can be chosen. You can also specify additional html tags for the whitelist with the additionalTags attribute. 没有
@ScriptAssert(lang=, script=, alias=) Any type 要使用这个约束条件,必须先要保证 Java Scripting API 即 JSR 223 ("Scripting for the JavaTM Platform")的实现在类路径当中. 如果使用的时 Java 6 的话,则不是问题, 如果是老版本的话, 那么需要把 JSR 223 的实现添加进类路径. 这个约束条件中的表达式可以使用任何兼容 JSR 223 的脚本来编写. (更多信息请参考 javadoc) 没有
@URL(protocol=, host=, port=, regexp=, flags=) String Checks if the annotated string is a valid URL according to RFC2396. If any of the optional parameters protocolhost or port are specified, the corresponding URL fragments must match the specified values. The optional parameters regexp and flags allow to specify an additional regular expression (including regular expression flags) which the URL must match.


温馨提示:若在升级会员或付费后阅读过程中遇到问题,请加客服微信号(cool-smiler)沟通解决,祝您生活愉快。
转载请注明原文链接:JavaWeb校验框架Hibernate Validator最佳实践
喜欢 (0)
[1186664388@qq.com]
分享 (0)
关于作者:
创享视界(creativeview.cn)是一个带动全民颠覆八小时工作制,通过投稿把自己的创意智慧变现的方式创造被动收入,从而实现财务自由的平台。我们相信,创新思维不仅有助于打造更出色的产品,还可以让世界变得更美好,让人人受益。
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
%d 博主赞过: