专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

JSR303参数校验

早期的参数校验形式

在早期的时候,java的参数校验停留在获取参数之后在代码层面做校验,类似如下操作:

   @PostMapping("/test")
   public String test(@RequestBody TestRequest request) {
        if (StringUtils.isEmpty(request.getName())) {
            return "姓名不能为空";
        }
        if (request.getName().length() > 6) {
            return "姓名长度不能超过6";
        }
        if (StringUtils.isEmpty(request.getPhone())) {
            return "电话不能为空";
        }
        if (!isPhone(request.getPhone())) {
            return "电话号码格式不正确";
        }

        return "SUCCESS";
    }

    /**
     * 校验电话格式
     *
     * @param phone
     * @return
     */
    private boolean isPhone(String phone) {
        return true;
    }

如代码中看到,每一种校验规则都需要在代码里面提现,那么光是参数的校验也会耗费开发人员大量的精力。也许有人会说使用我也可以使用Assert来将一个校验从3行代码变成1行代码,但是即便如此,如果需要校验的参数非常多,并且校验的种类也非常多,那么开发效率也会大幅降低。

既然如此,有没有一种不需要在代码层面就可以解决掉这种痛苦的办法呢?当然是有的,再java6里面推出了一种规范:JSR-303,JSR是Java Specification Requests的缩写,意思是Java 规范提案,又叫做Bean Validation。JSR 303是Java为bean数据合法性校验提供的标准框架。Hibernate Validator是Bean Validation的参考实现

Bean Validation中内置的constraint

以下图片来自互联网

80_1.png

Hibernate Validator 附加的 constraint

80_2.png

使用JSR303规范来做参数校验

有了统一的参数校验规范,现在来对上面早期的参数校验方式做一个重构。

将请求类改造成如下所示:

package com.yezi.springboot219.controller.request;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 10:11
 */
@Data
public class TestRequest {

    @NotEmpty(message = "姓名不能为空")
    @Length(min = 1, max = 6, message = "姓名长度必须在1-6之间")
    private String name;

    @NotBlank(message = "电话号码不能为空")
    @Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
    private String phone;

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

    @NotNull(message = "年龄不能为空")
    @Max(value = 18,message = "年龄不能超过18")
    private Integer age;
}

去掉controller中对字段的代码层面校验

    @PostMapping("/test")
    public String test(@RequestBody @Valid TestRequest request) {
        return "SUCCESS";
    }

发送测试请求去下:

80_3.png

结果:

