springboot hibernate-validator 校验

前言

这段时间在调整老系统相关的一些业务代码;发现一些模块,在无形中就被弄的有点乱了,由于每个开发人员技术水平不同、编码习惯差异;从而导致在请求、响应、异常这一块儿,出现了一些比较别扭的代码;但是归根究底,主要问题还是出在规范上面;不管是大到项目还是小到功能模块,对于请求、响应、异常这一块儿,应该是一块儿公共的模板化的代码,一旦定义清楚之后,是不需要做任何改动,而且业务开发过程中,也几乎是不需要动到他丝毫;所以,一个好的规范下,是不应该在这部分代码上出现混乱或者别扭的情况的;忍不住又得来整理一下这一块儿的东西;

作为一个后台的工程师,接受请求、处理业务、解决异常、响应数据,几乎覆盖了日常开发的全部;但是这个中间,除了业务代码是不可避免且无可替代之外;其他的三项操作,不管是啥功能,也都是大同小异的,那我们要如何来把这一块儿的东西抽离出来,让我们只需要去管业务,不用去管那些杂七杂八的的破事儿,从而腾出更多的时间学(mo)习(yu)呢?当然就是得去定义一个好的规则,运用优秀的轮子;让这部分重复的、可复用的工作给模板化、标准化

这样,开发一遍,后面就不需要再去弄这些通用的东西了。

思考一下,关于请求、响应、异常,我们到底要注意些啥问题呢?

问题点

请求

1. 如何优雅的接受数据?
2. 如何优雅的校验数据?

响应

1. 响应数据格式如何统一?
2. 错误码如何规范
3.如何将业务功能和响应给剥离开来?

异常

1. 异常如何捕获?
2. 业务异常、校验异常如何合理的转换为友好的标准响应?
3. 如何规避未捕获到的异常并优雅返回标准响应?

这一些列的问题,就衍生出,我们该如何去规范的问题?任何利用已有的优秀框架去解决这些问题?
接下来,就通过一个完整的示例,基于这三个大点下面的小问题,去把这个规范给讲清楚;
讲每个大的问题点之前,我会给大家一个或几个疑问;然后可以带着这些疑问,边思考边看。

下面的介绍,我们就以一个简单的用户信息(UserInfo)的CURD展开

hibernate-validator优雅的处理请求
疑问

  1. 我们要如何去校验请求的数据?
  2. 相同的对象去接受不同请求数据,如何能区别校验?

主要的目的是为了减少一些非必要的DTO对象

@RestController
@RequestMapping("user")
public class UserController{

    @PostMapping("add")
    public String add(@RequestBody UserAddRequestDto addInfo){
        // ......
        return "ok";
    }

    @PutMapping("update")
    public void update(@RequestBody UserUpdateRequestDto updateInfo)
    {
        // ......
        return "ok";
    }
}

这样?嗯!这样确实可以接受到请求参数,但是我们回归到上面的疑问;

参数如何校验?难道这样?

if(null==addInfo.getUserName()){
 throw new Exceprion();
}
if(null==addInfo.getPassWord()){
 throw new Exceprion();
}
// 。。。。

固然可以,这样真的好吗?很明显不好。。。。劳力伤神的事儿,咱可不干。
addInfo和updateInfo大部分属性都是一样的,添加的字段,大部分都是可以进行修改的,但是也有部分是不可以修改的;比如密码,一般都是单独写接口进行修改;

既然大部分都一样;有必要定义这么多个请求的DTO对象吗?有必要!!没办法啊!大部分一样,他也有不一样的地方!

那有没有能优雅的去解决参数校验问题,又可以将请求对象合多为一呢?

hibernate-validator就是一个可以完美的解决这些问题的优秀框架;
接下来,我们就详细的来看一下,如何使用这个工具。

hibernate-validator

