参数校验Jakarta Bean Validation学习
1.背景
我们在平时的学习与工作中,都需要对参数进行校验,比如在注册时,用户名密码不能为空,用户名长度必须小于10等等。虽然有些校验在前端页面会进行验证,但是后端为了增加健壮性也需要对这些参数进行判断(比如绕过前端页面而直接调用了接口,参数的合法性未知),可能就会在controller或者service中就会有如下代码的出现
package com.beemo.validation.controller; import com.beemo.validation.demo1.entity.Student; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestBody; import java.util.Objects; @RestController public class DemoController { @RequestMapping("/demo") public String saveDemo(@RequestBody Student student) { if (StringUtils.isEmpty(student.getName())) { return "学生名称不能为空"; } if (student.getName().length() > 10) { return "学生名称长度不能超过10位"; } if (Objects.isNull(student.getAge())) { return "学生年龄不能为空"; } if (student.getAge() <= 0) { return "学生年龄不能为负数"; } if (Objects.isNull(student.getNumber())) { return "学号不能为空"; } if (student.getNumber().length() != 10) { return "学号长度必须为10"; } // 其他判断 // 调用service的方法等 return "ok"; } @Data class Student { /** * 姓名 */ private String name; /** * 年龄 */ private Integer age; /** * 学号 */ private String number; } }
从例子中可以看到,这仅仅是一个实体类3个字段的简单验证,就已经占据了很多的篇幅,也需要我们进行手动编写这种判断代码,比较费时,代码读起来也没什么营养,大部分都是在判断合法性,等我们真正读到想要的业务逻辑代码可能需要往下翻好久,那么有没有办法能够让我们更简洁更优雅的去验证这些参数呢
2. Jakarta Bean Validation
2.1 Jakarta Bean Validation简介
首先要知道Jakarta就是Java更名之后的名称,Jakarta Bean Validation也就是Java Bean Validation,是一套Java的规范,它可以
通过使用注解的方式在对象模型上表达约束
以扩展的方式编写自定义约束
提供了用于验证对象和对象图的API
提供了用于验证方法和构造方法的参数和返回值的API
报告违反约定的集合
运行在Java SE,并且集成在Jakarta EE8中
例如:
public class User { private String email; @NotNull @Email public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } public class UserService { public void createUser(@Email String email, @NotNull String name) { ... } }
虽然可以手动运行校验,但更加自然的做法是让其他规则和框架在适时对数据进行校验(用户在表示框架中进行输入,业务服务通过CDI执行,实体通过JPA插入或者更新)
换句话说,即运行一次,到处约束
2.2 相关网址
在2020年2月份已经发布了3.0.0-M1
版本
其中Jakarta Bean Validation
只是一套标准,我们需要使用其他组织机构提供的实现来进行验证,官方支持的为Hibernate Validator
3. 动手实践
3.1 所需环境
这里JDK使用了JDK1.8,使用maven进行所需jar文件依赖,使用springboot搭建框架脚手架,使用lombok简化代码
如果用的不是这几个可以适当修改,大同小异,而且springboot以及或其他依赖的版本每天都在变化,各个版本之间难免有或多或少的差别,可能细节处与本文章有所不同,需要大家知晓,并且根据自己的版本进行调整(比如spring-boot-starter-parent版本2.2.7与2.3.0在验证异常时返回json格式与内容就有很大不同)
3.2 搭建空框架
- 使用
spring initializr
创建springboot项目,依次选择添加web
、validation
以及lombok
模块,生成的pom.xml
依赖如下。我这里spring-boot-starter-parent
的版本为2.3.0,再添加其他所需的pom
依赖
... <!-- spring-boot版本 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> ... <!-- web模块 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 验证模块,hibernate-validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <!-- guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> </dependency>
3.3 编写代码
编写背景:模拟英雄联盟游戏的技能与英雄的保存
这里的命名遵循外服名称而不是国服直译,例如英雄为champion而不是hero,技能为ability而不是skill
3.3.1 实体类
- 英雄
package com.beemo.validation.demo2.entity; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; /** * 英雄entity */ @Data public class Champion { /** * 英雄名称 */ @NotBlank(message = "英雄名称不能为空") private String name; /** * 英雄头衔 */ @NotBlank(message = "英雄头衔不能为空") private String title; /** * 英雄描述 */ @NotBlank(message = "英雄描述不能为空") private String description; /** * 英雄类型 * 坦克、刺客、射手、法师、辅助以及战士 */ @NotNull(message = "英雄类型不能为空") private Byte type; }
- 技能entity
package com.beemo.validation.demo2.entity; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; /** * 技能 */ @Data public class Ability { /** * 技能名称 */ @NotBlank(message = "技能名称不能为空") private String name; /** * 技能描述 */ @NotBlank(message = "技能描述不能为空") private String description; /** * 技能类型 * 例如魔法值、怒气、能量等 */ @NotNull(message = "技能类型不能为空") private Byte type; }
3.3.2 控制层
- 英雄controller
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Champion; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("/demo2/champion") @Validated public class ChampionController { /** * 保存 * @param entity 要保存的英雄实体 * @return 保存结果 */ @PostMapping("save") public String save(@Valid @RequestBody Champion entity) { // 调用service等 return "ok"; } }
- 技能controller
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Ability; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能实体 * @return 保存结果 */ @PostMapping("save") public String save(@Valid @RequestBody Ability entity) { // 调用service等 return "ok"; } }
3.3.3 测试
使用postman
或其他工具发送POST请求,进行验证,我们直接输入我们参数直接传一个内容为空的json,查看结果
可以看到,这里返回了400异常,意为参数错误
我们再把所有参数补全,再试一下
可以看到,如果我们把参数补全之后,返回的是“ok”,即进入controller执行该方法。
那么,例子中添加的几个注解都是什么意思,有什么作用,而且注解中写的message信息在验证后并没有输出,那么我们怎么样输出这些message呢
4. 注解含义
4.1 开启验证
首先我们看controller类最上方,我们标注了@Validataed(这里可以去掉,不用在控制类上添加该注解),该注解的含义是:这个类要启用参数校验。在save方法的参数中标注了@Valid,含义为我们要对紧跟的实体进行校验,而具体校验的内容,为实体类中的我们的定义的约束
以Ability类举例,在name字段上方标记了@NotBlank,意为定义了该字段不允许为空的约束,如果name为空,校验就不通过,就会返回我们之前碰到的400异常。而type字段也标注了@NotNull,也定义了该字段不允许为空的约束,具体的区别以及其他内置的约束如3.5所示
4.2 内置约束
内置约束位于javax.validation.constraints包
内,列表如下
4.2.1 @Null
- 被标注元素必须为
null
- 接收任意类型
比如在创建一个英雄时,ID需要由数据库自增生成,而不是我们自定义,那么该我们在接收前台传递的json时就必须为空
4.2.2 @NotNull
- 被标注元素必须不为
null
- 接收任意类型
定义一个字段不能为空,例如技能类型或者英雄名称
4.2.3 @AssertTrue
- 被标注元素必须true
- 支持的类型为
boolean
以及Boolean
null
被认为是有效的
要么为null,否则必须为true
4.2.4 @AssertFalse
- 被标注元素必须false
- 支持的类型为
boolean
以及Boolean
null
被认为是有效的
要么为null,否则必须为false
4.2.5 @Min
- 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
以及各自的包装类 - 注意
double
以及float
由于舍入错误而不被支持 null
被认为是有效的
4.2.6 @Max
- 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
以及各自的包装类 - 注意
double
以及float
由于舍入错误而不被支持 null
被认为是有效的
4.2.7 @DecimalMin
- 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
- 支持的类型为
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包装类 - 注意
double
以及float
由于舍入错误而不被支持 null
被认为是有效的
4.2.8 @DecimalMax
- 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
- 支持的类型为
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包装类 - 注意
double
以及float
由于舍入错误而不被支持 null
被认为是有效的
4.2.9 @Negative
- 被标注元素必须为是一个严格意义上的负数(即0被认为是无效的)
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包装类 null
被认为是有效的
4.2.10 @NegativeOrZero
- 被标注元素必须为是负数或者0
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包装类 null
被认为是有效的
4.2.11 @Positive
- 被标注元素必须为是一个严格意义上的正数(即0被认为是无效的)
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包装类 null
被认为是有效的
4.2.12 @PositiveOrZero
- 被标注元素必须为是正数或者0
- 支持的类型为
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包装类 null
被认为是有效的
4.2.13 @Size
- 被标注元素的大小必须在指定的边界区间
- 支持的类型为
CharSequence
(计算字符序列的长度) 、Collection
(计算集合的大小)、Map
(计算map的大小) 、Array
(计算数组的长度) null
被认为是有效的
4.2.14 @Digits
- 被标注元素必须是在可接受范围内的数字
- 支持的类型为
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包装类 null
被认为是有效的
4.2.15 @Past
- 被标注元素必须是过去的某个时刻、日期或者时间
- “现在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定义的,默认的ClockProvider
根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区 -
支持的类型为java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包装类
null
被认为是有效的
4.2.16 @PastOrPresent
- 被标注元素必须是过去或现在的某个时刻、日期或者时间
- “现在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定义的,默认的ClockProvider
根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区 - “现在”的概念相对的定义在使用的约束上,例如,如果约束在Year上,那么现在表示当前年份
-
支持的类型为java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包装类
null
被认为是有效的
4.2.17 @Future
- 被标注元素必须是未来的某个时刻、日期或者时间
- “现在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定义的,默认的ClockProvider
根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区 -
支持的类型为java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包装类
null
被认为是有效的
4.2.18 @FutureOrPresent
- 被标注元素必须是未来或现在的某个时刻、日期或者时间
- “现在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定义的,默认的ClockProvider
根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区 - “现在”的概念相对的定义在使用的约束上,例如,如果约束在Year上,那么现在表示当前年份
-
支持的类型为java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包装类
null
被认为是有效的
4.2.19 @Pattern
- 被标注的
CharSequence
必须匹配指定的正则表达式,该正则表达式遵循Java的正则表达式规定 - 支持的类型为
CharSequence
null
被认为是有效的
4.2.20 @NotEmpty
- 被标注元素必须不为
null
或者空(以字符串举例,不为null并且不为“”) - 支持的类型为
CharSequence
(计算字符序列的长度) 、Collection
(计算集合的大小)、Map
(计算map的大小) 、Array
(计算数组的长度)
4.2.21 @NotBlank
- 被标注元素必须不为
null
,并且必须包含至少一个非空格的字符 - 支持的类型为
CharSequence
4.2.22 @Email
- 字符串必须是格式良好的电子邮件地址
- 支持的类型为
CharSequence
5. 异常模块
还有一个问题,就是我们定义的message没有生效,比如“技能名称不能为空”,并没有出现在返回结果中,取而代之的是400异常,那么怎样才能返回我们想要的message呢
首先我们在controller当中定一个一个方法,用@ExceptionHandler注解标注一下,用来获取controller抛出的异常,然后我们跟踪一下断点,看一下到底是什么异常
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Ability; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能实体 * @return 保存结果 */ @PostMapping("save") public String save(@Valid @RequestBody Ability entity) { // 调用service等 return "ok"; } @ExceptionHandler public void handleException(Exception e) { e.printStackTrace(); } }
抛出的是org.springframework.web.bind.MethodArgumentNotValidException
在看一下DEBUG窗口中的每个参数,发现bindingResult->errors->field和defaultMessage,一个违反约束的字段名称,另一个是违我们自定义的message
此时我们就可以进行处理,返回我们想要的结果,而不是抛出400
5.1 优化返回值
在实际开发中,一般不会返回一个“ok”或者“success”这种字符串,通常情况下会返回一个json字符串,其中包含
- 一个表示结果的状态值,例如
HTML状态码
或自定义状态值 - 一个返回消息,解释该状态值或结果
- 承载数据
package com.beemo.demo2.common; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; @AllArgsConstructor @NoArgsConstructor public class R { private int code; private String msg; private Object data; public static R success() { return success(null); } public static R success(Object data) { return new R(1, "操作成功", data); } public static R violateConstraint(List<Map<String, String>> violation) { return new R(2, "参数校验未通过", violation); } }
修改controller
package com.beemo.demo2.controller; import com.beemo.demo2.common.R; import com.beemo.demo2.entity.Ability; import org.springframework.validation.FieldError; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; import java.util.stream.Collectors; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能实体 * @return 保存结果 */ @PostMapping("save") public R save(@Valid @RequestBody Ability entity) { // 调用service等 return R.success(); } }
将异常处理方法提出,标注@ControllerAdvice
注解,使得每个controller的异常都可以用该方法处理,并修改返回值,并且如果是单独提出来一个模块,需要在启引用该模块的启动类上加扫描
package com.beemo.common.config; import com.beemo.common.common.R; import org.springframework.stereotype.Component; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.stream.Collectors; @ControllerAdvice @ResponseBody public class MyExceptionHandler { @ExceptionHandler public R handleException(MethodArgumentNotValidException e) { List<String> violations = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage). collect(Collectors.toList()); return R.violateConstraint(violations); } }
package com.beemo.demo2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(scanBasePackages = "com.beemo.*") public class Demo2Application { public static void main(String[] args) { SpringApplication.run(Demo2Application.class, args); } }
然后我们再测试一下
发现得到的结果再也不是400异常,而是我们指定的message集合了
6. 验证非前台传递的参数
除了在controller
验证前台传递的参数之外,有时我们还需要验证诸如自己new的对象,或者从其他方法查询出来的对象,这时候我们可能需要把这些操作放在service
层或其他层
6.1 调用非本类的校验方法
例如我们自己new了一个对象,然后调用其他类的一个验证方法
建立一个service接口以及一个实现类
我们在实现类上,模拟controller校验,加上@Validated
以及@Valid
注解
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(Ability ability); }
package com.beemo.demo3.service.impl; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.Arrays; import java.util.List; /** * 技能service实现类 */ @Validated @Service public class AbilityServiceImpl implements IAbilityService { @Override public void saveOne(@Valid @NotNull Ability ability) { System.out.println("通过校验"); // 进行保存操作等... }
然后在controller中调用该方法
package com.beemo.demo3.controller; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/demo3/ability") @Validated public class AbilityController { @Autowired private IAbilityService abilityService; /** * 保存 * @return 保存结果 */ @PostMapping("save") public String save() { // new Ability ability = new Ability(); abilityService.saveOne(ability); return "ok"; } }
我们进行测试发现,并没有我们符合想象的返回R,相反在后台控制台报了一个异常
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AbilityServiceImpl#saveOne(Ability) redefines the configuration of IAbilityService#saveOne(Ability). at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final] at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.assertCorrectnessOfConfiguration(ExecutableMetaData.java:462) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final] at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:380) ~[ ......
一个重写的方法禁止重新定义参数的约束配置,但是方法AbilityServiceImpl#saveOne(Ability) 重新定义了 IAbilityService#saveOne(Ability)的配置
翻译过来就是如果你的接口没有定义约束,那么你的实现类就不能够定义该约束
按照异常信息,我们试着将验证放在接口中在尝试一下
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; @Validated /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(@Valid @NotNull Ability ability); }
测试之后发现返回结果为500异常,这次控制器打印异常信息明显跟上次不一样,貌似确实是通过校验了,只不过抛出的异常不一样
javax.validation.ConstraintViolationException: saveOne.ability: 不能为null at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE] ......
我们发现在service层中如果违法约束抛出的异常为ConstraintViolationException
,而并非在controller中的MethodArgumentNotValidException
我们再次改进异常处理方法,然后跟踪一下异常的信息
根据调试的信息,我们就可以处理我们的返回值了
@ExceptionHandler public R handleException2(ConstraintViolationException e) { List<String> violations = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessageTemplate).collect(Collectors.toList()); return R.violateConstraint(violations); }
再测试一下
测试成功
6.2 调用本类的校验方法
场景:我们需要从EXCEL中读取数据,然后保存数据库中,需要判断每一条记录,如果正确就进行保存,如果失败则打印日志,接口和实现类如下
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; @Validated /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(@Valid @NotNull Ability ability); /** * 批量保存EXCEL中的数据 */ void saveOnesFromExcel(); }
package com.beemo.demo3.service.impl; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.validation.ConstraintViolationException; import java.util.List; /** * 技能service实现类 */ @Service @Slf4j public class AbilityServiceImpl implements IAbilityService { @Override public void saveOne(Ability ability) { System.out.println("通过校验"); // 进行保存操作等... } /** * 批量保存EXCEL中的数据 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { saveOne(data.get(i)); System.out.println("第" + i + "条记录保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "条记录违法约束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "条记录保存失败"); } } } /** * 从EXCEL中读取 * @return */ private List<Ability> readFromExcel() { return Lists.newArrayList(new Ability(null, null, (byte)1), new Ability(null, "测试描述", null), new Ability("测试名称", null, null), new Ability("约德尔诱捕器", "布置一个陷阱,陷阱可以束缚敌方英雄2秒并将目标暴露在己方视野内3秒。", (byte)1)); } }
我们模拟了一个从EXCEL中读取list的方法,然后调用了save方法,该方法有参数验证,我们来进行测试
控制台打印成功,证明我们的约束并没有成功,但是我们的写法看似没问题
其实这个原因就是因为第一个方法saveFromExcel并没有标注验证,不论该方法怎么调用本类的验证方法都不会生效,此问题原因同@Transactional以及@Aysnc标注的方法,其本质原因是因为代理的问题,这里不做过多探讨,解决该问题的方法有三种
- (不推荐)将验证方法移到其他类中 。这种方法奏效,但是无缘无故需要多建立一个service,有时候可能就是一个空方法,只不过参数有验证,其他不知道的小伙伴看到可能会比较懵
- 注入
ApplicationContext
获取bean
@Autowired private ApplicationContext applicationContext; /** * 批量保存EXCEL中的数据 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { applicationContext.getBean(IAbilityService.class).saveOne(data.get(i)); System.out.println("第" + i + "条记录保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "条记录违法约束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "条记录保存失败"); } } }
3. 通过注入自己来获取当前类的实例,再调用该实例的方法。需要加@Lazy注解防止自我注入时spring抛出org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}异常
@Autowired @Lazy private IAbilityService abilityService; /** * 批量保存EXCEL中的数据 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { abilityService.saveOne(data.get(i)); System.out.println("第" + i + "条记录保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "条记录违法约束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "条记录保存失败"); } } }
6.3 关于@Validated的位置
我们已经清楚,约束配置的注解,例如@Valid
、@NotNull
等,需要在接口上进行配置,那么@Validated
需要标注在哪里呢,答案是接口和实现类都可以,但是标注位置不同,也有一些区别
- 标注在接口:意为实现类都回开启验证
- 标注在实现类:意为标注该注解的实现类才会开启验证,如果有一个实现类未标注
@Validated
,那么即使接口有约束配置,也不会在该实现类上进行校验
6.4 关于实现类需不需要标注约束配置
个人感觉有优点优缺点
优点:一般看代码的时候,都不会看接口,而是直接看实现类。如果标注在实现类上,可以更直观的看到该方法的约束配置
缺点:必须与接口完全对应,如果接口修改约束配置,那么实现类必须相应的进行修改,否则会抛出异常
6.5 想让枚举类属性验证生效,需要添加@valid注解
6.6 feign调用中校验也可以生效
无需在feign接口中添加校验注解,只需在controller的接口方法参数中添加@valid
转载:https://blog.csdn.net/csdn_mrsongyang/article/details/106115243