{
    "timestamp": "2020-06-08T02:59:38.463+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotEmpty.testRequest.name",
                "NotEmpty.name",
                "NotEmpty.java.lang.String",
                "NotEmpty"
            ],
            "arguments": [
                {
                    "codes": [
                        "testRequest.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "姓名不能为空",
            "objectName": "testRequest",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotEmpty"
        },
        {
            "codes": [
                "Pattern.testRequest.phone",
                "Pattern.phone",
                "Pattern.java.lang.String",
                "Pattern"
            ],
            "arguments": [
                {
                    "codes": [
                        "testRequest.phone",
                        "phone"
                    ],
                    "arguments": null,
                    "defaultMessage": "phone",
                    "code": "phone"
                },
                [],
                {
                    "defaultMessage": "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$",
                    "arguments": null,
                    "codes": [
                        "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$"
                    ]
                }
            ],
            "defaultMessage": "电话号码格式不正确",
            "objectName": "testRequest",
            "field": "phone",
            "rejectedValue": "12345678911",
            "bindingFailure": false,
            "code": "Pattern"
        },
        {
            "codes": [
                "Length.testRequest.name",
                "Length.name",
                "Length.java.lang.String",
                "Length"
            ],
            "arguments": [
                {
                    "codes": [
                        "testRequest.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                },
                6,
                1
            ],
            "defaultMessage": "姓名长度必须在1-6之间",
            "objectName": "testRequest",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "Length"
        }
    ],
    "message": "Validation failed for object='testRequest'. Error count: 3",
    "path": "/user/test"
}

根据结果可知,每个参数都得到了应有的校验,这里由于我们没有统一的异常处理,所以看起来返回结果不是很友好,通常情况下我们在项目中都会做统一的异常处理。再改造一下,代码如下所示:

package com.yezi.springboot219.handler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 11:10
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler");

    @ExceptionHandler
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public String defaultExceptionHandler(Throwable ex) {
        logger.error("error:", ex);
        return "FAIL";
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public String validationExceptionHandle(MethodArgumentNotValidException ex) {
        logger.error("error:", ex);
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
        List<String> msgList = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.toList());
        return String.join(",", msgList);
    }

}

再测试一下:

80_4.png

这样看起来就很清晰了。这里只是为了演示,没有做统一的返回结果封装,在实际开发中,通常会将返回的结果封装成json格式,不同的错误码对应不同的提示,方便前后端开发对接。这里就不再做进一步扩展。

@Valid与@Validated

这2个注解有什么区别呢?

首先看一下他们所属的包:

  • @Validated :org.springframework.validation.annotation.Validated
  • @Valid:javax.validation.Valid

可以看到@Validated属于spring,而@Valid属于javax。

但是在实际的基本使用中,这2者是没有区别的(注意这里说的是基本使用,也就是说,使用@Valid与@Validated是等价的)。

    @PostMapping("/test")
    public String test(@RequestBody @Validated TestRequest request) {
        return "SUCCESS";
    }

将注解替换一下,也能得到相同的结果:

80_5.png

看下2个注解的源码

  • @Valid
    @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface Valid {
    }

  • @Validated
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Validated {

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

    }

通过源码可以看到@Validated可以在类上面使用,并且多了一个value属性,那么这个value属性是干什么的?查看Spring Validation的文档可知,@Validated提供了一个分组功能,在校验参数时,可以根据不同的分组采用不同的校验机制。没有添加分组属性时,默认验证没有分组的验证属性。

分组校验

编写校验分组标识:

package com.yezi.springboot219.bean;

import javax.validation.groups.Default;

/**
 * @Description: 校验分组组
 * @Author: yezi
 * @Date: 2020/6/8 13:19
 */
public class ValidatedGroup {
    public interface  Add extends Default {}
    public interface  Delete extends Default {}
    public interface  Update extends Default {}
    public interface  Query extends Default {}
}

测试请求类:

package com.yezi.springboot219.controller.request;

import com.yezi.springboot219.bean.ValidatedGroup;
import lombok.Data;

import javax.validation.constraints.*;
import java.math.BigDecimal;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 13:25
 */
@Data
public class TestSaveRequest {

    @NotNull(groups = {ValidatedGroup.Update.class, ValidatedGroup.Delete.class}, message = "更新或者删除时id不能为空")
    private Long id;

    @NotBlank(groups = {ValidatedGroup.Update.class}, message = "更新时姓名不能为空")
    private String name;

    @NotNull(groups = {ValidatedGroup.Add.class}, message = "新增时余额不能为空")
    @Digits(integer = 10, fraction = 2)
    private BigDecimal balance;

    @NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时电话不能为空")
    @Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
    private String phone;

    @NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时邮件不能为空")
    @Email
    private String email;

}

根据我们编写的测试请求类型可预期:

  • 校验分组为Add时,会校验balance、phone、email三个请求字段
  • 校验分组为Update时,会校验id、name字段

测试如下:

   @PostMapping("/testAdd")
    public String testAdd(@RequestBody @Validated(value = ValidatedGroup.Add.class) TestSaveRequest request) {
        return "SUCCESS";
    }

    @PostMapping("/testUpdate")
    public String testUpdate(@RequestBody @Validated(value = ValidatedGroup.Update.class) TestSaveRequest request) {
        return "SUCCESS";
    }

testAdd结果:

80_6.png

testUpdate结果:

80_7.png

结果与预期完全符合。@Validated注解在分组校验时候,可以节省很多额外的开发,特别是当新增和更新时,一个需要传递主键一个不需要传递主键的情形,以前需要些一个AddRequest一个UpdateRequest,现在只有需要一个就够了。

嵌套校验

什么是嵌套校验呢?上代码:

package com.yezi.springboot219.controller.request;

import lombok.Data;

import javax.validation.constraints.NotNull;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 13:25
 */
@Data
public class TestNestRequest {

    @NotNull(message = "id不能为空")
    private Long id;

    @NotNull(message = "嵌套对象请求数据不能为空")
    private ItemRequest itemRequest;

}

package com.yezi.springboot219.controller.request;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 13:50
 */
@Data
public class ItemRequest {

    @NotEmpty(message = "name不能为空")
    private String name;
}

    @PostMapping("/testNest")
    public String testNest(@RequestBody @Valid TestNestRequest request) {
        return "SUCCESS";
    }

测试结果如下:

80_8.png

只校验了id,没有校验嵌套对象中的name属性,如果需要校验嵌套对象中的属性,需要改造一下TestNestRequest类,在itemRequest属性上加上@Valid注解,方能校验嵌套对象中的属性

package com.yezi.springboot219.controller.request;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

/**
 * @Description:
 * @Author: yezi
 * @Date: 2020/6/8 13:25
 */
@Data
public class TestNestRequest {

    @NotNull(message = "id不能为空")
    private Long id;

    @Valid
    @NotNull(message = "嵌套对象请求数据不能为空")
    private ItemRequest itemRequest;

}

测试结果:

80_9.png

一定要注意, 嵌套验证必须用@Valid注解。

文章永久链接:https://tech.souyunku.com/34116

未经允许不得转载:搜云库技术团队 » JSR303参数校验

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们