循序渐进了解如何使用JSR303进接口数据校验

循序渐进了解如何使用JSR303进接口数据校验

1. 引言

在一个完整的前后端项目中,为了确保用户输入的数据是想要的格式的数据,或者说避免他人通过某种手段获取到接口然后进行非法的数据请求。所以无论是前端还是后端都需要进行数据校验。

这时候有人可能就会说了,前端进行数据校验不就行了吗,他输入的数据不对,就不会发送请求给后端,这样不久安全了吗?

乍一听很有道理,但是万一那个用户不是小白,是一个程序猿呢,故意输入正确的数据,然后利用浏览器的调试工具然后获得到我们的接口,再利用Postman这样的测试工具对我们的接口进行数据交互,那不相当于接口完全暴露给他了吗

所以啊,后端进行数据的检验,是很有必要滴


2. 数据准备

下面模仿一个业务场景,我们需要对一个学生类进行“增删改查”的操作

由于这里是对接口数据的检验,所以就不对数据库进行操作了,只是简单模仿一下场景

学生类

public class Student {
    /**
     * 编号id
     */
    private Integer id;
    /**
     * 学生姓名
     */
    private String name;
    /**
     * 学生年龄
     */
    private Integer age;
    /**
     * 学生电话
     */
    private String phone;
    /**
     * 学生状态:1表示正常,0表示已经退学
     */
    private Integer status;
}

统一返回结果

@Data
public class R extends HashMap<String, Object>  {

    /**
     * 成功
     */
    public static R ok(String msg){
        R r = new R();
        r.put("code", 0);
        r.put("msg", msg);
        return r;
    }

    /**
     * 失败
     */
    public static R error(String msg){
        R r = new R();
        r.put("code", 1);
        r.put("msg", msg);
        return r;
    }
    
      /**
     * 重写put方法,使得返回值为R
     */
    @Override
    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }
    
}

StudentController,进行数据校验的较多的是更新和保存

@RestController
@RequestMapping("/student")
public class StudentController {
    @PostMapping("update")
    public R update(@RequestBody Student student){
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("更新成功").put("student", student);
    }

    @PostMapping ("save")
    public R save(@RequestBody Student student){
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("新增成功").put("student", student);
    }
}

3. 比较“笨”的数据校验

如果不适用任何工具类,手搓验证的话,就会像下面一样复杂

@RestController
@RequestMapping("/student")
public class StudentController {
    @PostMapping("update")
    public R update(@RequestBody Student student) {
        if (student == null) {
            return R.error("参数错误");
        } else if (student.getId() == null){
            return R.error("学生编号不能为空");
        }else if (student.getName() == null || student.getName() == "" || student.getName().contains(" ")) {
            return R.error("请输入正确的姓名");
        } else if (student.getAge() < 0 || student.getAge() == null) {
            return R.error("请输入正确的年龄");
        } else if (student.getPhone() == null || student.getPhone() == "" || student.getPhone().contains(" ")) {
            return R.error("请输入正确的电话号码");
        }
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("更新成功").put("student", student);
    }

    @PostMapping("save")
    public R save(@RequestBody Student student) {
        if (student == null) {
            return R.error("参数错误");
        } else if (student.getId() != null){
            return R.error("新增不能指定学生编号");
        }else if (student.getName() == null || student.getName() == "" || student.getName().contains(" ")) {
            return R.error("请输入正确的姓名");
        } else if (student.getAge() < 0 || student.getAge() == null) {
            return R.error("请输入正确的年龄");
        } else if (student.getPhone() == null || student.getPhone() == "" || student.getPhone().contains(" ")) {
            return R.error("请输入正确的电话号码");
        }
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("新增成功").put("student", student);
    }
}

就算使用MP带的ObjectUtils工具类替换上面的手搓,也会出现很多重复的代码,这样是很不友好的


4. 使用JSR303进行数据校验

JSR303是一套JavaBean参数校验标准,其定义了很多校验注解,我们可以使用在实体类使用注解来对对象的成员变量进行参数校验,大大减少了如上所示的繁琐的数据校验

以上面的更新学生信息为例,先导入依赖(下面那个是springboot集成的,用哪一个都行)

<!--校驗依賴-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>

对学生实体类的代码修改如下

