依赖:
<!--jsr3参数校验器-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.2.RELEASE</version>
<!-- 版本与springboot一致即可-->
</dependency>
<!-- 里面依赖了hibernate-validator,而hibernate-validator又是javax.validation的实现。javax.validation是标准-->
依赖的包有哪些注解可以参看:javax.validation.constraints
当然除了使用注解还可以使用正则表达式如;
/**
* 检索首字母
*/
@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空")
private String firstLetter;
备注:注解@NotBlank、@NotEmpty及@NotNull区别
1.@NotNull:
不能为null,但可以为empty(""," “,” ") ,一般用在基本数据类型的非空校验上,而且被其标注的字段可以使用 @size/@Max/@Min对字段数值进行大小的控制
2.@NotEmpty:
不能为null,而且长度必须大于0(" “,” "),一般用在集合类上面
3.@NotBlank:
这玩意只能作用在接收的String类型上,不能为null,而且调用trim()后,长度必须大于0
一、简单的数据校验
1.1实体类需要检验的字段加上注解@NotBlank(message="品牌名不能为空")
@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌名
*/
@NotBlank(message="品牌名不能为空")
private String name;
/**
* 其他字段省略....
*/
}
1.2controller方法参数需要使用注解@Valid才会生效
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
当发送请求:不携带name字段
POST http://localhost:xxxx/brand/save
Content-Type: application/json
{ "descript": "测试JSR303"}
请求结果如下:
{
"timestamp": "2020-04-29T09:36:04.125+0000",
"status": 400,
"error": "Bad Request"
}
备注:虽然成功校验了数据但是@NotBlank(message="品牌名不能为空")里边的message没有生效且也没有使用默认的message,即:ValidationMessages.properties的默认message。原因有待解决。。
但是这种返回的错误结果并不符合我们的业务需要。接下来看第二
二、简单的数据校验以及使用 BindingResult 封装错误结果
2.1依赖,实体类同上
2.2改写controller方法
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
HashMap<String, String> stringStringHashMap = new HashMap<>();
if (result.hasErrors()) {
List<FieldError> fieldErrors = result.getFieldErrors();
fieldErrors.forEach(item -> {
stringStringHashMap.put(item.getField(), item.getDefaultMessage());
});
return R.error(400,"数据异常").put("data", stringStringHashMap);
} else {
brandService.save(brand);
return R.ok().put("data", stringStringHashMap);
}
}
备注:R为系统统一返回数据的一个Map (自行封装)
给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义封装。
处理结果如下:
{
"msg": "数据异常",
"code": 400,
"data": {
"name": "品牌名不能为空"
}
}
但是针对于每一个请求设置了一个内容校验以及错误封装,显然不是太合适,实际上可以统一的对于异常进行处理。
三、数据校验之统一异常处理
介绍:https://www.cnblogs.com/lenve/p/10748453.html
3.1 添加一个全局异常处理类 com.xxxx.exception.MyExceptionControllerAdvice ,并使用 注解 @RestControllerAdvice(basePackages = "com.xxxx.controller")指定扫描controller包
定义一个方法handleValidException,并使用注解@ExceptionHandler(value = Exception.class)处理器捕获异常。方法参数为MethodArgumentNotValidException,即校验异常
@Slf4j
@RestControllerAdvice(basePackages = "com.xxxx.controller")
public class MyExceptionControllerAdvice {
//@ResponseBody //上边用了RestControllerAdvice这里可以不写
@ExceptionHandler(value = Exception.class)
public R handleValidException(MethodArgumentNotValidException e){
Map<String,String> map=new HashMap<>();
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError ->{
map.put(fieldError.getField(),fieldError.getDefaultMessage());
});
/**
* 如果不确定异常类型为 MethodArgumentNotValidException
* 可以先写Exception,然后使用
* log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
* 看看具体的异常类型
*/
return R.error(400,"数据校验异常").put("data",map);
}
//最后用于匹配上边未精确匹配到的其他异常
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
return R.error();
}
}
3.2controller改回原来的样子
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result*/) {
/* HashMap<String, String> stringStringHashMap = new HashMap<>();
if (result.hasErrors()) {
List<FieldError> fieldErrors = result.getFieldErrors();
fieldErrors.forEach(item -> {
stringStringHashMap.put(item.getField(), item.getDefaultMessage());
});
return R.error(400,"数据异常").put("data", stringStringHashMap);
} else {
brandService.save(brand);
return R.ok().put("data", stringStringHashMap);
}*/
brandService.save(brand);
return R.ok();
}
发送异步请求:
POST http://xxxx/brand/save
Content-Type: application/json
{ "descript": "2测试JSR303"}
请求结果:
{
"msg": "数据校验异常",
"code": 400,
"data": {
"name": "品牌名不能为空"
}
}
3.3 规范错误码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
com.xxxx.exception.BizCodeEnum
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnum {
UNKNOW_EXEPTION(10000,"系统未知异常"),
VALID_EXCEPTION(10001,"数据校验失败");
private int code;
private String msg;
BizCodeEnum(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
3.3.1 改造 MyExceptionControllerAdvice
@Slf4j
@RestControllerAdvice(basePackages = "com.xxxx.controller")
public class MyExceptionControllerAdvice {
//@ResponseBody //上边用了RestControllerAdvice这里可以不写
@ExceptionHandler(value = Exception.class)
public R handleValidException(MethodArgumentNotValidException e){
Map<String,String> map=new HashMap<>();
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError ->{
map.put(fieldError.getField(),fieldError.getDefaultMessage());
});
/**
* 如果不确定异常类型为 MethodArgumentNotValidException
* 可以先写Exception,然后使用
* log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
* 看看具体的异常类型
*/
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
return R.error();
}
}
测试校验失败请求结果如下:
{
"msg": "数据校验失败",
"code": 10001,
"data": {
"name": "品牌名不能为空"
}
}
但是实际上业务并没有这么简单,在新增品牌数据时,name确实不能为空,但是如果我们是修改品牌的某一项数据呢,此时name是可以不携带的,此时我们需要用到 分组校验功能(完成多场景的复杂校验)
四、分组校验功能(完成多场景的复杂校验)
org.springframework.validation.annotation.Validated; @Validated注解提供了 Class<?>[] value() default {}; 对象数组,其实际需要的是一个空接口。
4.1添加分组接口(空接口即可,根据请情况添加,例如系统添加、更新操作都需要校验数据)
public interface AddGroup {
}
public interface UpdateGroup {
}
4.2 给entity标记分组。生效情况请看下边有描述
@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@Null(message = "不能携带品牌id",groups = {AddGroup.class})
@NotNull(message = "品牌id不能为空",groups = {UpdateGroup.class})
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message="品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "logo不能为空",groups = {AddGroup.class})
@URL(message = "logo必须为一个URL",groups = {AddGroup.class,UpdateGroup.class})//不携带就不校验
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
//Pattern只能用在String对象上
//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotBlank(message = "检索字母不能为空",groups = {AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为a-z或者A-z中的一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序字段不能为空",groups = {AddGroup.class})
@Min(value = 0,message = "排序字段不能小于0",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
4.3 controller接口改写
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class)@RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
4.4 测试
4.41 save接口测试
POST http://xxx/brand/save
Content-Type: application/json
{ "descript": "2测试JSR303",
"name": "ss",
"logo": "ddddhttp://baidu.com",
"showStatus": "55",
"sort": "66",
"firstLetter": "ss"
}
结果:
{
"msg": "数据校验失败",
"code": 10001,
"data": {
"logo": "logo必须为一个URL",
"firstLetter": "首字母必须为a-z或者A-z中的一个字母"
}
}
修改后测试:
{ "descript": "2测试JSR303",
"name": "ss",
"logo": "http://baidu.com",
"showStatus": "55",
"sort": "66",
"firstLetter": "s"
}
结果;
{
"msg": "success",
"code": 0
}
4.42 update接口测试:
{
"descript": "2测试JSR303",
"name": "ss",
"logo": "sshttp://baidu.com",
"showStatus": "55",
"sort": "66",
"firstLetter": "ss"
}
结果:
{
"msg": "数据校验失败",
"code": 10001,
"data": {
"brandId": "品牌id不能为空",
"logo": "logo必须为一个URL",
"firstLetter": "首字母必须为a-z或者A-z中的一个字母"
}
}
测试完毕,update成功就不测了。
@Validated的生效有两种情况:
1、单纯使用@Validated注解,不添加 Class<?>[] value() default {};属性。此时entity上没有使用group属性的字段校验才会生效
如:BrandEntity.calss
@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌名
*/
@NotBlank(message="品牌名不能为空",groups = {AddGroup.class})
private String name;
/**
* 检索首字母
*/
@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空")
private String firstLetter;
/**
* 其他字段省略...
*/
}
controller:
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Validated @RequestBody BrandEntity brand/*, BindingResult result*/) {
brandService.save(brand);
return R.ok();
}
此时BrandEntity中,name字段的校验是不生效的,因为controller的方法使用的是@Validated注解没有携带AddGroup.class; 而firstLetter的校验是生效的,也就是说使用@Validated注解不携带group作用同@Valid。
2、使用@Validated(XxxxGroup.class)注解,实体类字段的校验必须带上group={XxxxGroup.cass}校验才会生效。
如,此时save操作,name,firstLetter两个字段校验都会生效。
@Data
@TableName("xxx")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌名
*/
@NotBlank(message="品牌名不能为空",groups = {AddGroup.class})
private String name;
/**
* 检索首字母
*/
@Pattern(regexp="^[a-zA-Z]$",message="首字母必须为空",group= {AddGroup.class})
private String firstLetter;
/**
* 其他字段省略...
*/
}
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/) {
brandService.save(brand);
return R.ok();
}
五、自定义校验注解
自定义校验注解步骤:
- 自定义注解
- 自定义校验器
- 关联注解和校验器
5.1 自定义校验注解
直接参考任意一个注解,例如参考@NotBlank注解
@Documented
@Constraint(
validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotBlank.List.class)
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
发现需要以下三个字段:
String message() default "{javax.validation.constraints.NotBlank.message}"; //错误信息
Class<?>[] groups() default {};//分组
Class<? extends Payload>[] payload() default {};//自定义负载信息
且需要的元注解有:注解的注解,称为元注解。
@Documented
@Constraint(
validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
其中
@Documented 这个注解只是用来标注生成javadoc的时候是否会被记录。可以不用。
@Constraint 这个注解是最重要的一个注解,用于关联我们将要自定义的校验器,可以关联多个校验器。
@Target 标注注解可以标注的地方如类、方法、字段等
@Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS RUNTIME SOURCE
按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。
一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解。
@Repeatable https://blog.csdn.net/weixin_42245133/article/details/99678509
所以假设我们需要使用的自定义注解@ListValue(vals={0,1,x,xx,xxx})为这样的形式用于校验以下字段
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
//Pattern只能用在String对象上
//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class}) //x
private Integer showStatus;
@ListValue注解如下:
@Documented
@Constraint(
validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
//vals
int [] vals() default {};
}
5.2 自定义校验器
我们点击 @Constraint( validatedBy = {})中的validateBy发现,他需要的是一个 ConstraintValidator
@Documented
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraint {
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
ConstraintValidator如下所示:是一个接口,那么我们可以自定义一个该接口的实现作为我们的校验器
A是我们自定义的注解,T是我们需要校验的字段的类型
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
//初始化数据 A就是我们自定义的注解
}
//
boolean isValid(T var1, ConstraintValidatorContext var2);//
}
ListValueConstraintValidatorForInteger接口如下:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
public class ListValueConstraintValidatorForInteger implements ConstraintValidator<ListValue, Integer> {
HashSet set = new HashSet<Integer>();
@Override
public void initialize(ListValue constraintAnnotation) {
// 初始化数据
int[] vals = constraintAnnotation.vals();
if (vals.length > 0 && vals != null) {
for (int i = 0; i < vals.length; i++) {
set.add(vals[i]);
}
}
}
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
//校验数据 integer就是我们需要检验的数据
return set.contains(integer);
}
}
5.3 关联自定义注解以及自定义校验器
只需要在自定义注解上的@constraint元注解上加上自定义校验器即可,可以填多个
@Documented
@Constraint(
validatedBy = {ListValueConstraintValidatorForInteger.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
//vals
int [] vals() default {};
}
5.4 测试
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0,1},message = "显示状态必须为0或1",groups = {AddGroup.class,UpdateGroup.class})
@NotNull(message = "显示状态不能为空",groups = {AddGroup.class})
//@Pattern注解不能使用在Integer上报错:No validator could be found for constraint
//Pattern只能用在String对象上
//@Pattern(regexp = "^[0-1]$",message = "showStatus只能为0或者1",groups = {AddGroup.class,UpdateGroup.class}) //x
private Integer showStatus;
请求:
POST http://xxx/update
Content-Type: application/json
{
"brandId": "23",
"descript": "SSS试JSR303",
"name": "ss",
"logo": "http://baiSSSSdu.com",
"showStatus": "55",
"sort": "66",
"firstLetter": "s"
}
结果:
{
"msg": "数据校验失败",
"code": 10001,
"data": {
"showStatus": "显示状态必须为0或1"
}
}