Spring Boot 参数校验、校验工具类

Spring Boot实现参数校验

目录

参数校验的目的

不做参数校验带来的问题:

  • 入库异常(数据库字段长度为50,实际数据长度为100)
  • NPE异常(未接收到参数就进行调用)
  • 被同事嫌弃(500行代码,250行都在用if…esle做参数校验)
  • ……

做参数校验的好处:

  • 规避了不必要的异常
  • 减少代码量
  • ……

如何进行参数校验

if-else进行参数校验

写的代码可能是这个样子的?????当校验的参数个数变多,校验代码会惨不忍睹…

if...else参数校验

阅读代码的人的心情是这样的……

苦恼ing

硬编码校验的缺点:

  • 代码行数会随着校验参数的个数增多而增多,不利于维护
  • 业务逻辑中含有大量的验证代码,不便于阅读
  • ……

使用注解进行校验

当我们使用注解进行参数校验后,代码很简洁,很优雅…在Spring Boot项目中支持Spring MVC自动参数校验。

注解校验

分类
  • Hibernate Validator 参考网址:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface)

  • Spring Validator ——对Hibernate Validtor的进一步封装,支持Speing MVC自动校验 参考地址:https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/core.html#validation

使用
jar包引入

以下以Spring Boot项目为例进行说明

(1)普通项目引入依赖

      <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.1.Final</version>
        </dependency>

(2)spring boot项目

低版本spring-boot-starter-web模块中包含了hibernate-validator,因此不需要重新再次引入,版本高于2.3.x需要手动引入依赖

2.3.x以下版本依赖结构

注解说明
验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,boolean验证注解的元素值是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型验证注解的元素值大于等于@Min指定的value值
@Max(value=值)和@Min要求一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)和@Min要求一样验证注解的元素值大于等于@DecimalMin指定的value值
@DecimalMax(value=值)和@Min要求一样验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@Min要求一样验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

实战

实际业务开发中,为了避免入参错误对业务系统有影响,一般会都会在Controller层进行参数校验,常见的方式主要包含了两种:

  1. get、delete等请求,参数形式为RequestParam/PathVariable
  2. post、put等请求,参数形式为RequestBoty
RequestParam/PathVariable形式的参数校验

Get、Delete请求一般会使用RequestParam/PathVariable形式参数参数,这种形式的参数校验一般需要以下两个步骤,如果校验失败,会抛出ConstraintViolationException异常。

  1. 必须在Controller类上标注@Validated注解;
  2. 在接口参数前声明约束注解(如@NotBlank等)
@Validated
@RestController
@RequestMapping("/validate")
public class ValidationController {

    private Logger log = LoggerFactory.getLogger(ValidationController.class);

    /**
     * get、delete请求使用requestParam/PathVariable形式传递参数的,
     * 参数校验需要在Controller添加上@Validated注解,并在参数列表中添加对应的校验注解即可
     *
     * @param id ID
     * @return true/false 成功或失败
     */
    @GetMapping("/get")
    public ResultObject<Boolean> validateGetRequest(@NotBlank(message = "id不能为空") String id,
                                                    @NotBlank(message = "appkey不能为空") String appkey) {
        // 具体业务逻辑调用
        log.info("id [{}] appKey [{}]", id, appkey);
        return ResultObject.success();
    }
}
RequestBoty形式的参数校验

POSTPUT请求一般会使用requestBody传递参数,这种情况下,在入参对象上添加@Validated注解就能实现自动参数校验。

比如,有一个保存用户信息的接口,要求username长度是2-20位,password字段长度是8-20位,还要加上邮箱校验。如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。

@Data
public class UserDTO {

    @NotBlank(message = "id不能为空")
    private String id;

    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 20)
    private String username;

    @NotBlank(message = "密码不能为空")
    @Length(min = 8, max = 20)
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

}

在对应的接口上加上@Valid或者@Validated注解即可。

/**
 * post、put等请求使用requestBody形式传递参数的,
 * 在参数列表中使用@Valid或者@Validated进行校验即可
 *
 * @param userDTO 入参
 * @return true/false 成功或失败
 */
@PostMapping("/post")
public ResultObject<Boolean> validatePostRequest(@Valid @RequestBody UserDTO userDTO) {
    // 具体业务逻辑调用
    log.info("userDTO [{}]", userDTO);
    return ResultObject.success();
}

