Spring-boot如何实现参数校验

Spring-boot如何实现参数校验

目录

Spring-boot如何实现参数校验

一、为什么需要参数校验

二、Validator框架发展史

1、Hibernate-Validator参数校验

2、Springboot-Validator参数校验

三、使用校验注解定义对象

1、定义入参对象

2、常用的注解

3、接口调用触发校验机制

4、参数异常触发全局异常拦截器

5、自定义校验注解

四、分组校验

1、场景

2、自定义分组接口

3、入参对象中引用分组类别

4、接口调用引用分组校验

五、小结


一、为什么需要参数校验


在平时的接口开发中,为了预防非法参数传入对业务造成影响,经常需要对接口入参进行校验拦截,例如:登陆时需要判断用户名密码是否为空,新增用户的时候需要判断用户手机号、邮箱是否符合规范。早期将参数校验写入了代码中,长期下来校验逻辑很繁琐,代码可读性也比较差。所以,引进了Validator参数校验框架。

Validator参数校验框架就是为了减少开发人员日常代码量,对于参数校验提供方便,提升开发效率。

Validator校验框架遵循了JSR-303验证规范(参数校验规范), 
JSR是 Java Specification Requests的缩写。

二、Validator框架发展史

1、Hibernate-Validator参数校验

(1)早先 POM文件 依赖引入

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

(2)某个版本之后迁移依赖位置

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

2、Springboot-Validator参数校验

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

<!-- Springboot-Validate -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注:从 springboot-2.3开始,校验包被独立成了一个 starter组件,并且内部也引用了 上述(2)的依赖,
所以需要引入validation和web,而 springboot-2.3之前的版本只需要引入 web 依赖就可以了。

三、使用校验注解定义对象

1、定义入参对象

                

@Data
public class MenuInfoEditionInput {

    /**
     * 主键ID
     */
    @NotEmpty(message = "主键ID不可为空!")
    private String id;

    /**
     * 版本号
     */
    @NotEmpty(message = "版本号不可为空!")
    private String editionId;

    /**
     * 版本名称
     */
    @NotEmpty(message = "版本名称不可为空!")
    private String editionName;

    /**
     * 排序号
     */
    @NotNull(message = "版本排序号不可为空!")
    private Integer sortNo;

    /**
     * 学科ID
     */
    @NotNull(message = "学科ID不可为空!")
    private Integer subId;

}

                每个注解后需要根据业务需求设计返回描述,即message值。

2、常用的注解

                

常见的约束注解
注解描述
@AssertFalse可以为null,如果不为null的话必须为false
@AssertTrue可以为null,如果不为null的话必须为true
@DecimalMax设置不能超过最大值
@DecimalMin设置不能超过最小值
@Digits设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
@Future日期必须在当前日期的未来
@Past日期必须在当前日期的过去
@Max最大不得超过此最大值
@Min最大不得小于此最小值
@NotNull不能为null,可以是空
@Null必须为null
@NotBlank字符串不能为null,字符串trim()后也不能等于“”
@NotEmpty不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
@Pattern必须满足指定的正则表达式
@Size集合、数组、map等的size()值必须在指定范围内
@Email必须是email格式
@Length长度必须在指定范围内
@Range值必须在指定范围内
@URL必须是一个URL

                        注:此表格只是简单的对注解功能的说明,并没有对每一个注解的属性进行说明;可详见源码。

3、接口调用触发校验机制

 (1) Controller层校验设计

                

@RestController
@CrossOrigin
@RequestMapping(value = "/menuEdition")
public class MenuInfoEditionController {
    private static Logger logger = LoggerFactory.getLogger(MenuInfoEditionController.class);

    @Autowired
    private IMenuInfoEditionService menuInfoEditionService;

    /**
     * 目录版本列表查询
     */
    @PostMapping(value = "/selectEditionList")
    public ApiResult<List<MenuInfoEditionOutput>> selectListEditionBySubId(@Validated @RequestBody MenuInfoEditionInput menuInfoEditionInput) {
        logger.info("开始查询目录版本列表……");
        List<MenuInfoEditionOutput> menuInfoEditionOutputList = menuInfoEditionService.selectListEditionBySubId(menuInfoEditionInput);
        logger.info("目录列表查询结束……");
        return ApiResult.OK(menuInfoEditionOutputList);
    }

}

这里定义的方法上使用了 @RequestBody 注解,用于接收前端传送的JSON数据。注意:使用Validator校验必须加上 @Validated 注解才能生效,否则无用。

(2)执行结果

