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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容