大数跨境
0
0

必读!SpringBoot接口参数校验N种实用技巧大揭秘

必读!SpringBoot接口参数校验N种实用技巧大揭秘 Spring全家桶实战案例
2023-11-08
2
导读:必读!Springboot接口参数校验N种实用技巧大揭秘💯

环境:SpringBoot2.6.12

实际的开发工作中大部分的接口都是需要进行参数有效性校验的,参数可能是简单的基本数据类型,也可能是对象类型,基本上所有接收参数的接口都是需要对这些参数进行校验的,你对这些参数是怎么校验的?接下来带你一起见识下我在实际项目中都应用过哪些校验姿势!。该案例会详细介绍如下 7 方面的内容。

  1. 简单参数校验

  2. 参数校验分组

  3. 单个参数校验

  4. 嵌套参数校验

  5. 自定义工具类参数校验

  6. 国际化支持

  7. AOP 验证参数统一处理


在正式介绍主体内容前我们还是先要了解学习一些规范 JSR303

JSR 是什么?

JSR Java Specification Requests 的缩写,意思是 Java 规范提案。是指向 JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交 JSR,以向 Java 平台增添新的 API 服务JSR 已成为 Java 界的一个重要标准。JSR-303 JAVA EE 6 中的一项子规范,叫做 Bean ValidationHibernate Validator Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。相关注解如下:

Spring中提供了SpringValidation验证框架对参数的验证机制提供了@ValidatedSpring'sJSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),结合BindingResult对象可以直接获取错误信息。在本案中这两种是等效的,但是也有区别,在接下来的案例中将会说明。

1. 配置依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><scope>runtime</scope></dependency></dependencies>

org.aspectj 依赖是在最后我们要通过 AOP 技术来实现统一参数的校验。

2. 参数验证

  • 简单参数校验

public class Users {  @NotEmpty(message = "姓名必需填写")  private String name ;  @Min(value = 10, message = "年龄不能小于 10")  private Integer age ;  @Length(min = 6, max = 18, message = "邮箱介于 6 到 18 之间")  private String email ;  @NotEmpty(message = "电话必需填写")  private String phone ;  // 这里的2个接口在下面的案例中会使用到  public static interface G1 {}  public static interface G2 {}}

这里对需要校验的字段都应用了不同的注解来约束。接下来就是在Controller接口上添加相应的注解即可:

@ResponseBodypublic class UsersController extends BaseController {  @RequestMapping(value = "/valid/save1", method = RequestMethod.POST)  public Object save1(@RequestBody @Validated Users user, BindingResult result) {    Optional<List<String>> op = valid(result) ;    if (op.isPresent()) {      return op.get() ;    }    return "success" ;  }}public class BaseController {  protected Optional<List<String>> valid(BindingResult result) {    if (result.hasErrors()) {      return Optional.of(result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.toList())) ;    }    return Optional.empty() ;  }}

接收参数的 Users 对象前面要是用@Validated 注解,并且通过 BindingResult 来收集错误信息(可判断是否有错误信息);测试如下:

正确情况

  • 参数校验分组

有些时候我们这一个对象可能会应用到不同的场景,出现不同的校验规则该怎么做呢?这时候我们就可以应用分组功能,不同的应用场景指明不同的分组即可,开始撸。注意:JSR303 是没有分组功能的。

public class Users {  @NotEmpty(message = "姓名不能为空", groups = G1.class)  private String name ;  @Min(value = 10, message = "年龄不能小于 10", groups = G1.class)  @Min(value = 20, message = "年龄不能小于 20", groups = G2.class)  private Integer age ;  @Length(min = 6, max = 18, message = "邮箱介于 6 到 18 之间", groups = {G1.class, G2.class})  private String email ;  @NotEmpty(message = "电话必需填写")  private String phone ;  public static interface G1 {}  public static interface G2 {}}

这里不同的字段上加了 groups 属性,指明属于哪个分组。注意在该实体类中我们又定义了 2 个类 G1G2 就是为了分组用的(具体指明哪个分组)。接口处理:

@RequestMapping(value = "/valid/save1", method = RequestMethod.POST)public Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) {  Optional<List<String>> op = valid(result) ;  if (op.isPresent()) {    return op.get() ;  }  return "success" ;}@RequestMapping(value = "/valid/save2", method = RequestMethod.POST)public Object save2(@RequestBody @Validated(Users.G2.class) Users user, BindingResult result) {  Optional<List<String>> op = valid(result) ;  if (op.isPresent()) {    return op.get() ;  }  return "success" ;}

在这个两个接口中 @Validated(Users.G2.class)分别指明了自己的分组,接下来测试看看效果。

分组 G1 测试:

从这里返回的信息来看我们的 phone 虽然写了@NotEmpty 但是并没有起作用,因为我们并没有指明他的分组,并且接口上我们指明了是用 G1 分组。

分组 G2 测试:

在这个接口中发现 name 验证是 G1 的,所以这里不会进行校验,并且年龄的判断是不能小20 了。

  • 单个参数校验