@Data
public class Student {
    /**
     * 编号id
     * 使用JSR-303校验id不能为null
     */
    @NotNull()
    private Integer id;
    /**
     * 学生姓名
     * 使用JSR-303校验name不能为null并且不能是空字符串和空格
     */
    @NotBlank()
    private String name;
    /**
     * 学生年龄
     * 使用JSR-303校验name不能为null并且大于等于0
     */
    @NotNull()
    @Min(value = 0)
    private Integer age;
    /**
     * 学生电话
     * 使用JSR-303校验phone必须是0-9组成的字符串
     */
    @NotBlank
    @Pattern(regexp = "^[0-9]*$")
    private String phone;
    /**
     * 学生状态:1表示正常,0表示已经退学
     */
    private Integer status;
}

然后这里就添加了数据校验规则,添加完校验规则之后,还需要在controller添加@Valid注解

@PostMapping("update")
public R update(@Valid @RequestBody Student student) {
    /**
     * 对应的对数据库进行操作
     */
    return R.ok("更新成功").put("student", student);
}

用postman测试接口,出现400

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeNfJikF-1659854849337)(SpringBoot 接口数据校验.assets/image-20220806221053184.png)]

而控制台打印告诉我们不能为null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-30V8mliE-1659854849338)(SpringBoot 接口数据校验.assets/image-20220806221130436.png)]

这样数据校验就成功了,有人获取就疑惑这里的提示信息是在哪来的呢?

其实吧,它是在ValidationMessages_zh.properties中已经写好的了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kS0HEdCB-1659854849339)(SpringBoot 接口数据校验.assets/image-20220806222358876.png)]

我们也可以自定义提示信息,如下所示

@Data
public class Student {
    /**
     * 编号id
     * 使用JSR-303校验id不能为null
     */
    @NotNull(message = "学生编号不能为null")
    private Integer id;
    /**
     * 学生姓名
     * 使用JSR-303校验name不能为null并且不能是空字符串和空格
     */
    @NotBlank(message = "请输入正确的姓名")
    private String name;
    /**
     * 学生年龄
     * 使用JSR-303校验name不能为null并且大于等于0
     */
    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄必须大于等于0")
    private Integer age;
    /**
     * 学生电话
     * 使用JSR-303校验phone必须是0-9组成的字符串
     */
    @NotBlank
    @Pattern(regexp = "^[0-9]*$", message = "联系电话必须由0-9数字组成")
    private String phone;
    /**
     * 学生状态:1表示正常,0表示已经退学
     */
    private Integer status;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lKFrxA2-1659854849340)(SpringBoot 接口数据校验.assets/image-20220806222528879.png)]


5. 将数据检验不通过的信息返回给前端

上面虽然完成了对数据的校验,但是它的提示只会在控制台输出,这样前端不能在后端检验完成之后返回相应的响应

@PostMapping("update")
public R update(@Valid @RequestBody Student student, BindingResult result) {
    if (result.hasErrors()){
        Map<String, String> map = new HashMap<>();
        List<FieldError> fieldErrors = result.getFieldErrors();
        fieldErrors.forEach((fieldError -> {
            String field = fieldError.getField();
            String message = fieldError.getDefaultMessage();
            map.put(field, message);
        }));
        return R.error("参数错误").put("data", map);
    }
    /**
     * 对应的对数据库进行操作
     */
    return R.ok("更新成功").put("student", student);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TwIdBGW3-1659854849340)(SpringBoot 接口数据校验.assets/image-20220806223733953.png)]

OK!!!!!这样就能让前端知道后端的检验结果了

但是这样还没结束,这样每个接口都要写一遍这样的代码,代码重复度太多。


6. 集中处理异常

由上面就知道了,当检验数据不通过的时候,就会报异常,前端得到的状态码是400

这样,我们集中处理异常,然后将数据检验不通过的提示提取出来,再返回给前端,这样就能用一个类解决上面出现的问题了

package com.example.jsr303.exception;

import com.example.jsr303.vo.R;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @ClassName ExceptionControllerAdvice
 * @Description TODO
 * @Author kang
 * @Date 2022/8/6 下午 10:43
 * @Version 1.0
 */
