场景
SpringBoot+@Validated实现参数验证(非空、类型、范围、格式等)-若依前后端导入Excel数据并校验为例:
上面实现SpringBoot参数校验时以自带的注解进行校验,如果需要进行自定义校验规则,比如
请求时必须携带某个请求码,而且该请求码字符串必须在指定范围,即从枚举类中指定包含的。
上面是以post请求为例,下面以get请求为例。
实现
1、首先添加所需依赖
<!--参数校验依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、要实现非空校验已经有 @NotBlank注解,所以要实现自定义规则注解,可以新建
自定义注解类
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 自定义校验注解,校验请求参数是否存在指定枚举类中
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Constraint(validatedBy = EnumStringValidator.class)
public @interface EnumString {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
以上注解的含义:
@Retention
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Target用来表示注解作用范围,超过这个作用范围,编译的时候就会报错。
@Target:
注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包,用于记录java文件的package信息
@Constraint
此注解的验证逻辑交给EnumStringValidator来处理,这里是一个数组,我们可以传入多个处理逻辑。
ConstraintValidator接口,它有两个泛型,第一个是自定义的注解类,第二个就是要验证的数据的类型,
这两个类里面都有两个方法,initialize和isValid,第一个是初始化方法,第二个是验证的逻辑方法,
返回true,则验证通过,否则则不通过。
并且这里注解只有一个属性message需要传递,用来设置校验不通过时显示的信息。
3、实现上面具体的校验类EnumStringValidator
import com.badao.demo.enums.MineMessageEnum;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义校验逻辑实现-判断api请求码是否合法/是否存在
*/
public class EnumStringValidator implements ConstraintValidator<EnumString,String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
MineMessageEnum resolve = MineMessageEnum.resolve(s);
if(null == resolve){
return false;
}else {
return true;
}
}
}
注意这里只需isValid进行参数校验规则自定义,如果需要传递另外的参数,可以通过
重写initialize方法,获取上面自定义注解的其他变量并在此进行变量赋值。
这里自定义的校验规则是判断字符串类型参数是否包含在指定枚举类MineMessageEnum中。
这里规则就需要根据自己实际需要去实现了,校验通过则在isValid中返回true,否则返回false。
下面附枚举类MineMessageEnum的实现
public enum MineMessageEnum
{
JJT("jjt", "0001","名称1", Constants.SIFANGJI),
ZLW("zlw", "0002","名称2",Constants.SIFANGJI),
CCL("ccl", "0003","名称3",Constants.KEERMA);
private final String apiCode;
private final String mineCode;
private final String mineName;
private final String signalRule;
private static final Map<String, MineMessageEnum> mappings = new HashMap<>();
static
{
for (MineMessageEnum messageEnum : values())
{
mappings.put(messageEnum.apiCode, messageEnum);
}
}
@Nullable
public static MineMessageEnum resolve(@Nullable String mineApiCode)
{
return (mineApiCode != null ? mappings.get(mineApiCode) : null);
}
MineMessageEnum(String apiCode, String mineCode, String mineName,String signalRule)
{
this.apiCode = apiCode;
this.mineCode = mineCode;
this.mineName = mineName;
this.signalRule = signalRule;
}
public String getApiCode() {
return apiCode;
}
public String getMineCode() {
return mineCode;
}
public String getMineName() {
return mineName;
}
public String getSignalRule() {
return signalRule;
}
}
4、封装请求参数实体,并在属性上添加参数校验注解
import com.badao.demo.constant.Constants;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 自定义请求参数带参数校验
*/
@Data
public class MyRequestParams {
@EnumString(message = Constants.ILLEGAL_API_CODE)
@NotBlank(message = Constants.NO_API_CODE)
private String mineApiCode;
}
注意这里使用lombok,所以省略了get、set方法。
给注解设置message内容时这里使用的是常量类,也可直接使用字符串。
附常量类
public class Constants {
//请求响应常量
public static final String NO_API_CODE = "请求码不能为空";
public static final String ILLEGAL_API_CODE = "请求码不存在";
}
5、在Controller中接口参数添加注解
@GetMapping("/badao")
public AjaxResult badao(@Validated MyRequestParams requestParams)
此时进行接口请求时如果参数mineApiCode为空或者不存在上面枚举类的指定范围中,
则会直接提示400,bad request,会触发异常。
但是并没有实现按照我们传递的message显示不同的提示信息。
这里因为没有对异常进行捕获和处理响应。
6、新建全局异常处理类,并添加注解@RestControllerAdvice
添加该注解则会对添加了@RestController注解的类进行异常捕获和处理
@RequestMapping("test")
@RestController
public class TestController{
比如上面的接口的controller。
所以新建全局异常捕获和处理类
import com.badao.demo.common.AjaxResult;
import com.badao.demo.constant.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//注意引入包的路径
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// @ExceptionHandler(Exception.class)
// public AjaxResult handleException(Exception e)
// {
// log.error(e.getMessage(), e);
// return AjaxResult.error(e.getMessage());
// }
/**
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
public AjaxResult handleBindException(BindException e) {
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(HttpStatus.BAD_REQUEST, message);
}
/**
* 自定义验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return AjaxResult.error(message);
}
}
注意这里不光可以只对BindException和MethodArgumentNotValidException类型的异常进行捕获和处理
还可对其它类型异常、自定义异常、Exception类进行捕获和处理。
所以说怎么能知道接口校验不通过时触发的是何种类型,放开上面的
// @ExceptionHandler(Exception.class)
// public AjaxResult handleException(Exception e)
// {
// log.error(e.getMessage(), e);
// return AjaxResult.error(e.getMessage());
// }
Exception的处理,注释掉其他处理,然后在此处打断点,触发参数校验不通过时就可以看到具体的异常类型了。
当具体类型的捕获和Exception类型同时存在时,则会只触发具体类型的Handler
所以这里会具体触发BindException
但是这里为什么是
String message = e.getAllErrors().get(0).getDefaultMessage();
为什么要获取第一个的message,因为当同时触发多个错误时,需要优先取一个显示即可。
get的顺序与注解的顺序有关
这里@NotBlank在下面,则get时索引为0。
6、测试参数校验效果
参数不为空
参数不合法