单个参数的校验不需要实体对象,一般就是吧 JSR303 相关的注解直接应用到接口参数上即可。同时还需要在 Controller 类上添加@Validated 注解。

@Validatedpublic class UsersController extends BaseController {  @PackMapping("/valid/find")  public Object find(@NotEmpty(message = "参数 Id 不能为空") String id) {    return "查询到参数【" + id + "】" ;  }}

该接口中直接将注解应用到参数上。测试:

同时控制台会输出如下异常:

你也发现这种异常信息提示很不友好,接下来我们做个简单的局部异常处理

我们只需要在Controller添加如下方法即可:

@ExceptionHandler(ConstraintViolationException.class)@ResponseBodypublic Object ConstraintViolationExceptionHandler(ConstraintViolationException e) {  String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());  return message ;}

在该 Controller 中我们添加了一个异常处理句柄(简单吧将错误信息输出)。

  • 嵌套参数校验

在实际的工作中往往参数对象比这复杂的多,Users 对象中可能还嵌套有其他的对象,这个其他的对象也可能需要参数的校验。接下来我们就来看看这种嵌套参数是如何校验的。

public class Users {  @NotEmpty(message = "姓名不能为空", groups = G1.class)  private String name ;  @Min(value = 10, message = "年龄不能小于 10", groups = G1.class)  @Min(value = 20, message = "年龄不能小于 20", groups = G2.class)  private Integer age ;  @Length(min = 6, max = 18, message = "邮箱介于 6 到 18 之间", groups = {G1.class, G2.class})  private String email ;  @NotEmpty(message = "电话必需填写")  private String phone ;  @Valid  private Address address;}

注意:嵌套对象 Address 的校验需要在上面加@Valid 注解。

public class Address {  @NotEmpty(message = "地址信息必需填写")  private String addr ;}

测试接口:

@RequestMapping(value = "/valid/save3", method = RequestMethod.POST)public Object save3(@RequestBody @Validated Users user, BindingResult result) {  Optional<List<String>> op = valid(result) ;  if (op.isPresent()) {    return op.get() ;  }  return "success" ;}

接口上没有什么特别的与之前的一模一样。注意:这里的校验没有设定分组,所以校验时都是校验的没有设置分组的字段。

测试:

这里发现我们的地址信息根本就没有进行校验。接着我们吧参数变动下

参数中我们吧 address 字段设置上后 参数进行校验了。接下来修改 Users 实体,吧 Address 默认 new 出来再进行测试

@Validprivate Address address = new Address();

发现即便我们的入参没有 address 字段也能进行校验了,这里大家需要注意下。

  • 自定义参数校验

请查看【技巧】API接口参数验证的必备神器,让你的代码更高效!

  • 国际化支持

public class Users {  @NotEmpty(message = "{name.notempty}", groups = G1.class)  private String name ;  @Min(value = 10, message = "年龄不能小于 10", groups = G1.class)  @Min(value = 20, message = "年龄不能小于 20", groups = G2.class)  private Integer age ;  @Length(min = 6, max = 18, message = "邮箱介于 6 到 18 之间", groups = {G1.class,  G2.class})  private String email ;  @NotEmpty(message = "电话必需填写")  private String phone ;  @Valid  private Address address = new Address();}

注意这里的 name 字段中的 message 属性我们使用了表达式的方式,而 name.notempty 为我们在资源文件中定义的 key。接下来,在 resources/下新建如下属性文件:

属性文件必须是 ValidationMessages 开头。默认文件及 zh_CN 内容:

name.notempty=姓名必需填写

en_US 内容:

name.notempty=name is require

测试:

为了模拟英文环境,我们需要设置请求头 Accept-Languageen-US

显示了 en_US.properties 中定义的消息,到此国际化完成。

  • AOP 验证参数统一处理

自定义注解标记需要进行统一参数校验处理的接口。

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface EnableValidate {}

AOP切面类

@Component@Aspectpublic class ValidateAspect {  @Pointcut("@annotation(com.pack.params.valid.EnableValidate)")  public void valid() {}  @Before("valid()")  public void validateBefore(JoinPoint jp) {    Object[] args = jp.getArgs() ;    for (Object arg : args) {      if (arg instanceof BindingResult) {        BindingResult result = (BindingResult) arg ;        if (result.hasErrors()) {          String messages = result.getAllErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.joining(",")) ;          throw new ParamsException(messages) ;        }      }    }  }}

定义了一个前置通知,拦截标记有@EnableValidate 注解的接口。如果有异常信息收集错误信息然后抛出异常信息。测试:

@PackMapping(value = "/valid/save1", method = RequestMethod.POST)@EnableValidatepublic Object save1(@RequestBody @Validated(Users.G1.class) Users user, BindingResult result) {  Optional<List<String>> op = valid(result) ;  if (op.isPresent()) {    return op.get() ;  }  return "success" ;}

到此我们通过 AOP 技术实现了参数统一处理,但是这样输出错误信息很不友好,接下来我们来完善下,通过全局异常通知拦截处理。这里的异常信息我们可以通过全局异常处理下格式。

完毕!!!


【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读195
粉丝0
内容832