@RestControllerAdvice
public class ExceptionControllerAdvice {
    /**
     * 处理特定异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        Map<String, String> map = new HashMap<>();
        if (result.hasErrors()) {
            List<FieldError> fieldErrors = result.getFieldErrors();
            fieldErrors.forEach((fieldError -> {
                String field = fieldError.getField();
                String message = fieldError.getDefaultMessage();
                map.put(field, message);
            }));
        }
        return R.error("数据检验不通过").put("data", map);
    }


    /**
     * 处理其他异常
     *
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    public R handleException() {

        return R.error("服务器内部异常");
    }
}

这里使用了RestControllerAdvice来处理,RestControllerAdvice是全局接口处理异常的类,其是由ControllerAdvice和@ResponseBody组合成的,规定了返回数据类型是json

个人赶紧这种处理全局异常的方法,非常像Controller层, @ExceptionHandler类似@PostMapping,用来区别异常

如上,如果是MethodArgumentNotValidException异常就会执行handleVaildException方法,然后给前端返回错误信息

这样Controller就能改回原来的样子

@PostMapping("update")
public R update(@Valid @RequestBody Student student) {
    /**
     * 对应的对数据库进行操作
     */
    return R.ok("更新成功").put("student", student);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lrYujzJR-1659854849341)(SpringBoot 接口数据校验.assets/image-20220806225149417.png)]

大功告成!!!!!


7. 使用JSR303进行分组校验

当我们进行新增操作的时候会发现学生ID并不能自定义,所以上面的数据校验只适用于修改操作

那有人会说,在id加一个@Null注解不就行了吗,但是既有@NotNull注解又有@Null注解,程序也不知道什么时候要为null,什么时候为Null呀

所以,就出现了分组校验,可以规定哪一个接口对应哪一种数据校验

比如现在有修改和新增,那么就分成两组,修改组为UpdateGroup,新增组为AddGroup

要实现分组校验,首先先创建两个接口——UpdateGroup和AddGroup,接口里面不需要写任何东西

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpsjT1xJ-1659854849341)(SpringBoot 接口数据校验.assets/image-20220807140604436.png)]

然后修改学生实力类代码,给它们分组

@Data
public class Student {
    /**
     * 编号id
     * 使用JSR-303校验id不能为null
     */
    @Null(message = "新增不能指定学生编号", groups = {AddGroup.class})
    @NotNull(message = "学生编号不能为null", groups = {UpdateGroup.class})
    private Integer id;
    /**
     * 学生姓名
     * 使用JSR-303校验name不能为null并且不能是空字符串和空格
     */
    @NotBlank(message = "请输入正确的姓名", groups = {AddGroup.class, UpdateGroup.class})
    private String name;
    /**
     * 学生年龄
     * 使用JSR-303校验name不能为null并且大于等于0
     */
    @NotNull(message = "年龄不能为空", groups = {AddGroup.class, UpdateGroup.class})
    @Min(value = 0, message = "年龄必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
    private Integer age;
    /**
     * 学生电话
     * 使用JSR-303校验phone必须是0-9组成的字符串
     */
    @NotBlank(message = "学生电话不能为空", groups = {AddGroup.class, UpdateGroup.class})
    @Pattern(regexp = "^[0-9]*$", message = "联系电话必须由0-9数字组成", groups = {AddGroup.class, UpdateGroup.class})
    private String phone;
    /**
     * 学生状态:1表示正常,0表示已经退学
     */
    private Integer status;
}

修改完实例类之后还需要修改controller层,给每个接口分配,让他们匹配每个类对应的数据校验规则

使用@Validated注解规定数据校验规则

@RestController
@RequestMapping("/student")
public class StudentController {
    @PostMapping("update")
    public R update(@Validated(UpdateGroup.class) @RequestBody Student student) {
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("更新成功").put("student", student);
    }

    @PostMapping("save")
    public R save(@Validated(AddGroup.class) @RequestBody Student student) {
        /**
         * 对应的对数据库进行操作
         */
        return R.ok("新增成功").put("student", student);
    }
}

测试接口,大功告成!!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cd8YK7O1-1659854849342)(SpringBoot 接口数据校验.assets/image-20220807141834743.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKScudJc-1659854849342)(SpringBoot 接口数据校验.assets/image-20220807141939084.png)]


8. 使用JSR303自定义校验注解

JSR303为我们提供了大量常用的校验注解,但是吧,总有些校验规则比较特殊

比如Student中的status成员变量,我们规定它只能是0或者1,但是没有这样现成的注解,所以我们可以自定义一个校验注解

