• Spring boot使用Javax.validation和ControllerAdvice来进行参数校验


    对于写Java的同学来说,参数校验是繁琐且重复性很高的代码。很多时候我们的业务代码编写之前先要进行很多的参数校验,浪费了大量的时间和精力。而java中其实已经内置了参数校验的工具,本篇文章主要介绍如何使用Javax.validation来进行参数校验。

    @validated注解

    @validated是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。看到一下注解的源码,我们可以看到@Validated注解可以作用在类、方法和参数上。

    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Validated {
     Class<?>[] value() default {};
    }
    

    废话不多说,我们直接举个例子来看看@validated到底好不好用。实际项目中很常见的应用是分页查询的接口,通常分页查询至少需要当前页和页大小这两个字段。通常我们会把分页请求需要的参数封装成一个PageQuery。一个常见的分页参数类,在很多接口中需要使用。我们就可以给这样的参数加上@Validated注解。表示此类开启参数校验

    public Result<Page<Map<String, Object>>> getPage(@Validated PageQuery pageQuery) {  
     // 设置页大小,当前页  
        Page<Map<String, Object>> page = new Page<>(pageQuery.getCurrentPage(), pageQuery.getPageSize());  
        page.setRecords(service.findPage(page, pageQuery));  
        return this.responseBody(Result.ResponseEnum.GET_SUCCESS, page);  
    }
    

    在类的字段上,我们定义校验的规则和返回的错误提示。@validated中所有的校验注解,可以参考下面的表格。

    限制 说明
    @Null 限制只能为null
    @NotNull 限制必须不为null
    @AssertFalse 限制必须为false
    @AssertTrue 限制必须为true
    @DecimalMax(value) 限制必须为一个不大于指定值的数字
    @DecimalMin(value) 限制必须为一个不小于指定值的数字
    @Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
    @Max(value) 限制必须为一个不大于指定值的数字
    @Min(value) 限制必须为一个不小于指定值的数字
    @Past 限制必须是一个过去的日期
    @Future 限制必须是一个将来的日期
    @Pattern(value) 限制必须符合指定的正则表达式
    @Size(max,min) 限制字符长度必须在min到max之间
    @NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
    @NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
    @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
    对于页大小,我们限制为非空且不能小于1;对于当前页我们限制为非空。
    public class PageQuery implements Serializable {  
     private static final long serialVersionUID = 1L;  
      
    /**  
     * 页大小  
     */  
     @NotNull(message = "页大小不能为空")  
     @Min(message = "页大小不能小于1", value = 1)  
     Integer pageSize;  
      
    /**  
     * 当前页  
     */  
     @NotNull(message = "当前页不能为空")  
     Integer currentPage;  
     
    

    我们用一个明显校验不通过的参数来请求下这个接口,看看会返回什么。我在请求参数中不传递pageSize参数,然后发送请求。

    {
        "timestamp": "2021-12-16T03:09:36.238+0000",
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
                "codes": [
                    "NotNull.pageQuery.currentPage",
                    "NotNull.currentPage",
                    "NotNull.java.lang.Integer",
                    "NotNull"
                ],
                "arguments": [
                    {
                        "codes": [
                            "pageQuery.currentPage",
                            "currentPage"
                        ],
                        "arguments": null,
                        "defaultMessage": "currentPage",
                        "code": "currentPage"
                    }
                ],
                "defaultMessage": "当前页不能为空",
                "objectName": "pageQuery",
                "field": "currentPage",
                "rejectedValue": null,
                "bindingFailure": false,
                "code": "NotNull"
            },
            {
                "codes": [
                    "NotNull.pageQuery.env",
                    "NotNull.env",
                    "NotNull.java.lang.String",
                    "NotNull"
                ],
                "arguments": [
                    {
                        "codes": [
                            "pageQuery.env",
                            "env"
                        ],
                        "arguments": null,
                        "defaultMessage": "env",
                        "code": "env"
                    }
                ],
                "defaultMessage": "设备所属环境信息不能为空",
                "objectName": "pageQuery",
                "field": "env",
                "rejectedValue": null,
                "bindingFailure": false,
                "code": "NotNull"
            },
            {
                "codes": [
                    "NotNull.pageQuery.pageSize",
                    "NotNull.pageSize",
                    "NotNull.java.lang.Integer",
                    "NotNull"
                ],
                "arguments": [
                    {
                        "codes": [
                            "pageQuery.pageSize",
                            "pageSize"
                        ],
                        "arguments": null,
                        "defaultMessage": "pageSize",
                        "code": "pageSize"
                    }
                ],
                "defaultMessage": "页大小不能为空",
                "objectName": "pageQuery",
                "field": "pageSize",
                "rejectedValue": null,
                "bindingFailure": false,
                "code": "NotNull"
            }
        ],
        "message": "Validation failed for object='pageQuery'. Error count: 3",
        "path": "/localLoadBalance/approval/getApproval"
    }
    

    可以看到,返回的结果中包含了我们之前预设的校验提示和内容。不过到这里我们的任务还没有结束,实际项目中,我们不允许接口返回这样的类型。大多数情况下,我们希望接口的返回结果有通用的模板格式。而上面那样的返回方式需要前端做大量的解析,而且也不符合后端接口的规范。因此我们希望能有一个全局的处理器,来解析@validated抛出的异常。

    使用全局异常处理类来进行统一的异常处理

    在Spring boot项目中,我们可以使用@ControllerAdvice注解来进行全局的异常处理,当然@ControllerAdvice的用处不止是异常处理,还可以实现统一的参数绑定和数据的预处理。详情可以参考# SpringMVC 中 @ControllerAdvice 注解的三种使用场景!

    首先我们新增一个handler,当然你也可以指定一个包来扫描包下的所有controller。如@ControllerAdvice(basePackages="com.test.controller"),然后我们使用@ExceptionHandler来进行异常的处理。

    此处需要说明的是,我们是针对@validated进行的异常的处理,因此我们希望异常校验类只拦截@validated注解抛出的异常。所以在本方法中,我只让@ExceptionHandler拦截了BindException。其次,针对参数校验出现多个异常的情况,我们把多个错误信息通过逗号分隔开来。

    @ControllerAdvice  
    public class GlobalHandler {  
     private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);  
      
        /**  
     * 全局处理所有使用了@validation校验参数的controller  
     * @param e 捕获到validation抛出异常  
     * @return 返回参数中所有的校验错误,以,分隔不用的错误信息  
     */  
     @ResponseBody  
     @ExceptionHandler(BindException.class)  
     public Result<Void> exceptionHandler(BindException e) {  
     String errors=e.getBindingResult().getAllErrors().stream()  
     .map(ObjectError::getDefaultMessage)  
     .collect(Collectors.joining(","));  
            logger.error("Request params error,caught by global exception handler,{}",errors);  
            return Result.<Void>toBuilder()  
     .code(0)  
     .msg(errors)  
     .builder();  
        }  
    }
    

    再次准备一个含有错误参数的请求,这次我们不传currentPage,pageSize的值为-1。我们看看会返回什么。

    {
     "code": 0,
     "msg": "当前页不能为空,页大小不能小于1",
     "data": null
    }
    

    可以看到,返回的结果符合我们的预期。

    在获取错误信息的地方我们看到有针对BindException的异常信息解析,涉及了多个.操作。有经验的老鸟可能觉得这里容易出现空指针异常。不过此处你大可放心,BindException中的BindingResult是绝对不会为null的。我们看下源码,可以看到内部是用断言来保证结果不为空的。

    	/**
    	 * Create a new BindException instance for a BindingResult.
    	 * @param bindingResult the BindingResult instance to wrap
    	 */
    	public BindException(BindingResult bindingResult) {
    		Assert.notNull(bindingResult, "BindingResult must not be null");
    		this.bindingResult = bindingResult;
    	}
    

    参考文章

    SpringMVC 中 @ControllerAdvice 注解的三种使用场景! - 江南一点雨 - 博客园 (cnblogs.com)

    javax.validation 参数验证 - 不朽丶 - 博客园 (cnblogs.com)

    @Validated详解 - yuxinkuan - 博客园 (cnblogs.com)

  • 相关阅读:
    寒假学习进度15
    寒假学习进度14
    寒假学习进度13
    Markdown使用笔记
    MVC
    阅读笔记大型网站技术架构01
    周总结1大数据采集技术与应用(徳拓)五次实验总结
    阅读笔记架构漫谈03
    质量属性易用性分析
    阅读笔记架构漫谈02
  • 原文地址:https://www.cnblogs.com/rever/p/15697946.html
Copyright © 2020-2023  润新知