在一个完整的前后端项目中,为了确保用户输入的数据是想要的格式的数据,或者说避免他人通过某种手段获取到接口然后进行非法的数据请求。所以无论是前端还是后端都需要进行数据校验。
这时候有人可能就会说了,前端进行数据校验不就行了吗,他输入的数据不对,就不会发送请求给后端,这样不久安全了吗?
乍一听很有道理,但是万一那个用户不是小白,是一个程序猿呢,故意输入正确的数据,然后利用浏览器的调试工具然后获得到我们的接口,再利用Postman这样的测试工具对我们的接口进行数据交互,那不相当于接口完全暴露给他了吗
所以啊,后端进行数据的检验,是很有必要滴
下面模仿一个业务场景,我们需要对一个学生类进行“增删改查”的操作
由于这里是对接口数据的检验,所以就不对数据库进行操作了,只是简单模仿一下场景
学生类
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);
}
}
如果不适用任何工具类,手搓验证的话,就会像下面一样复杂
@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工具类替换上面的手搓,也会出现很多重复的代码,这样是很不友好的
JSR303是一套JavaBean参数校验标准,其定义了很多校验注解,我们可以使用在实体类使用注解来对对象的成员变量进行参数校验,大大减少了如上所示的繁琐的数据校验
以上面的更新学生信息为例,先导入依赖(下面那个是springboot集成的,用哪一个都行)
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>2.0.1.Finalversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
<version>2.3.7.RELEASEversion>
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
而控制台打印告诉我们不能为null
这样数据校验就成功了,有人获取就疑惑这里的提示信息是在哪来的呢?
其实吧,它是在ValidationMessages_zh.properties中已经写好的了
我们也可以自定义提示信息,如下所示
@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;
}
上面虽然完成了对数据的校验,但是它的提示只会在控制台输出,这样前端不能在后端检验完成之后返回相应的响应
@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);
}
OK!!!!!这样就能让前端知道后端的检验结果了
但是这样还没结束,这样每个接口都要写一遍这样的代码,代码重复度太多。
由上面就知道了,当检验数据不通过的时候,就会报异常,前端得到的状态码是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);
}
大功告成!!!!!
当我们进行新增操作的时候会发现学生ID并不能自定义,所以上面的数据校验只适用于修改操作
那有人会说,在id加一个@Null注解不就行了吗,但是既有@NotNull注解又有@Null注解,程序也不知道什么时候要为null,什么时候为Null呀
所以,就出现了分组校验,可以规定哪一个接口对应哪一种数据校验
比如现在有修改和新增,那么就分成两组,修改组为UpdateGroup,新增组为AddGroup
要实现分组校验,首先先创建两个接口——UpdateGroup和AddGroup,接口里面不需要写任何东西
然后修改学生实力类代码,给它们分组
@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);
}
}
测试接口,大功告成!!!!
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;
}
当然这个message我们也可以写在配置文件中,让它自己来读
首先新建一个ValidationMessages.properties文件,一定要是这个名字
com.example.jsr303.validation.StatusValue.message=学生状态必须提交指定值
然后执行代码,把实体类的message删除掉,执行代码,也是可以的
这里面列出一下常用的校验注解
这里常用的校验注解内容是参考https://blog.csdn.net/junR_980218/article/details/124590311