springboot进行参数校验
文章目录
- 前言
- 普通的逻辑代码校验
- PathVariable 路径变量校验
- JSR规范是什么?
- springboot-JSR303参数校验
- 引入参数校验依赖
- 官网提供的校验注解
- 方法上对某参数校验
- 全局异常拦截错误信息
- javaBean 参数校验
- 全局异常捕获升级
- 参数分组校验隔离
- 为什么校验要分组隔离?
- 如何选择触发不同的校验分组?
- List< Bean>类型参数校验
- 参数校验-自定义注解
- 定义校验注解
- 定义校验规则
- 实践测试
- 项目源码
前言
实际项目中,通常会对用户的请求参数进行校验…而参数校验呢,又分为前端校验,后端校验…
本文呢,就来讲一讲咱们后端常用的参数校验方式
普通的逻辑代码校验
最传统的后端参数校验,则是接受到参数后,根据一定规则去比对,判断是否符合操作参数要求…
代码示例如下:
public AjaxResult addUser(User user) {
if (user.getAge()<18) {
throw new CommonException("未成年人,不允许添加");
}
if (user.getAge()>65) {
throw new CommonException("超过法定退休年龄");
}
//TODO 这里假设不符合规范...
if (user.getAccount().equals("asdasd")) {
throw new CommonException("账户不符合规范");
}
return AjaxResult.success(true);
}
这样的代码,按照逻辑处理来说,是没有问题的…
但按照代码简介性处理方式等,则显得代码非常冗余,如同胶水代码…最终要得是,如果其他方法也需要判断user
信息的合理的话,又需要在写(copy)一份…
这种参数校验方式呢,代码丑陋冗余,写法low…不是特别推荐…
PathVariable 路径变量校验
通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中,其也可以对参数类型进行校验…如果参数类型不匹配,则会抛出404异常…
代码示例如下:
@RequestMapping("/add/user/{name}/{age}")
public AjaxResult demo(@PathVariable("age") Integer age, @PathVariable("name") String name) {
//TODO 逻辑处理...
return AjaxResult.success(age + name);
}
这种方式呢,仅仅只能校验传入参数是否符合方法规范(参数数量、参数类型),但如果不符合规范,其异常信息404又不太好处理,容易与实际意义上的url不存在混淆…进而忽视了参数校验的意义…
所以这种方式呢,我们仅仅还是将其作为一个restful
风格参数传递方式较好…作为参数校验方面入手,不太符合其原本PathVariable
的初衷…也不是特别推荐…
接下来…进入重头戏…
参数校验,既然有需求,那么肯定就有实现…
JSR规范是什么?
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求
那么我们项目中如何使用JSR呢?
使用JSR303
即可…JSR-303
是 JAVA EE 6
中的一项子规范,叫做 Bean Validation
,即对象校验…
我们接下来就演示在spring 项目中 使用jsr303规范 实现对参数的校验…
springboot-JSR303参数校验
引入参数校验依赖
<!--jsr3参数校验器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
接下来,就可以快乐的使用啦!
其提供了许多的参数校验注解,我们只需要在需要校验的参数地方打上注解即可使用,当入参不符合注解规范,则会抛出异常…
官网提供的校验注解
例如下方注解…
约束注解 | 作用说明 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotBlank | 被注释的字符串的必须非空 |
@NotEmpty | 被注释的字符串、集合、Map、数组必须非空 |
@Email | 被注释的元素必须是电子邮箱地址 |
@AssertFalse | 被注释的元素必须为 false |
@AssertTrue | 被注释的元素必须为 true |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
… | … |
可以查阅hibernate-validator
和validator
相关资料…
方法上对某参数校验
需要注意的是,在方法参数上使用校验注解,必须在其类上打上@Validated
注解
@RequestMapping("/add/demo")
public AjaxResult demo(@Max(value =65,message = "最大年龄为65")
@Min(value = 18,message = "最小年龄为18")
Integer age,
@NotBlank(message = "年龄不能为空")
String name) {
//TODO 逻辑处理...
return AjaxResult.success(age + name);
}
结果如下…
这个错误信息,对用户来说,体验性是不好的,所以呢,我们通常呀结合RestControllerAdvice
+ExceptionHandler
对异常进行拦截…统一以json形式将错误进行返回…
全局异常拦截错误信息
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
/**
* @author leilei
* @version 1.0
* @date 2020/10/22 21:21
* @desc: 增强类 可以自己在 @ExceptionHandler(xxx.class) 定义异常类型 这样就会以json形式展示错误信息
*/
@Slf4j
@RestControllerAdvice
public class ExceptionsHandler {
/** 参数不符合规范异常 */
@ExceptionHandler({ConstraintViolationException.class})
public AjaxResult ConstraintViolationException(ConstraintViolationException ex) {
log.error("参数不符合规范异常"+ex.getMessage());
return AjaxResult.error(ex.getMessage());
}
}
javaBean 参数校验
实际开发中,可能涉及到传入参数为一个java bean 对象…可能是以JSON形式接受,可能直接是以对象参数接受…但,我们需要对参数对象进行校验…
那么,这个时候呢,我们就可以像Swagger
一般,在需要校验的实体类的字段上,添加校验注解,然后,在入参方法中加上@Validated
注解即可…
@Data
public class User {
@NotBlank(message = "账户不能为空")
@Length(min = 11,max = 11,message = "账户长度限定为十一位")
private String account;
@NotBlank(message = "用户密码不能为空")
@Length(min = 6,max = 12,message = "密码长度不正确5<密码<12")
private String passWord;
@Max(value = 65, message = "age应<=65")
@Min(value = 18, message = "age应=>18")
private Integer age;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮件格式不对")
private String email;
private Boolean sex;
}
接口方法
注意,校验对象参数必须添加 @Validated
或@Valid
注解
二者区别:
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
@RequestMapping("/addUser")
public AjaxResult add(@Validated @RequestBody User user) {
return userService.addUser(user);
}
那么这个时候呢,网上一大堆,都需要在接口方法中加上BindingResult
才能捕获到异常…这种做法…
说实话,可以,是可以…但方式有点憨憨…
为什么不全局异常捕获呢????好好地接口要啥参数填啥参数就行了,搞个额外参数干啥呢???
全局异常捕获升级
@ExceptionHandler(value = {MethodArgumentNotValidException.class , BindException.class})
public AjaxResult runtimeExceptionHandler(Exception ex) {
log.error("参数校验异常:{}", ex.getMessage());
BindingResult bindingResult = null;
if (ex instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException)ex).getBindingResult();
} else if (ex instanceof BindException) {
bindingResult = ((BindException)ex).getBindingResult();
}
StringBuilder errorMessage = new StringBuilder();
if (bindingResult != null) {
bindingResult.getFieldErrors().parallelStream()
.forEach(e -> errorMessage.append(e.getDefaultMessage()).append(" !"));
}
return AjaxResult.error(errorMessage.toString(), 400);
}
参数分组校验隔离
为什么校验要分组隔离?
开发中,在新增或者修改用户传递的对象信息可能校验的规则不同
例如:新增时要求ID为空,修改时要求ID 不等于空…
那么这个时候,就需要对校验注解进行分组隔离了…即新增走新增校验规则,修改时走修改校验规则…
从我们使用的注解中可以看到,其还能设置一个group
属性,默认为{},这个就是参数校验分组,我们之前不设置group属性,那么在使用到这个javabean的时候,其作用在参数上的所有校验注解都会生效,
所以呢,咱们可以对我们设置的参数校验注解进行分组设置…
例如这样…
我们对User中的ID 参数校验注解进行分组,当新增时,ID必须为null,当修改时ID 必须不能为Null…
@Data
public class User {
@Null(message = "ID必须为null",groups = User.Default.class)
@NotNull(message = "ID不能为null",groups = User.Update.class)
private Integer id;
@NotBlank(message = "账户不能为空",groups = User.Default.class)
@Length(min = 11,max = 11,message = "账户长度限定为十一位",groups = User.Default.class)
private String account;
@NotBlank(message = "用户密码不能为空",groups = User.Default.class)
@Length(min = 6,max = 12,message = "密码长度不正确5<密码<12",groups = User.Default.class)
private String passWord;
@Max(value = 65, message = "age应<=65",groups = User.Default.class)
@Min(value = 18, message = "age应=>18",groups = User.Default.class)
private Integer age;
@NotBlank(message = "邮箱不能为空",groups = User.Default.class)
@Email(message = "邮件格式不对",groups = User.Default.class)
private String email;
private Boolean sex;
public interface Default {
}
public interface Update {
}
}
如何便是完成了参数注解分组校验隔离了…
需要注意的是:环境名设置必须在当前实体类采用内部接口形式否则会报错
如何选择触发不同的校验分组?
依然使用Validated
注解,其注解可以指定Value 属性…value指明注解环境即可…
例如我这里的…
@RequestMapping("/addUser")
public AjaxResult add(@Validated(value = User.Default.class) @RequestBody User user) {
return userService.addUser(user);
}
如此,便指明了参数校验环境分组为Default…那么,我添加的ID字段 校验的@Null
则会生效… 以及之前的字段校验注解也会生效
新增测试 则会使用User.Default.class
分组的校验分组
修改测试 则会使用User.Update.class
校验分组,目前来说,Update分组,只有ID不为空校验…
@RequestMapping("/editUser")
public AjaxResult update(@Validated(value = User.Update.class) @RequestBody User user) {
return userService.update(user);
}
List< Bean>类型参数校验
即有时候,接口穿的参数可是一个Json集合…我们有需要对JSON集合中的实体类的属性做校验…这个情况,如下进行呢?
在原本User参数校验的情况下,咱们来辨析controller接口试一试
@RequestMapping("/addUser/batch")
public AjaxResult addBatch(@RequestBody @Validated(value =User.Default.class) List<User> users) {
//TODO 业务路基操作
return AjaxResult.success();
}
居然直接新增成功了!!!这说明,参数校验根本没有生效啊!!!
解决办法:
自定义一个POJO实体类…实现LIst接口…我们将传递过来的集合实体泛型作为我们的POJO泛型…
例如下边这样:
@Data
public class ValidResultList<E> implements List<E> {
@Valid
private List<E> list = new ArrayList<>();
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index,c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
return list.set(index,element);
}
@Override
public void add(int index, E element) {
list.add(index,element);
}
@Override
public E remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex,toIndex);
}
}
controller接口
将我们编写的POJO 类作为参数
public AjaxResult addBatch(@RequestBody @Validated(value =User.Default.class) ValidResultList<User> users) {
//TODO 业务路基操作
return AjaxResult.success();
}
如此一来,便可成功捕获道字段不匹配的信息了!!…但是呢,也存在一个问题,,,其提示参数没有隔离开来…
参数校验-自定义注解
可能内置的校验注解无法符合我们的业务需求,这个时候呢,我们便可以自定义注解…
自定义校验注解呢 分为两个步骤:
#1.定义注解
#2.定义校验规则类
比如我们查看源码:@Null
注解,其注解类中包含了这些信息…
那么,我们照猫画虎,一样来根据自己的业务定义一个校验注解…
比如说,校验字段存在于我们的字典表中…
定义校验注解
首先,咱们需要定义一个注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
//指定作用字段上以及方法参数上
@Target({ElementType.PARAMETER,ElementType.FIELD})
//指定校验规则类
@Constraint(validatedBy = HasDictionaryCheck.class)
public @interface HasDictionary {
//默认提示信息
String message() default "字典表中该值不存在";
//默认分组
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注解有了,那的有校验规则吧!不然咋知道其符不符合规范呢?所以啊,下一步是定义该注解的校验规则类
定义校验规则
然后,我们需要定义注解对应的校验规则类
重点:
校验类需实现ConstraintValidator
接口
两个泛型 前一个为要校验的注解,后一个为校验值类型
public class HasDictionaryCheck implements ConstraintValidator<HasDictionary,Object> {
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//TODO 业务逻辑
return value.equals("leilei");
}
}
由于本Demo 暂写死校验逻辑 期望打上注解的值 传参为leilei
…否则校验失败…
如此,自定义校验注解便完成了…咱们来测试一波…
咱们需要在原来的实体类中打上校验注解
由于调用的接口指定了校验分组为User.Default.class
分组,所以咱们新的注解也要指定一下分组,其才可生效…
测试
如果,咱们想自己手动约束其传入参数…例如这样…根据自己填入的规则限定传入参数
那么,咱们依然可以照猫画虎,定义一个校验注解…
比如,要求前端传过来的值必须包含在我们后端设置的值之中
注解:
/**
* @author lei
* @version 1.0
* @date 2020/10/24 17:04
* @desc 自定义值 参数校验 例如参数必须是(张三,李四,王五)中所包含的值
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Constraint(validatedBy = HasContainCheck.class)
public @interface HasContain {
//自己设置的校验值
String values();
//默认提示信息
String message() default " ";
//默认分组
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
校验规则
public class HasContainCheck implements ConstraintValidator<HasContain, Object> {
private String values;
/**
* 此方法是在加载该校验注解最先执行的方法,可在内做逻辑操作处理......
*
* @param hasContain
*/
@Override
public void initialize(HasContain hasContain) {
this.values = hasContain.values();
}
/**
* 参数校验规则
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
String[] parameters = values.split(",");
long count = Arrays.stream(parameters)
.filter(value::equals)
.count();
return count > 0;
}
}
实践测试
自定义参数校验注意事项:
我们自定义校验类在实现了ConstraintValidator
接口后,校验类实际交由了Spring来管理,我们是可以在类中直接注入spring容器中其他bean的,我们在做校验业务逻辑的时候,可以充分利用这一点,来壮大我们的自定义校验注解
项目源码