Spring Boot 2.x基础教程:JSR-303实现请求参数校验

请求参数的校验是不少新手开发很是容易犯错,或存在较多改进点的常见场景。比较常见的问题主要表如今如下几个方面:html

  • 仅依靠前端框架解决参数校验,缺失服务端的校验。这种状况常见于须要同时开发先后端的时候,虽然程序的正常使用不会有问题,可是开发者忽略了非正常操做。好比绕过前端程序,直接模拟客户端请求,这时候就会忽然在前端预设的各类限制,直击各类数据访问接口,使得咱们的系统存在安全隐患。
  • 大量地使用if/else语句嵌套实现,校验逻辑晦涩难通,不利于长期维护。

因此,针对上面的问题,建议服务端开发在实现接口的时候,对于请求参数必需要有服务端校验以保障数据安全与稳定的系统运行。同时,对于参数的校验实现须要足够优雅,要知足逻辑易读、易维护的基本特色。前端

接下来,咱们就在本篇教程中详细说说,如何优雅地实现Spring Boot服务端的请求参数校验。java

JSR-303

在开始动手实践以前,咱们先了解一下接下来咱们将使用的一项标准规范:JSR-303git

什么是JSR?github

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人均可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。web

JSR-303定义的是什么标准?spring

JSR-303 是JAVA EE 6 中的一项子规范,叫作Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中全部内置 constraint 的实现,除此以外还有一些附加的 constraint。json

Bean Validation中内置的constraint后端

Hibernate Validator附加的constraint数组

在JSR-303的标准之下,咱们能够经过上面这些注解,优雅的定义各个请求参数的校验。更多关于JSR的内容能够参与官方文档或参考资料中的引文[1]。

动手实践

已经了解了JSR-303以后,接下来咱们就来尝试一下,基于此规范如何实现参数的校验!

准备工做

读者能够拿任何一个使用Spring Boot 2.x构建的提供RESTful API的项目做为基础。也可使用Spring Boot 2.x基础教程:使用Swagger2构建强大的API文档中构建的实验工程做为基础,您能够经过下面仓库中的chapter2-2目录取得:

固然,您也能够根据前文再构建一个做为复习,也是彻底没有问题的。

快速入门

咱们先来作一个简单的例子,好比:定义字段不能为Null。只须要两步

第一步:在要校验的字段上添加上@NotNull注解,具体以下:

@Data
@ApiModel(description="用户实体")
public class User {

    @ApiModelProperty("用户编号")
    private Long id;

    @NotNull
    @ApiModelProperty("用户姓名")
    private String name;

    @NotNull
    @ApiModelProperty("用户年龄")
    private Integer age;

}

第二步:在须要校验的参数实体前添加@Valid注解,具体以下:

@PostMapping("/")
@ApiOperation(value = "建立用户", notes = "根据User对象建立用户")
public String postUser(@Valid @RequestBody User user) {
    users.put(user.getId(), user);
    return "success";
}

完成上面配置以后,启动应用,并用POST请求访问localhost:8080/users/接口,body使用一个空对象,{}。你能够用Postman等测试工具发起,也可使用curl发起,好比这样:

curl -X POST \
  http://localhost:8080/users/ \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 72745d04-caa5-44a1-be84-ba9c115f4dfb' \
  -H 'cache-control: no-cache' \
  -d '{
    
}'

不出意外,你能够获得以下结果:

{
    "timestamp": "2019-10-05T05:45:19.221+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotNull.user.age",
                "NotNull.age",
                "NotNull.java.lang.Integer",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                }
            ],
            "defaultMessage": "不能为null",
            "objectName": "user",
            "field": "age",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "NotNull.user.name",
                "NotNull.name",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "不能为null",
            "objectName": "user",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 2",
    "path": "/users/"
}

其中返回内容的各参数含义以下:

  • timestamp:请求时间
  • status:HTTP返回的状态码,这里返回400,即:请求无效、错误的请求,一般参数校验不经过均为400
  • error:HTTP返回的错误描述,这里对应的就是400状态的错误描述:Bad Request
  • errors:具体错误缘由,是一个数组类型;由于错误校验可能存在多个字段的错误,好比这里由于定义了两个参数不能为Null,因此存在两条错误记录信息
  • message:概要错误消息,返回内容中很容易能够知道,这里的错误缘由是对user对象的校验失败,其中错误数量为2,而具体的错误信息就定义在上面的errors数组中
  • path:请求路径

请求的调用端在拿到这个规范化的错误信息以后,就能够方便的解析并做出对应的措施以完成本身的业务逻辑了。

尝试一些其余校验

在完成了上面的例子以后,咱们还能够增长一些校验规则,好比:校验字符串的长度、校验数字的大小、校验字符串格式是否为邮箱等。下面咱们就来定义一些复杂的校验定义,好比:

