不断学习更新中…
同教程代码
- 码云
1.背景
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. Jakarta Bean Validation
2.1 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 相关网址
2.2 相关网址
- 首页
- 2.0首页
- 2.0官方规范学习文档
在2020年2月份已经发布了
3.0.0-M1
版本
其中
Jakarta Bean Validation
只是一套标准,我们需要使用其他组织机构提供的实现来进行验证,官方支持的为
Hibernate Validator
3.动手实践
3.动手实践
3.1 所需环境
3.1 所需环境
这里JDK使用了
JDK1.8
,使用
maven
进行所需jar文件依赖,使用
springboot
搭建框架脚手架,使用
lombok
简化代码
如果用的不是这几个可以适当修改,大同小异,而且springboot以及或其他依赖的版本每天都在变化,各个版本之间难免有或多或少的差别,可能细节处与本文章有所不同,需要大家知晓,并且根据自己的版本进行调整(比如spring-boot-starter-parent版本2.2.7与2.3.0在验证异常时返回json格式与内容就有很大不同)
3.2 搭建空框架
3.2 搭建空框架
- 使用
创建springboot项目,依次选择添加spring initializr
、web
以及validation
模块,生成的lombok
依赖如下。我这里pom.xml
的版本为2.3.0,再添加其他所需的spring-boot-starter-parent
依赖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 编写代码
3.3 编写代码
编写背景:模拟英雄联盟游戏的技能与英雄的保存
这里的命名遵循外服名称而不是国服直译,例如英雄为champion而不是hero,技能为ability而不是skill
3.3.1 实体类
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 控制层
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 测试
3.3.3 测试
使用
postman
或其他工具发送POST请求,进行验证,我们直接输入我们参数直接传一个内容为空的json,查看结果
可以看到,这里返回了400异常,意为参数错误
我们再把所有参数补全,再试一下
可以看到,如果我们把参数补全之后,返回的是“ok”,即进入controller执行该方法。
那么,例子中添加的几个注解都是什么意思,有什么作用,而且注解中写的message信息在验证后并没有输出,那么我们怎么样输出这些message呢
4. 注解含义
4. 注解含义
4.1 开启验证
4.1 开启验证
首先我们看controller类最上方,我们标注了
@Validataed
,该注解的含义是:这个类要启用参数校验。在save方法的参数中标注了
@Valid
,含义为我们要对紧跟的实体进行校验,而具体校验的内容,为实体类中的我们的定义的约束
以
Ability
类举例,在
name
字段上方标记了
@NotBlank
,意为定义了该字段不允许为空的约束,如果
name
为空,校验就不通过,就会返回我们之前碰到的400异常。而type字段也标注了
@NotNull
,也定义了该字段不允许为空的约束,具体的区别以及其他内置的约束如
3.5
所示
4.2 内置约束
4.2 内置约束
内置约束位于
javax.validation.constraints
保内,列表如下
4.2.1 @Null
4.2.1 @Null
- 被标注元素必须为
null
- 接收任意类型
比如在创建一个英雄时,ID需要由数据库自增生成,而不是我们自定义,那么该我们在接收前台传递的json时就必须为空
4.2.2 @NotNull
4.2.2 @NotNull
- 被标注元素必须不为
null
- 接收任意类型
定义一个字段不能为空,例如技能类型或者英雄名称
4.2.3 @AssertTrue
4.2.3 @AssertTrue
- 被标注元素必须true
- 支持的类型为
以及boolean
Boolean
-
被认为是有效的null
要么为null,否则必须为true
4.2.4 @AssertFalse
4.2.4 @AssertFalse
- 被标注元素必须false
- 支持的类型为
以及boolean
Boolean
-
被认为是有效的null
要么为null,否则必须为false
4.2.5 @Min
4.2.5 @Min
- 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
以及各自的包装类long
- 注意
以及double
由于舍入错误而不被支持float
-
被认为是有效的null
4.2.6 @Max
4.2.6 @Max
- 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
以及各自的包装类long
- 注意
以及double
由于舍入错误而不被支持float
-
被认为是有效的null
4.2.7 @DecimalMin
4.2.7 @DecimalMin
- 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
- 支持的类型为
、BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
以及各自的包装类long
- 注意
以及double
由于舍入错误而不被支持float
-
被认为是有效的null
4.2.8 @DecimalMax
4.2.8 @DecimalMax
- 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
- 支持的类型为
、BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
以及各自的包装类long
- 注意
以及double
由于舍入错误而不被支持float
-
被认为是有效的null
4.2.9 @Negative
4.2.9 @Negative
- 被标注元素必须为是一个严格意义上的负数(即0被认为是无效的)
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
以及各自的包装类double
-
被认为是有效的null
4.2.10 @NegativeOrZero
4.2.10 @NegativeOrZero
- 被标注元素必须为是负数或者0
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
以及各自的包装类double
-
被认为是有效的null
4.2.11 @Positive
4.2.11 @Positive
- 被标注元素必须为是一个严格意义上的正数(即0被认为是无效的)
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
以及各自的包装类double
-
被认为是有效的null
4.2.12 @Positive OrZero
4.2.12 @Positive OrZero
- 被标注元素必须为是正数或者0
- 支持的类型为
、BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
以及各自的包装类double
-
被认为是有效的null
4.2.13 @Size
4.2.13 @Size
- 被标注元素的大小必须在指定的边界区间
- 支持的类型为
(计算字符序列的长度) 、CharSequence
(计算集合的大小)、Collection
(计算map的大小) 、Map
(计算数组的长度)Array
-
被认为是有效的null
4.2.14 @Digits
4.2.14 @Digits
- 被标注元素必须是在可接受范围内的数字
- 支持的类型为
、BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
以及各自的包装类long
-
被认为是有效的null
4.2.15 @Past
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
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
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
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
4.2.19 @Pattern
- 被标注的
必须匹配指定的正则表达式,该正则表达式遵循Java的正则表达式规定CharSequence
- 支持的类型为
CharSequence
-
被认为是有效的null
4.2.20 @NotEmpty
4.2.20 @NotEmpty
- 被标注元素必须不为
或者空(以字符串举例,不为null并且不为“”)null
- 支持的类型为
(计算字符序列的长度) 、CharSequence
(计算集合的大小)、Collection
(计算map的大小) 、Map
(计算数组的长度)Array
4.2.21 @NotBlank
4.2.21 @NotBlank
- 被标注元素必须不为
,并且必须包含至少一个非空格的字符null
- 支持的类型为
CharSequence
4.2.22 @Email
4.2.22 @Email
- 字符串必须是格式良好的电子邮件地址
- 支持的类型为
CharSequence
5. 异常模块
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. 验证非前台传递的参数
6. 验证非前台传递的参数
除了在
controller
验证前台传递的参数之外,有时我们还需要验证诸如自己new的对象,或者从其他方法查询出来的对象,这时候我们可能需要把这些操作放在
service
层或其他层
6.1 调用非本类的校验方法
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 调用本类的校验方法
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,有时候可能就是一个空方法,只不过参数有验证,其他不知道的小伙伴看到可能会比较懵
- 注入
获取beanApplicationContext
@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的位置
6.3 关于@Validated的位置
我们已经清楚,约束配置的注解,例如
@Valid
、
@NotNull
等,需要在接口上进行配置,那么
@Validated
需要标注在哪里呢,答案是接口和实现类都可以,但是标注位置不同,也有一些区别
- 标注在接口:意为实现类都回开启验证
- 标注在实现类:意为标注该注解的实现类才会开启验证,如果有一个实现类未标注
,那么即使接口有约束配置,也不会在该实现类上进行校验@Validated
6.4 关于实现类需要不需要标注约束配置
6.4 关于实现类需要不需要标注约束配置
个人感觉有优点优缺点
优点:一般看代码的时候,都不会看接口,而是直接看实现类。如果标注在实现类上,可以更直观的看到该方法的约束配置
缺点:必须与接口完全对应,如果接口修改约束配置,那么实现类必须相应的进行修改,否则会抛出异常