{
        "code": 5000,
        "message": "HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Integer'. Check configuration for 'subId'",
        "path": null,
        "traceId": "bf1b061badf1441395be5ead0cf4026d",
        "data": null
}

4、参数异常触发全局异常拦截器

根据上述拦截提示发现,尽管系统中已经引入了全局异常拦截器,可是对于 Validator校验的返回内容不太友好,显得有些臃肿,不便于阅读,故,将拦截内容进行改进,优化一下。

@Slf4j
@ControllerAdvice
public class ValidException {

    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ApiResult handleMethodArgumentNotValidException(Exception exception) {
        StringBuilder errorInfo = new StringBuilder();
        BindingResult bindingResult=null;
        if(exception instanceof MethodArgumentNotValidException){
            bindingResult= ((MethodArgumentNotValidException)exception).getBindingResult();
        }
        if(exception instanceof BindException){
            bindingResult= ((BindException)exception).getBindingResult();
        }
        for(int i = 0; i < bindingResult.getFieldErrors().size(); i++){
            if(i > 0){
                errorInfo.append(",");
            }
            FieldError fieldError = bindingResult.getFieldErrors().get(i);
            errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
        }
        log.error(errorInfo.toString());
        //这里返回自己的Result的结果类。
        return  ApiResult.validateFailed(errorInfo.toString());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ApiResult  handleDefaultException(Exception exception) {
        log.error(exception.toString());
        //这里返回自己的Result的结果类。
        return  ApiResult.validateFailed("服务器错误",exception);
    }

}

测试结果:

{
    "code": 5000,
    "message": "subId :学科ID不可为空!",
    "path": null,
    "traceId": "81e696c472654c67a771dd59ab39809b",
    "data": null
}

5、自定义校验注解

(1)创建自定义注解

                        

@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Constraint(validatedBy = {MobileValidator.class})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Mobile.List.class)
public @interface Mobile {

    /**
     * 错误提示信息,可以写死,也可以填写国际化的key
     */
    String message() default "手机号码不正确";

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

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

    String regexp() default "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        Mobile[] value();
    }
}

(2)自定义校验逻辑

public class MobileValidator implements ConstraintValidator<Mobile, String> {

    /**
     * 手机验证规则
     */
    private Pattern pattern;

    @Override
    public void initialize(Mobile mobile) {
        pattern = Pattern.compile(mobile.regexp());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return pattern.matcher(value).matches();
    }
}

(3)增加校验注解

//手机号
@Mobile
private String userPhone;

(4)测试结果

{
    "code": 5000,
    "message": "userPhone :手机号码不正确",
    "path": null,
    "traceId": "79da96c472654c67a771dd59ab3qv518",
    "data": null
}

四、分组校验

1、场景

        入参对象中,一些属性在新增时需要必填,但是在修改时不需要必填,如上边的 MenuInfoEditionInput对象中,editionIdeditionName 在新增时需要必填,但是在修改时不需要。之前遇到过同事的处理方式是,创建两个接收对象,分别对象新增、修改两个业务类别,这样可以实现最终的效果,但是业务分类多的情况下,这样会造成类膨胀,对于代码阅读性不友好。

        其实,在 Validator校验框架中考虑到了这个实际应用场景并给予了解决方案,已经引入了业务分组的概念,简称分组校验

2、自定义分组接口

import javax.validation.groups.Default;

public interface ResourceValidateGroup extends Default {

    // 资源管理工具--目录信息校验分组
    interface ManageMenuInfo extends Default {
        // 查询目录
        interface Select {}

        // 新增目录
        interface Insert {}

        // 修改目录
        interface Update {}

        // 删除目录
        interface Delete {}

        // 新增或修改目录
        interface InsertOrUpdate {}
    }

    // 资源管理工具--目录版本校验分组
    interface ManageMenuInfoEdition extends Default {
        // 查询目录
        interface Select {}

        // 新增目录
        interface Insert {}

        // 修改目录
        interface Update {}

        // 删除目录
        interface Delete {}

        // 新增或修改目录
        interface InsertOrUpdate {}
    }

}

自定义一个  ResourceValidateGroup 接口,用于组合各个业务场景的校验分组情况,继承了Default类,具体用于什么场景,待定。

3、入参对象中引用分组类别

@Data
public class MenuInfoEditionInput {

    /**
     * 主键ID
     */
    @NotEmpty(message = "主键ID不可为空!", groups = {ResourceValidateGroup.ManageMenuInfoEdition.Delete.class})
    private String id;

    /**
     * 版本号
     */
    @NotEmpty(message = "版本号不可为空!", groups = {ResourceValidateGroup.ManageMenuInfoEdition.InsertOrUpdate.class})
    private String editionId;