注意:在Spring中,我们使用@Valid 注解进行方法级别验证,同时还能用它来标记成员属性以进行验证。

但是,此注释不支持分组验证。@Validated则支持分组验证。

在全局校验中增加校验异常

未做全局异常处理的时候是这样的

未做异常处理的返回结果

MethodArgumentNotValidExceptionSpring Boot中进行绑定参数校验时的异常,需要在Spring Boot中处理,其他需要处理ConstraintViolationException异常进行处理.

  • 为了优雅一点,我们将参数异常,业务异常,统一做了一个全局异常,将控制层的异常包装到我们自定义的异常中
  • 为了优雅一点,我们还做了一个统一的结构体,将请求的code,和msg,data一起统一封装到结构体中,增加了代码的复用性

做了全局异常处理以后是这样的

全局异常处理后的返回结果

分组校验

背景:在执行保存更新操作的时候,校验的参数可能存在差异,比如保存的时候不需要校验Id,而更新的时候就需要校验id(主键),写两个实体类复用率会很低。所以这个时候分组校验就很有必要

定义分组

定义分组,我们在UserDTO中定义一个Update分组,主要用于校验更新时的操作,一般情况下需要实现Default分组。

定义好分组后,需要在对应的字段上添加上需要校验的分组。

例如,只有在更新的时候需要校验id是否为空,我们在这个加上对应的分组即可。

@Data
public class UserDTO {

    /**
     * 在更新时校验id
     */
    @NotBlank(message = "id不能为空", groups = Update.class)
    private String id;

    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 20)
    private String username;

    @NotBlank(message = "密码不能为空")
    @Length(min = 8, max = 20)
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    /**
     * 更新分组
     */
    public static class Update implements Default {
    }
}

使用分组校验

在需要校验的地方@Validated声明校验组

/**
 * 【分组校验】@Validated注解实现
 *
 * @param userDTO 入参
 * @return true/false 成功或失败
 */
@PostMapping("/put")
public ResultObject<Boolean> validateGroupOne(@Validated(value = {UserDTO.Update.class}) @RequestBody UserDTO userDTO) {
    // 具体业务逻辑调用
    log.info("userDTO [{}]", userDTO);
    return ResultObject.success();
}

嵌套校验

在上诉的例子中,我们使用的基本都是一个DTO中只包含了常用的数据类型,实际开发过程中,可能还包含了嵌套的数据结构,即一个DTO中的属性是另一个我们自定义的业务类。这种情况下就需要进行嵌套校验。

例如,保存用户信息的时候至少需要有一个部门信息,那么需要在departments属性上添加@Valid注解。

@Data
public class UserDTO {

    /**
     * 在更新时校验id
     */
    @NotBlank(message = "id不能为空", groups = Update.class)
    private String id;

    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 20)
    private String username;

    @NotBlank(message = "密码不能为空")
    @Length(min = 8, max = 20)
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull(message = "部门信息不能为空")
    @Valid
    private List<DepartmentDTO> departments;

    /**
     * 更新分组
     */
    public static class Update implements Default {
    }

}

@Data
public class DepartmentDTO {

    @NotBlank(message = "部门名称不能为空")
    private String deptName;

    @NotBlank(message = "部门编码不能为空")
    private String deptCode;

}

嵌套校验可以和分组校验配合使用;

嵌套校验会对集合中的每一项进行校验。

自定义校验工具类

有时候,直接在Controller层进行参数校验,并不满足我们所有的需求,这时,就需要自定义校验工具类。结合全局异常处理,就能非常方便简洁地进行参数校验了

public class ValidationUtils {

    private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory();

    /**
     * 参数校验
     *
     * @param paramObject 需要校验的参数
     */
    public static void validateObject(Object paramObject) {
        Validator validator = VALIDATOR_FACTORY.getValidator();
        Set<ConstraintViolation<Object>> validateResult = validator.validate(paramObject);
        if (CollectionUtils.isEmpty(validateResult)) {
            return;
        }

        StringBuilder errorMessageSb = new StringBuilder();
        for (ConstraintViolation<Object> violation : validateResult) {
            errorMessageSb.append(violation.getMessage()).append(";");
        }
        throw new GlobalException(ResultCodeEnum.USER_PARAMETER_ERROR.getCode(), errorMessageSb.toString());
    }