我们可以模仿着现成的校验注解比如Null来自己写一个自定义的校验注解

下面这个是@Null的注解

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Null.List.class)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface Null {
    String message() default "{javax.validation.constraints.Null.message}";

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

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

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

然后我们自己新建一个注解StatusValue

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {}
)
public @interface StatusValue {
    String message() default "{com.example.jsr303.validation.StatusValue.message}";

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

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

    int[] values() default {};
}

其中values是我们需要规定的数据,比如我们规定是0和1,那么到时候就给注解的values赋值给0和1

这样还不行,还需要加一个校验器,也就是validatedBy里面的内容

下面是validatedBy点进去的发现我们需要一个ConstraintValidator类

@Documented
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraint {
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

说干就干,我们就新建一个实现ConstraintValidator接口的类

public class StatusValueConstraintValidator implements ConstraintValidator<StatusValue, Integer> {
    
    private Set<Integer> set = new HashSet<>();
    
    /**
     * 初始化
     * @param constraintAnnotation
     */
    @Override
    public void initialize(StatusValue constraintAnnotation) {
        int[] values = constraintAnnotation.values();
        for (int value : values) {
            set.add(value);
        }
    }

    /**
     * 判断是否校验成功
     * @param integer 需要检验的数据
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);
    }
}

首先,在初始化的时候,将我们将数据初始化,也就是比如说我们需要规定数据为0和1,然后就将数据0和1从constraintAnnotation拿那个values(这个values就是自己定义的,你定义了什么,这个名字就应该是什么)出来,然后放到集合中

然后在isValid方法进行校验**,第一个变量是需要校验的数据**,这里就校验一下这个数据是否在集合中就行了

接着在StatusValue注解中给它添上校验规则

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {StatusValueConstraintValidator.class}
)
public @interface StatusValue {
    String message() default "{com.example.jsr303.validation.StatusValue.message}";

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

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

    int[] values() default {};
}

这样自定义的校验注解的完成了,给Student的成员变量status测试一下试一试

@Data
public class Student {
    /**
     * 编号id
     * 使用JSR-303校验id不能为null
     */
    @Null(message = "新增不能指定学生编号", groups = {AddGroup.class})
    @NotNull(message = "学生编号不能为null", groups = {UpdateGroup.class})
    private Integer id;
    /**
     * 学生姓名
     * 使用JSR-303校验name不能为null并且不能是空字符串和空格
     */
    @NotBlank(message = "请输入正确的姓名", groups = {AddGroup.class, UpdateGroup.class})
    private String name;
    /**
     * 学生年龄
     * 使用JSR-303校验name不能为null并且大于等于0
     */
    @NotNull(message = "年龄不能为空", groups = {AddGroup.class, UpdateGroup.class})
    @Min(value = 0, message = "年龄必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
    private Integer age;
    /**
     * 学生电话
     * 使用JSR-303校验phone必须是0-9组成的字符串
     */
    @NotBlank(message = "学生电话不能为空", groups = {AddGroup.class, UpdateGroup.class})
    @Pattern(regexp = "^[0-9]*$", message = "联系电话必须由0-9数字组成", groups = {AddGroup.class, UpdateGroup.class})
    private String phone;
    /**
     * 学生状态:1表示正常,0表示已经退学
     */
    @StatusValue(values = {0, 1}, groups = {AddGroup.class, UpdateGroup.class}, message = "学生状态只能是0和1")
    private Integer status;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyV3a6R2-1659854849343)(SpringBoot 接口数据校验.assets/image-20220807144104362.png)]

当然这个message我们也可以写在配置文件中,让它自己来读

首先新建一个ValidationMessages.properties文件,一定要是这个名字

com.example.jsr303.validation.StatusValue.message=学生状态必须提交指定值

然后执行代码,把实体类的message删除掉,执行代码,也是可以的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oP2TkqhO-1659854849343)(SpringBoot 接口数据校验.assets/image-20220807144405459.png)]


9. 常用的校验注解

这里面列出一下常用的校验注解

这里常用的校验注解内容是参考https://blog.csdn.net/junR_980218/article/details/124590311

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxpgZVr0-1659854849344)(SpringBoot 接口数据校验.assets/1881324b017c4ddead9a9999b6ca484b.png)]


  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起名方面没有灵感

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值