    /**
     * 版本名称
     */
    @NotEmpty(message = "版本名称不可为空!", groups = {ResourceValidateGroup.ManageMenuInfoEdition.InsertOrUpdate.class})
    private String editionName;

    /**
     * 排序号
     */
    @NotNull(message = "版本排序号不可为空!", groups = {ResourceValidateGroup.ManageMenuInfoEdition.InsertOrUpdate.class})
    private Integer sortNo;

    /**
     * 学科ID
     */
    @NotNull(message = "学科ID不可为空!", groups = {ResourceValidateGroup.ManageMenuInfoEdition.InsertOrUpdate.class, ResourceValidateGroup.ManageMenuInfoEdition.Delete.class, ResourceValidateGroup.ManageMenuInfoEdition.Select.class})
    private Integer subId;

}

4、接口调用引用分组校验

@RestController
@CrossOrigin
@RequestMapping(value = "/menuEdition")
public class MenuInfoEditionController {
    private static Logger logger = LoggerFactory.getLogger(MenuInfoEditionController.class);

    @Autowired
    private IMenuInfoEditionService menuInfoEditionService;

    /**
     * 目录版本列表查询
     */
    @PostMapping(value = "/selectEditionList")
    public ApiResult<List<MenuInfoEditionOutput>> selectListEditionBySubId(
            @Validated({ResourceValidateGroup.ManageMenuInfoEdition.Select.class, Default.class}) 
            @RequestBody MenuInfoEditionInput menuInfoEditionInput) {
        logger.info("开始查询目录版本列表……");
        List<MenuInfoEditionOutput> menuInfoEditionOutputList = menuInfoEditionService.selectListEditionBySubId(menuInfoEditionInput);
        logger.info("目录列表查询结束……");
        return ApiResult.OK(menuInfoEditionOutputList);
    }

    /**
     * 目录版本新增或修改
     */
    @PostMapping(value = "/insertOrUpdateEditionList")
    public ApiResult<List<MenuInfoEditionOutput>> insertOrUpdateListMenuInfoEdition(
            @Validated({ResourceValidateGroup.ManageMenuInfoEdition.InsertOrUpdate.class, Default.class}) 
            @RequestBody List<MenuInfoEditionInput> menuInfoEditionInput) {
        logger.info("开始新增目录版本列表……");
        List<MenuInfoEditionOutput> menuInfoEditionOutputList = menuInfoEditionService.insertOrUpdateListMenuInfoEdition(menuInfoEditionInput);
        logger.info("目录列表新增结束……");
        return ApiResult.OK(menuInfoEditionOutputList);
    }

    /**
     * 目录版本删除
     */
    @PostMapping(value = "/deleteEditionList")
    public ApiResult<List<MenuInfoEditionOutput>> deleteMenuInfoEditionList(
            @Validated({ResourceValidateGroup.ManageMenuInfoEdition.Delete.class, Default.class}) 
            @RequestBody List<MenuInfoEditionInput> menuInfoEditionInput) {
        logger.info("开始删除目录版本列表……");
        List<MenuInfoEditionOutput> menuInfoEditionOutputList = menuInfoEditionService.deleteMenuInfoEditionList(menuInfoEditionInput);
        logger.info("目录列表删除结束……");
        return ApiResult.OK(menuInfoEditionOutputList);
    }

}

这里需要注意,如果校验分组继承了 Default类Controller层接口引用校验分组时,需要加上 Default分类才能生效,具体原因还在研究中……

五、小结

非空校验是校验的第一步, 除了非空校验,我们还需要做到以下几点:

  • 普通参数 - 需要限定字段的长度。如果会将数据存入数据库,长度以数据库为准,反之根据业务确定。
  • 类型参数 - 最好使用正则对可能出现的类型做到严格校验。比如type的值是【0|1|2】这样的。
  • 列表(list)参数 - 不仅需要对list内的参数是否合格进行校验,还需要对list的size进行限制。比如说 100。
  • 日期,邮件,金额,URL这类参数都需要使用对于的正则进行校验。
  • 参数真实性 - 这个主要针对于 各种Id 比如说 userId、merchantId,对于这样的参数,都需要进行真实性校验

参数校验越严格越好,严格的校验规则不仅能减少接口出错的概率,同时还能避免出现脏数据,从而来保证系统的安全性和稳定性。

最后,本人开发经验不是很多,好多东西都在学习记录中,文章写的不好还请见谅,如有建议可以在评论区讨论,感谢大家!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值