优点

  1. 解耦,数据的校验与业务逻辑进行分离,降低耦合度
    到controller的对象就已经是校验过的对象了,接受到之后就只需要安心处理业务就好,不用再进行数据校验相关逻辑

  2. 规范的校验方式,减少参数校验所带来的繁琐体力活
    以注解的方式配置校验规则;大大减少校验的工作量,而且复用性强

  3. 简洁代码,提高代码的可读性
    以注解方式即可完成属性校验,去掉了各种冗长的校验代码;且所有的校验规则都定义在对象内部;使得代码结构更加清晰,可读性非常强。

注解说明
下面包含了validator的所有内置的注解

注解作用
@AssertFalse被注释的元素必须为 false
@AssertTrue被注释的元素必须为 true
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Email被注释的元素必须是电子邮箱地址
@Future被注释的元素必须是一个将来的日期
@Length(min=,max=)被注释的字符串的大小必须在指定的范围内
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Negative该值必须小于0
@NegativeOrZero该值必须小于等于0
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@NotBlank(message =)验证字符串非null,且长度必须大于0
@NotEmpty被注释的字符串的必须非空
@Past被注释的元素必须是一个过去的日期
@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
@Positive该值必须大于0
@PositiveOrZero该值必须大于等于0
@Range(min=,max=,message=)被注释的元素必须在合适的范围内
@Size(max=, min=)数组大小必须在[min,max]这个区间
@URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
@Valid该注解主要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用,这样在检查当前对象的同时也会检查该字段所引用的对象

如何简单使用?

第一步;引入依赖

<dependency>
   <groupId>org.hibernate.validator</groupId>
   <artifactId>hibernate-validator</artifactId>
</dependency>

第二步;属性添加对应的注解

按照上面表格的说明,根据自己定义属性的特点,添加相应的注解。

如下示例,用户名,密码,年龄不能为空;那我们就用@NotBlank @NotNull去修饰,如果违背规则,就会按message的文本提示

年龄不能小于0岁、大于120岁;那么就用@min @max进行约束

message描述了违背校验规则之后的描述。

@Data
public class UserRequestDto {
    /**
     * 用户名
     */
    @NotBlank(message = "姓名不能为空")
    public String userName;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    public String passWord;

    /**
     * 年龄
   */
    @NotNull(message = "年龄不能为空")
    @Min(value = 0,message = "年龄不能小于0岁")
    @Max(value = 120,message = "年龄不能大于120岁")
    private Integer age;
    
    /**
     * 手机号码;使用正则进行匹配
     */
    @NotBlank(message = "手机号码不能为空")
    @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!")
    private String phoneNum;
 
    // 。。。。
}

第三步,Controller的参数加上@Validated

@PostMapping("add")
public String add(@Validated @RequestBody UserRequestDto userRequestDto) {
 // 。。。。
}

第四步,测试
在这里插入图片描述
第五步,异常处理

上面的操作可以看出,当请求参数如果不符合条件的话,就已经抛出异常并响应客户端了;

但是异常并没有针对性的处理,也没有进行友好的提示;前端收到错误之后,没办法根据错误信息准确的判断出是什么问题;因此对于的异常还需要进行特殊处理;
全局异常:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 处理自定义异常
     *
     */
    @ExceptionHandler(value = BusinessException.class)
    public AjaxResult bizExceptionHandler(BusinessException e) {
        log.error(e.getMessage(), e);
        return AjaxResult.defineError(e);
    }
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public AjaxResult exceptionHandler( MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);
        return AjaxResult.otherError(e.getFieldError().getDefaultMessage(),500);

    }
    /**
     *处理其他异常
     *
     */
    @ExceptionHandler(value = Exception.class)
    public AjaxResult exceptionHandler( Exception e) {
        log.error(e.getMessage(), e);
        return AjaxResult.otherError(ErrorEnum.INTERNAL_SERVER_ERROR);

    }
}

异常枚举类

public enum ErrorEnum {
    // 数据操作错误定义
    SUCCESS(200, "成功"),
    NO_PERMISSION(403,"你没得权限"),
    NO_AUTH(401,"未登录"),
    NOT_FOUND(404, "未找到该资源!"),
    INTERNAL_SERVER_ERROR(500, "服务器异常请联系管理员"),
    ;

