在进行web开发时,对于前端发送的参数,需要进行合法性验证,虽然前端也会进行验证,但是用户仍然可以使用postman等工具或者手动拼接url等方式绕过验证,因此后端也需要验证
要验证参数,可以使用JSR303校验机制,他适用于大多数验证场景
使用JSR303校验,首先需要导入对应的starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
然后在实体类的字段上添加校验注解,并且可以使用注解的message属性指定错误信息
@Data public class Person { @NotNull(message = "名字不能为空") String name; @NotBlank(message = "地址不能为空") String address; @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "请填写正确的手机号") String phone; }
常用的校验注解有以下这些,覆盖了大多数的校验场景
validator内置注解:
@Null:被注释的元素必须为null
@NotNull:被注释的元素必须不为null
@AssertTrue:被注释的元素必须为true
@AssertFalse:被注释的元素必须为false
@Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min):被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past:被注释的元素必须是一个过去的日期
@Future:被注释的元素必须是一个将来的日期
@Pattern(value):被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的注解:
@Email:被注释的元素必须是电子邮箱地址
@Length:被注释的字符串的大小必须在指定的范围内
@NotEmpty:被注释的字符串的必须非空
@Range:被注释的元素必须在合适的范围内
@NotBlank:验证字符串非null,且长度必须大于0
关于@NotNull
,@NotEmpty
和@NotBlank
之间的区别如以下
@NotNull
适用于任何类型,被标注的元素必须不能为null@NotEmpty
适用于String类型,Map类型或者数组,不能为null,且长度必须大于0@NotBlank
只能用于String类型,不能为null,且调用trim()后,长度必须大于0添加完需要的校验注解之后,在请求处理方法中的接收的请求参数上添加@Validated
注解,即可开启校验功能
@RestController public class MyController { @RequestMapping("/check") public String check(@Validated Person person) { return "checked"; } }
@Validated
也可以用于校验Bean赋值,将注解添加在Bean的类名之上即可
@Component @Validated public class Person { @NotNull(message = "名字不能为空") String name; @NotBlank(message = "地址不能为空") String address; @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "请填写正确的手机号") String phone; }
启动项目,发送请求到测试接口
发送一个带正确参数的请求,可以通过校验
再发送一个缺少name属性的请求,请求没有通过
查看后台发现被校验拦截,同时打印了错误信息
如果需要处理校验错误的信息,可以在被校验的参数之后添加一个BindingResult
类的参数,与被校验的参数需要写在一起,然后使用BindingResult
对象的方法判断是否校验错误和获取错误信息
现在我们在发送校验错误的时候将错误信息收集,然后在校验错误的时候将错误信息返回给前端
@RestController public class MyController { @RequestMapping("/check") public String check(@Validated Person person, BindingResult result) { HashMap<String, String> map = new HashMap<>(); if (result.hasErrors()) { result.getFieldErrors().forEach(error -> map.put(error.getField(), error.getDefaultMessage())); return map.toString(); } return "checked"; } }
再次测试接口
如果我们不想在每一个接口上都编写校验错误处理逻辑,我们可以使用统一异常处理机制,编写一个全局异常处理器,绑定BindException
异常,在处理方法中处理校验错误既可
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) public String handleRunTimeException(BindException e) { HashMap<String, String> map = new HashMap<>(); e.getFieldErrors().forEach(error -> map.put(error.getField(), error.getDefaultMessage())); return map.toString(); } }
关于统一异常处理的详细使用,可以参考我的这篇文章:全局异常处理
有时候我们并不是每个接口都需要校验所有的字段,比如新增接口和更新接口需要校验的字段有可能是不同的,这时候可以使用校验分组功能
首先定义两个空的接口,并且继承Default
接口
新增分组
public interface AddPerson extends Default { }
更新分组
public interface UpdatePerson extends Default { }
然后在校验注解的groups属性中绑定分组,这个属性是一个数组,如果有多个分组需要校验该字段可以用数组的方式表示
由于接口都继承了Default
接口,在没有指定分组的情况下默认所有分组都要校验
@Data public class Person { @NotNull(message = "名字不能为空", groups = {AddPerson.class, UpdatePerson.class}) String name; @NotBlank(message = "地址不能为空", groups = AddPerson.class) String address; @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "请填写正确的手机号") String phone; }
在@Validated
注解指定该次校验属于哪个分组,这样就只会校验该分组下的字段,注意如果这时候不指定分组,则指定了分组的字段都不会校验
@RestController public class MyController { @RequestMapping("/add") public String addUser(@Validated(value = AddPerson.class) Person person, BindingResult result) { HashMap<String, String> map = new HashMap<>(); if (result.hasErrors()) { result.getFieldErrors().forEach(error -> map.put(error.getField(), error.getDefaultMessage())); return map.toString(); } return "checked"; } @RequestMapping("/update") public String updateUser(@Validated(value = UpdatePerson.class) Person person, BindingResult result) { HashMap<String, String> map = new HashMap<>(); if (result.hasErrors()) { result.getFieldErrors().forEach(error -> map.put(error.getField(), error.getDefaultMessage())); return map.toString(); } return "checked"; } }
我们测试更新接口,不带address属性,而更新分组不需要校验这个属性,所以可以通过校验
然后发送一个不带name属性的请求,这个属性所有的分组都要校验,所以校验不通过
没有分组的phone属性也会被校验