@Data
@ApiModel(description="用户实体")
public class User {

    @ApiModelProperty("用户编号")
    private Long id;

    @NotNull
    @Size(min = 2, max = 5)
    @ApiModelProperty("用户姓名")
    private String name;

    @NotNull
    @Max(100)
    @Min(10)
    @ApiModelProperty("用户年龄")
    private Integer age;

    @NotNull
    @Email
    @ApiModelProperty("用户邮箱")
    private String email;

}

发起一个能够出发nameageemail都校验不经过的请求,好比下面这样:

curl -X POST \
  http://localhost:8080/users/ \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 114db0f0-bdce-4ba5-baf6-01e5104a68a3' \
  -H 'cache-control: no-cache' \
  -d '{
    "name": "abcdefg",
    "age": 8,
    "email": "aaaa"
}'

咱们将获得以下的错误返回:

{
    "timestamp": "2019-10-05T06:24:30.518+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.user.name",
                "Size.name",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                },
                5,
                2
            ],
            "defaultMessage": "个数必须在2和5之间",
            "objectName": "user",
            "field": "name",
            "rejectedValue": "abcdefg",
            "bindingFailure": false,
            "code": "Size"
        },
        {
            "codes": [
                "Min.user.age",
                "Min.age",
                "Min.java.lang.Integer",
                "Min"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                },
                10
            ],
            "defaultMessage": "最小不能小于10",
            "objectName": "user",
            "field": "age",
            "rejectedValue": 8,
            "bindingFailure": false,
            "code": "Min"
        },
        {
            "codes": [
                "Email.user.email",
                "Email.email",
                "Email.java.lang.String",
                "Email"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                },
                [],
                {
                    "defaultMessage": ".*",
                    "codes": [
                        ".*"
                    ],
                    "arguments": null
                }
            ],
            "defaultMessage": "不是一个合法的电子邮件地址",
            "objectName": "user",
            "field": "email",
            "rejectedValue": "aaaa",
            "bindingFailure": false,
            "code": "Email"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 3",
    "path": "/users/"
}

errors数组中的各个错误明细中,知道各个字段的defaultMessage,能够看到很清晰的错误描述。

Swagger文档中的体现

可能有读者会问了,个人接口中是定了这么多。上一篇教程中,不是还教了如何自动生成文档么,那么对于参数的校验逻辑该如何描述呢?

这里要分两种状况,Swagger自身对JSR-303有必定的支持,可是支持的并那么完善,并无覆盖全部的注解的。

好比,上面咱们使用的注解是能够自动生成的,启动上面咱们的实验工程,而后访问http://localhost:8080/swagger-ui.html,在Models不是,咱们能够看到以下图所示的内容:

其中:nameage字段相比上一篇教程中的文档描述,多了一些关于校验相关的说明;而email字段则没有体现相关校验说明。目前,Swagger共支持如下几个注解:@NotNull@Max@Min@Size@Pattern。在实际开发过程当中,咱们须要分状况来处理,对于Swagger支自动生成的能够利用原生支持来产生,若是有部分字段没法产生,则能够在@ApiModelProperty注解的描述中他,添加相应的校验说明,以便于使用方查看。

番外:也许你会有这些疑问

当请求参数校验出现错误信息的时候,错误格式能够修改吗?

答案是确定的。这里的错误信息实际上由Spring Boot的异常处理机制统一组织并返回的,咱们将在后面的教程中详细介绍,Spring Boot是如何统一处理异常返回以及咱们该如何定时异常返回。

spring-boot-starter-validation是必须的吗?

有读者以前问过,看到不少教程都写了还要引入spring-boot-starter-validation依赖,这个依赖究竟是否须要?(本篇中并无引入)

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

其实,只须要仔细看一下spring-boot-starter-validation依赖主要是为了引入了什么,再根据当前本身使用的Spring Boot版原本判断便可。实际上,spring-boot-starter-validation依赖主要是为了引入下面这个依赖:

<dependency>
   <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.14.Final</version>
    <scope>compile</scope>
</dependency>

咱们能够看看当前工程的依赖中是否有它,就能够判断是否还须要额外引入。在Spring Boot 2.1版本中,该依然其实已经包含在了spring-boot-starter-web依赖中,并不须要额外引入,因此您在本文中找不到这一步。

代码示例

本文的完整工程能够查看下面仓库中的chapter2-3目录:

若是您以为本文不错,欢迎Star支持,您的关注是我坚持的动力!

参考资料

  • [1] https://www.jianshu.com/p/554533f88370
  • [2] http://blog.didispace.com/spring-boot-starter-swagger-1.3.0/

欢迎关注个人公众号:程序猿DD,得到独家整理的学习资源和平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:didispace.com

相关文章
相关标签/搜索