1.前言
何为参数校验?
在我们写项目时,在写controller时,多多少少会写过类似这样的代码
@RequestMapping("/{studentId}")
public ResponseEntity<String> queryInfo(@PathVariable("studentId") String sudentId){
if(!StringUtils.isEmpty(sudentId)){
//....
}
}
我们需要对前端传过来的数据进行校验,再进行业务操作…
或许你会想,校验就校验呗。但是如果我们需要验证的数据有很多呢,一个方法有十个,有100个这样的方法呢…就会导致我们在数据校验上浪费时间,显然我们并不希望这样。
在Controller层有时候需要对接口的输入参数进行校验,若是采用自身的校验逻辑代码来实现的话,会有一些弊端,一是会分散自己的注意力,不能让自己专心撰写业务逻辑代码;二是会让校验逻辑代码和业务逻辑代码产生耦合性,代码体积也比较臃肿。为了规避这种情况,我们可以采用Spring validation的Validated注解来完成接口参数校验的工作,下面举实例说明。
2.如何使用
2.1 添加maven依赖
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
2.2 在校验字段上添加校验注解
class Profile{
@NotNull(message = "字段值不能为空")
private String name;
@NotNull
private String sex;
@Max(value = 20,message = "最大长度为20")
private String address;
@NotNull
@Size(max=10,min=5,message = "字段长度要在5-10之间")
private String fileName;
@Pattern(regexp = "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.[a-zA-Z0-9]{2,6}$",message = "不满足邮箱正则表达式")
private String email;
@AssertTrue(message = "字段为true才能通过")
private boolean isSave;
@Future(message = "时间在当前时间之后才可以通过")
private Date date;
}
2.3 在Controller层使用@Validated进行验证
@RequestMapping("file/upload")
public void upload(@RequestPart("files") MultipartFile files, @Validated Profile profile, Errors error) throws IOException {
if(error.hasErrors()){
return;
}
files.transferTo(new File(files.getOriginalFilename()));保存文件
}
也可以直接在controller中进行校验
- Controller开启Validated校验
@RestController
@RequestMapping("/test")
@Validated
public class TestController{
...}
- 方法上直接校验参数
@GetMapping(value = "/method/{type}")
public ResponseModel test(
@Pattern(regexp = "[1,2]",message = "类型只能为1或2")@PathVariable(value = "type")String types,
@NotBlank(message="内容不能为空")String content,
@Length(max = 64,message = "长度最大为64")String title,
@NotEmpty(message = "集合不能为空")List<String> ids,
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$",message = "手机号格式有误") @NotBlank String mobile,
@Max(value = 100,message = "最大值为100") @Min(value = 1,message = "最小值为1")Integer intValue) {
...}
2.4 有哪些注解
@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(regex=,flag=) 被注释的元素必须符合指定的正则表达式
2.5 分组校验
介绍一下这样的场景:在对用户的帐号密码进行编辑保存以及新增是两种不一样的情况。
编辑修改->保存:只需要验证username与password是否符合条件即可,不需要验证id(因为在数据库中id已经存在)。
新增->保存:新增需要验证username与password是否符合条件,还要验证id。
这时候就用到groups分组分情况对Bean属性变量进行验证,也可以满足多验证。具体的需要一下两个步骤
第一步:创建分组接口类
分组接口类只是普通的接口类并没有多大意义,只是用来标识这个属性哪种情况下被验证,这类似于java.io.Serializable
public interface addUser{
}
public interface editUser{
}
第二步:Controller方法参数中增加xxx.class接口
在对新增的用户进行ID验证,增加@Validated({addUser.class})
接口类用来表示新增的User.getId()
需要验证。
@Controller
public class UserController {
@RequestMapping("/saveAdd")
public String saveAddUser(@Validated({
addUser.class}) User user, BindingResult result) {
if(result.hasErrors()) {
return "error";
}
return "success";
}
}
第三步:Bean中添加groups分组
在User实体类中添加groups分组@NotEmpty(groups={addUser.class})
与UserController中@Validated({addUser.class})
对应,说明在执行saveAddUser
新增用户的情况下,才对新增的用户id进行验证。
public class User {
//在分组addUser时,验证id不能为空,其他情况下不做验证
@NotEmpty(groups={
addUser.class})
private String id;
@NotEmpty(message = "用户名不能为空")
private String username;
@Size(min=6 ,max= 20 ,message = "密码最少6位,最高20位")
private String password;
......
}
以上三步就可以简单地完成分组验证,但是对分组验证补充一下三点:
- 不分配groups分组时,默认每次都需要验证
- 通过groups分组可以对同一个变量进行多个验证,如下代码
//对用户名进行两次不同情况的验证。
@NotEmpty(groups={
First.class})
@Size(min=1,max=10,groups={
Second.class})
public String username;
- 默认的情况下,不同的分组约束验证是无序的,但是在有些情况下验证的相互约束很重要(比如前一个组验证失败,后面的将不再验证等情况),所以groups分组的验证也有前后验证顺序。使用@GroupSequence注解进行排序。
/*
* 分组顺序接口类
*/
import javax.validation.GroupSequence;
//分组序列先Frist再Second
@GroupSequence({
First.class,Second.class})
public interface Group{
}
@Controller
public class UserController {
@RequestMapping("/saveAdd")
public String saveAddUser(@Validated({
Group.class}) User user, BindingResult result) {
if(result.hasErrors()) {
return "error";
}
return "success";
}
3.异常捕获
上面我们了解了如何进行数据校验,但是有一个问题,如果校验不通过,出现异常怎么办?我们想获取异常信息,然后告诉前端到底发生了怎么异常。
如何封装到一个JSON对象返回给前端呢?用一个案例来演示
- 添加测试Bean,测试类中定义几个字段,并且每个字段都做一定的限制
public class BeanValidation {
@Size(min=6,max=10)
private String field1;
@Range(min=0,max=1)
private Long field2;
@AssertFalse
private Boolean field3;
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
public Long getField2() {
return field2;
}
public void setField2(Long field2) {
this.field2 = field2;
}
public Boolean getField3() {
return field3;
}
public void setField3(Boolean field3) {
this.field3 = field3;
}
}
- 添加测试接口,接口中使用@Validated注解对参数进行合法性检查,如果参数合法,返回原始数据
@RequestMapping("globalexceptiontest")
public Object globalExceptionTest(@Validated @RequestBody BeanValidation data)
{
ResultMsg resultMsg = new ResultMsg(ResultStatusCode.OK.getErrcode(), ResultStatusCode.OK.getErrmsg(), data);
return resultMsg;
}
- 如果未添加全局异常处理,将会使用默认的异常处理,返回结果,如下图
返回的结果和自己的数据结构有很大的差异,对于前端处理返回结果也很麻烦
- 自定义参数异常返回的数据类
ArgumentInvalidResult
public class ArgumentInvalidResult {
private String field;
private Object rejectedValue;
private String defaultMessage;
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
public Object getRejectedValue() {
return rejectedValue;
}
public void setRejectedValue(Object rejectedValue) {
this.rejectedValue = rejectedValue;
}
public String getDefaultMessage() {
return defaultMessage;
}
public void setDefaultMessage(String defaultMessage) {
this.defaultMessage = defaultMessage;
}
}
- 添加全局异常处理类GlobalExceptionHandler
@ControllerAdvice
//如果返回的为json数据或其它对象,添加该注解
@ResponseBody
public class GlobalExceptionHandler {
//添加全局异常处理流程,根据需要设置需要处理的异常,本文以MethodArgumentNotValidException为例
@ExceptionHandler(value=MethodArgumentNotValidException.class)
public Object MethodArgumentNotValidHandler(HttpServletRequest request,
MethodArgumentNotValidException exception) throws Exception
{
//按需重新封装需要返回的错误信息
List<ArgumentInvalidResult> invalidArguments = new ArrayList<>();
//解析原错误信息,封装后返回,此处返回非法的字段名称,原始值,错误信息
for (FieldError error : exception.getBindingResult().getFieldErrors()) {
ArgumentInvalidResult invalidArgument = new ArgumentInvalidResult();
invalidArgument.setDefaultMessage(error.getDefaultMessage());
invalidArgument.setField(error.getField());
invalidArgument.setRejectedValue(error.getRejectedValue());
invalidArguments.add(invalidArgument);
}
ResultMsg resultMsg = new ResultMsg(ResultStatusCode.PARAMETER_ERROR.getErrcode(), ResultStatusCode.PARAMETER_ERROR.getErrmsg(), invalidArguments);
return resultMsg;
}
}
- 运行测试
- 当参数合法
- 当参数非法,返回与参数合法时有相同风格的错误信息
注意:
- 这里@ControllerAdvice注解标注,@ControllerAdvice是@Controller的增强版,一般与@ExceptionHandler搭配使用。
如果标注@Controller,异常处理只会在当前controller类中的方法起作用,但是使用@ControllerAdvice,则全局有效。
- @ExceptionHandler注解里面填写想要捕获的异常类class对象