    /**
     * 参数校验【支持分组校验】
     *
     * @param paramObject 需要校验的参数
     * @param classes     具体的分组
     */
    public static void validateObject(Object paramObject, Class<?> classes) {
        Validator validator = VALIDATOR_FACTORY.getValidator();
        Set<ConstraintViolation<Object>> validateResult = validator.validate(paramObject, classes);
        if (CollectionUtils.isEmpty(validateResult)) {
            return;
        }

        Set<String> errorMsgSet = new HashSet<>();
        for (ConstraintViolation<Object> violation : validateResult) {
            errorMsgSet.add(violation.getMessage());
        }
        throw new GlobalException(ResultCodeEnum.USER_PARAMETER_ERROR.getCode(), String.join(",", errorMsgSet));
    }

    private ValidationUtils() {
    }
}

全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);


    /**
     * 方法参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultObject<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("[全局异常处理] [参数校验不通过]{}", e.getMessage(), e);
        return ResultObject.<String>builder()
                .success(false)
                .code(ResultCodeEnum.USER_PARAMETER_ERROR.getCode())
                .msg(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage())
                .build();
    }

    /**
     * 方法RequestParam/PathVariable形式参数校验异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultObject<String> handleConstraintViolationException(ConstraintViolationException e) {
        log.error("[全局异常处理] [参数校验不通过]{}", e.getMessage(), e);
        return ResultObject.<String>builder()
                .success(false)
                .code(ResultCodeEnum.USER_PARAMETER_ERROR.getCode())
                .msg(e.getMessage())
                .build();
    }

    /**
     * 方法参数校验异常[类型不配备]
     */
    @ExceptionHandler(UnexpectedTypeException.class)
    public ResultObject<String> handleUnexpectedTypeException(UnexpectedTypeException e) {
        log.error("[全局异常处理] [参数校验类型不匹配]{}", e.getMessage(), e);
        return ResultObject.builder(e.getMessage()).success(false)
                .code(ResultCodeEnum.USER_PARAMETER_UNEXPECTED_TYPE.getCode())
                .msg("校验参数类型异常,请检查注释使用是否正确")
                .build();
    }

    /**
     * 自定义异常
     */
    @ExceptionHandler(GlobalException.class)
    public ResultObject<String> handleGlobalException(HttpServletResponse response, GlobalException e) {

        if (ResultCodeEnum.USER_NOT_PERMISSION.getCode().equals(e.getCode())) {
            // 未授权
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }
        log.error("[全局异常处理] [自定义异常]{}", e.getMessage(), e);
        return ResultObject.builder(e.getMessage()).success(false)
                .code(e.getCode())
                .msg(e.getMessage())
                .build();
    }

    /**
     * 捕捉其他所有异常
     */
    @ExceptionHandler(Exception.class)
    public Object handleOtherException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);

        if (status == HttpStatus.NOT_FOUND) {
            log.error("[全局异常处理] [ajax请求] 用户访问页面不存在,异常信息", ex);
            return ResultObject.builder()
                    .code(ResultCodeEnum.SERVICE_INTERFACE_NOT_FOUND.getCode())
                    .msg(ResultCodeEnum.SERVICE_INTERFACE_NOT_FOUND.getMsg())
                    .success(false)
                    .data(null)
                    .build();
        }

        if (status == HttpStatus.BAD_REQUEST) {
            log.error("[全局异常处理] [ajax请求] 用户访问路径错误不存在,异常信息", ex);
            return ResultObject.builder()
                    .code(ResultCodeEnum.SERVICE_INTERFACE_NOT_FOUND.getCode())
                    .msg(ResultCodeEnum.SERVICE_INTERFACE_NOT_FOUND.getMsg())
                    .success(false)
                    .data(null)
                    .build();
        }

        // 系统异常
        log.error("[全局异常处理] [ajax请求] 出现异常,异常信息", ex);
        return ResultObject.builder()
                .code(ResultCodeEnum.SYSTEM_ERROR.getCode())
                .msg(ResultCodeEnum.SYSTEM_ERROR.getMsg())
                .success(false)
                .data(null)
                .build();

    }


    /**
     * 获取响应状态码
     *
     * @param request 请求
     * @return Http状态码
     */
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}

项目demo地址

源码地址:https://gitee.com/szimo/arrow-note-code/tree/master/spring-validation

关于我

gitee:背柴火的小男孩 (szimo) - Gitee.com

微信公众号:

image-20211128173456308

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值