    /** 错误码 */
    private Integer errorCode;

    /** 错误信息 */
    private String errorMsg;

    ErrorEnum(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }
}

自定义异常

public class BusinessException extends RuntimeException{
    private static final long serialVersionUID = 1L;
    /**
     * 错误状态码
     */
    protected Integer errorCode;
    /**
     * 错误提示
     */
    protected String errorMsg;

    public BusinessException(){
    }

    public BusinessException(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

通用返回类

public class AjaxResult {
    //是否成功
    private Boolean success;
    //状态码
    private Integer code;
    //提示信息
    private String msg;
    //数据
    private Object data;
    public AjaxResult() {

    }
    //自定义返回结果的构造方法
    public AjaxResult(Boolean success,Integer code, String msg,Object data) {
        this.success = success;
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
    //自定义异常返回的结果
    public static AjaxResult defineError(BusinessException de){
        AjaxResult result = new AjaxResult();
        result.setSuccess(false);
        result.setCode(de.getErrorCode());
        result.setMsg(de.getErrorMsg());
        result.setData(null);
        return result;
    }
    //其他异常处理方法返回的结果
    public static AjaxResult otherError(ErrorEnum errorEnum){
        AjaxResult result = new AjaxResult();
        result.setMsg(errorEnum.getErrorMsg());
        result.setCode(errorEnum.getErrorCode());
        result.setSuccess(false);
        result.setData(null);
        return result;
    }
    public static AjaxResult otherError(String msg, Integer code){
        AjaxResult result = new AjaxResult();
        result.setMsg(msg);
        result.setCode(code);
        result.setSuccess(false);
        result.setData(null);
        return result;
    }
    public Boolean getSuccess() {
        return success;
    }
    public void setSuccess(Boolean success) {
        this.success = success;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }

}

上面我们已经将请求的参数以一种比较优雅的方式给验证了;但是并没有将请求对象合并,依然还是使用的addInfo和updateInfo对参数进行接受的;下面就一起来看一下,如何将这边同质化的对象进行优雅的合并。

请求对象的合并

group分组校验说明

上面的业务场景中添加和修改用户信息,添加的时候,密码字段是必传的;修改的时候,密码是不需要传的;那我们能否把添加和修改所有用到的属性定义到一个对象中,然后根据不同的请求,去校验参数,比如,调用添加接口,密码是必传的;调用修改接口,就不需要传密码;为了能做到接口区分校验,就可以用到group这个关键参数;

group的理解

可以简单的理解就是把各个属性进行分组;校验的时候,会根据当前Controller指定的组进行校验,这些组里面包含了那些属性,就只校验那些属性,其他不在范围内的,就直接给忽略调掉。

group定义
group的定义是以接口为基本单元;也就是一个接口代表一个组;

使用示例
定义基础的、修改、添加的接口(group)

// 基础的校验接口,标识着所有操作都需要校验的字段
public interface UserRequestDtoSimpleValidate {};

// 修改的校验;继承自UserRequestDtoSimpleValidate 
// 也就是说指定为这个组的时候在满足当前校验规则的同时还得校验simple接口的属性
public interface UserRequestDtoUpdateValidate extends UserRequestDtoSimpleValidate {}

// 原理同上
public interface UserRequestDtoAddValidate extends UserRequestDtoUpdateValidate {}

在这里插入图片描述

属性校验添加上分组配置

@Data
public class UserRequestDtoGroups {
    /**
     * 用户名
     */
    @NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class)
    public String userName;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空",groups = UserRequestDtoAddValidate.class)
    public String passWord;

    /**
     * 年龄
     */
    @NotNull(message = "年龄不能为空",groups = UserRequestDtoSimpleValidate.class)
    @Min(value = 0,message = "年龄不能小于0岁",groups = UserRequestDtoSimpleValidate.class)
    @Max(value = 120,message = "年龄不能大于120岁",groups = UserRequestDtoSimpleValidate.class)
    private Integer age;

    /**
     * 手机号码;使用正则进行匹配
     */
    @NotBlank(message = "手机号码不能为空",groups = UserRequestDtoAddValidate.class)
    @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!",groups = UserRequestDtoAddValidate.class)
    private String phoneNum;

}

Controller指定分组进行校验

如下@Validated中,指定分组接口类;可以一个,也可以多个,这样就会按照指定的分组进行参数校验

 @PostMapping("add")
    public String add(@Validated(UserRequestDtoAddValidate.class) @RequestBody UserRequestDtoGroups dtoGroups) {
        // 后续业务
        return "ok";
    }

    @PutMapping("update")
    public void update(@Validated(UserRequestDtoUpdateValidate.class) @RequestBody UserRequestDtoGroups dtoGroups)  {
        // 后续业务
       
    }

测试:
在这里插入图片描述
结果没有报错,因为熟悉走的是新增的校验group,而controller写的是修改的group,所以不会报错。
;修改属性的校验分组修改为修改的校验。

在这里插入图片描述
因为分组一致则进行了校验。

自定义校验

上面的所有校验,全部使用的是内置的注解,实际的使用过程中,不可避免的有一些特殊的业务场景,参数规则太过于个性化,内置的注解无法满足我们的需求时,要怎么办?比如说,文本必须全部是大写或者小写(该需求其实也可以通过正则表达式的方式进行);为了剧情需要,那我们可以基于这个需求,来自定义一个校验器;

定义大小写的枚举

用于注解使用的时候,来指定是校验规则是大写的还是小写的

public enum CaseMode {
    //大写
    UPPER,
    //小写
    LOWER;
}

定义校验大小写的注解

@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//指定校验器
@Constraint(validatedBy = CaseCheckValidator.class)
public @interface CaseCheck {
    String message() default "";

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

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

    CaseMode value() default CaseMode.UPPER;
}

这里面,我们将CaseMode枚举作为了注解中的value参数,可以根据需要动态设置大小写的参数,这里默认就是大写的;

@Constraint(validatedBy = CaseCheckValidator.class) 指明的使用CaseCheckValidator这个校验器进行数据校验;具体的校验规则,判断逻辑,就是写在这个校验器里面。

自定义校验器

public class CaseCheckValidator implements ConstraintValidator<CaseCheck, String> {
    //大小写的枚举
    private CaseMode caseMode;

    @Override
    public void initialize(CaseCheck caseCheck) {
        this.caseMode = caseCheck.value();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //如果文本是空,则不进行校验,因为有其他的注解是可以校验空或者空字符串的
        if (null == value) {
            return true;
        }

        //文本只能是字母的正则
        String pattern = "^[a-zA-Z]*$";
        //校验传进来的是否是只包含了字母的文本
        boolean isMatch = Pattern.matches(pattern, value);
        //如果存在其他字符则返回校验失败
        if (!isMatch) {
            return false;
        }

        //如果没有指定方式,则直接返回false
        if (null == caseMode) {
            return false;
        }

        //判断是否符合大小写条件
        if (caseMode == CaseMode.UPPER) {
            return value.equals(value.toUpperCase());
        } else {
            return value.equals(value.toLowerCase());
        }
    }
}

泛型说明
该校验器继承自ConstraintValidator这个接口;并传递了两个泛型参数;第一个是指明你自定义的注解;第二个是该注解作用的属性类型;

校验初始化
如果属性添加了该校验器对应的注解,就会初始化(initialize)该校验器时,将你加在属性上面的注解传递进来;

验证
初始化完会调用isValid方法·,并传递属性值;拿到属性值之后,就可以根据初始化传入的注解指定的规则,对属性值进行校验。验证通过返回true,并进行下一个属性的校验;验证失败返回false,并抛出异常;
测试

/**
 * 用户名
 */
@NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class)
@CaseCheck(value = CaseMode.UPPER,message = "用户名必须大写字母",groups = UserRequestDtoSimpleValidate.class)
public String userName;

// 。。。。

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值