我们在使用spring项目的时候,经常使用@Valid
来对入参进行校验,比如必须为空,必须不为空,长度多少,是否符合邮件格式等等,同时也可以正则。
如下图所示,
@Null(message = "id不能传入",groups = { MyValidDTOIdNull.class})
@NotNull(message = "id必须传入",groups = { MyValidDTOIdNotNull.class})
private String id;
@Length(min = 2, max = 3, message = "姓名长度为2")
private String name;
@Email(message = "电子邮件格式不正确")
private String email;
private Integer age;
@NotNull(message = "salary必须要存在",groups = { MyValidDTOIdNotNull.class})
private Long salary;
然而,使用@Valid
或者@Validated
的时候,并没有对入参为指定的值限制的注解。
工作中,一般指定的值为枚举的code,或者Integer类型为0或者1等,或者String为指定值。
现在给出具体的实现代码,可以直接使用。
以下项目地址可点击:qsm-valid
给出引用的jar
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
1、QsmSpecifiedSelector注解
指定值选择器,用于dto中想要校验的字段上。指明入参可以为哪些值,这些值可以为String类型,也可以为Integer类型,也可以为枚举的code。
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/** * @author qxx@xx.com * @date 2020/11/24 10:24 * @description 指定值选择器,用于需要校验的字段上 */
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { QsmSpecifiedValidator.class})
@Repeatable(QsmSpecifiedSelector.List.class)
public @interface QsmSpecifiedSelector {
//默认错误消息
String message() default "必须为指定值";
String[] strValues() default { };
int[] intValues() default { };
//使用指定枚举,1、使用属性命名code。2、枚举上使用QsmSpecifiedEnumValue
Class<?> enumValue() default Class.class;
//分组
Class<?>[] groups() default { };
//负载
Class<? extends Payload>[] payload() default { };
//指定多个时使用
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
QsmSpecifiedSelector[] value();
}
}
2、QsmSpecifiedValidator校验器
实现javax.validation.ConstraintValidator接口,该类是实现具体的校验规则,判断入参的值是否通过校验。
import lombok.SneakyThrows;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
/** * @author qxx@xx.com * @date 2020/11/24 10:25 * @description 指定值校验器 */
public class QsmSpecifiedValidator implements ConstraintValidator<QsmSpecifiedSelector, Object> {
private String[] strValues;
private int[] intValues;
private Class<?> cls;
@Override
public void initialize(QsmSpecifiedSelector constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
cls = constraintAnnotation.enumValue();
}
@SneakyThrows
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (null == value) {
return true;
}
if (cls.isEnum()) {
Object[] objects = cls.getEnumConstants();
for (Object obj : objects) {
//此处方法getCode需要根据自己项目枚举的命名而变化
Method method = cls.getDeclaredMethod("getCode");
String expectValue = String.valueOf(method.invoke(obj));
if (expectValue.equals(String.valueOf(value))) {
return true;
}
}
} else {
if (value instanceof String) {
for (String s : strValues) {
if (s.equals(value)) {
return true;
}
}
} else if (value instanceof Integer) {
for (Integer s : intValues) {
if (s == value) {
return true;
}
}
}
}
return false;
}
}
3、使用
controller层
@RestController
@RequestMapping("/valid")
public class ValidController {
@PostMapping("/valid")
public String valid(@RequestBody @Valid MyValidDTO validDTO) {
return "ok";;
}
}
入参dto为MyValidDTO类,指定name字段的入参必须为qsm或者hn。学生标志的入参只能为0或者1。国家为枚举Nation。
枚举代码如下
public interface SspClaimEnum {
@Getter
@AllArgsConstructor
enum Nation {
AAAA("a", "国家A"),
BBBB("b", "国家B");
private String code;
private String desc;
@Override
public String toString() {
return getCode();
}
}
}
入参dto为MyValidDTO类
@Data
public class MyValidDTO {
@QsmSpecifiedSelector(strValues = { "qsm", "hn"}, message = "姓名必须为指定值qsm,hn")
private String name;
@QsmSpecifiedSelector(intValues = { 0, 1}, message = "学生标志必须为指定值0,1")
private Integer studentFlag;
@QsmSpecifiedSelector(enumValue = SspClaimEnum.Nation.class, message = "国家必须为指定值a,b")
private String nation;
}
到此就可以了
另外,为了友好的返回给前端,我们一般全局异常捕获@RestControllerAdvice
和@ExceptionHandler
、返回使用result类,里面包含code和desc,已经数据泛型T。以后有时间再补充。
@Data
public class ReturnResult<T> {
private String code;
private String desc;
private T result;
}
【补充,2021-04-01】
为了友好的返回给前端,现在也把相关类给出。读者也可以根据自己公司的类进行相应调整。
ReturnResult
统一返回结果类
import lombok.Data;
import lombok.ToString;
/** * class description: 返回结果集 * * @author qxx@xx.com * @date **/
@Data
@ToString
public class ReturnResult<T> {
private String code;
private String desc;
private T result;
public ReturnResult() {
}
private ReturnResult(String code, String desc) {
this.code = code;
this.desc = desc;
}
private ReturnResult(String code, String desc, T result) {
this.code = code;
this.desc = desc;
this.result = result;
}
public static <T> ReturnResult<T> success(T result) {
return new ReturnResult<>("0000", "成功", result);
}
public static <T> ReturnResult<T> error(T result) {
return new ReturnResult("9999", "失败", result);
}
public static <T> ReturnResult<T> error(String code, String desc) {
return new ReturnResult(code, desc);
}
public static <T> ReturnResult<T> error() {
return new ReturnResult("9999", "失败");
}
}
GlobalExceptionHandler
统一异常处理类
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.stream.Collectors;
/** * @author qin.com * @date * @description 全局的异常处理类:接受controller抛出的所有异常。也可以分情况处理。 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = { BindException.class})
public ReturnResult customParamValidException(BindException e) {
log.warn("==================== [warn], 统一异常处理:参数校验失败, e->{}", e.getMessage());
List<String> errorMessageSet = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());
return ReturnResult.error("9999", errorMessageSet.toString());
}
@ExceptionHandler(value = { ValidationException.class})
public ReturnResult customParamValidException(ValidationException e) {
log.warn("==================== [warn], 统一异常处理:参数校验失败, e->{}", e.getMessage());
return ReturnResult.error("9999", e.getMessage());
}
@ExceptionHandler(Exception.class)
public ReturnResult customException(Exception e) {
log.error(" [error], 统一异常处理:EXCEPTION, e->{} ", e.getMessage(), e);
return ReturnResult.error();
}
}
调用url:http://localhost:8080/valid/valid
入参
{
"name": "demoData",
"studentFlag": 3,
"nation": "c"
}
返参
{
"code": "9999",
"desc": "[姓名必须为指定值qsm,hn, 国家必须为指定值a,b, 学生标志必须为指定值0,1]",
"result": null
}
【补充,2021-04-08】
若对使用@PathVariable参数进行校验,则需要在类的上面使用注解@Validated
,和在全局异常处理类中加入对ValidationException
异常的处理,已加入到上面GlobalExceptionHandler
类中。
具体使用如下
@RestController
@RequestMapping("/valid2")
@Validated
public class ValidTwoController {
@GetMapping("/update/state/{id}/{state}")
public String valid(
@PathVariable(name = "id") @QsmSpecifiedSelector(strValues = { "qsm", "hn"}, message = "姓名必须为指定值qsm或hn") String id,
@PathVariable(name = "state") @QsmSpecifiedSelector(strValues = { "success", "fail"}, message = "状态必须为指定值success或fail") String state) {
return id + state;
}
}
访问url,get请求http://localhost:8080/valid2/update/state/aaa/bbb
得到结果为
{
"code": "9999",
"desc": "valid.id: 姓名必须为指定值qsm或hn, valid.state: 状态必须为指定值success或fail"
}
【补充,2021-04-02】
若仅仅使用,上面知识点即可。
为了更好的知道其校验的底层原理,可以看源码可以得到。
现在简述如下:
看此处源码,应当知道DispatcherServlet#doDispatch的大致过程,如若还不清楚,可以查看SpringBoot源码——请求全过程源码分析——一步一步详细分析
此处校验参数,发生在第四步,即mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
由于源码路径深度较多,因此现在分析关键的断点。
断点一、org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
的Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
处,这里的意思就是去处理入参,入参处理很多步骤,比如类型转化,数据复制,参数校验等。我们这里关注参数校验。
断点二、org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
的binder.validate(validationHints);
,注意到WebDataBinder binder
这个类重要,作用就是处理参数的。
断点三,org.springframework.validation.DataBinder#validate(java.lang.Object...)
的((SmartValidator) validator).validate(target, bindingResult, validationHints);
处,这里代表入参开始规则校验。
断点四,org.hibernate.validator.internal.engine.ValidatorImpl#validateConstraintsForSingleDefaultGroupElement
的for ( MetaConstraint<?> metaConstraint : metaConstraints ) {
,这个地方我们发现有3处。查看可以得到,第一个就是nation字段,并且可以看到根据属性上面QsmSpecifiedSelector
注解,得到了对应的校验器QsmSpecifiedValidator
。
其中boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
处开始执行校验的规则了。
断点五,org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#createAndInitializeValidator
的constraintValidator = validatorDescriptor.newInstance( constraintFactory );
处和紧邻下面一句的initializeValidator( descriptor, constraintValidator );
。
第一句使用了newInstance
,原因是,我们写的QsmSpecifiedValidator
校验器的属性都是私有的,从属于对象,因此每个请求线程都会新建自己的校验器对象,保证了并发的安全性。
断点六,就是我们的校验器的initialize
初始化方法。也就是赋值而已,以便将这些值用于之后执行isValid的逻辑。
断点七、org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateSingleConstraint
处的isValid = validator.isValid( validatedValue, constraintValidatorContext );
就是执行我们的校验器的isValid
方法。可以得到isValid为false
。
接下来,重复执行断点四的逻辑,以为有3个字段需要校验,所以还有2遍执行。3遍执行完成之后。
断点八,org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument
处的throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
,由于存在校验失败的参数,所以这里就抛出异常了。
断点九,org.springframework.web.servlet.DispatcherServlet#processDispatchResult
这是就是处理mv和异常的位置。
断点十,org.springframework.web.servlet.DispatcherServlet#processHandlerException
处的for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
,第二个循环的resolver处理逻辑
断点十一,org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
处的红色框框中
断点十二,进入getExceptionHandlerMethod
方法之后,最后处可以看到,返回了ServletInvocableHandlerMethod
,其中有看到我们写的全局异常处理类GlobalExceptionHandler
,找到看处理异常MethodArgumentNotValidException
父类BindException
的方法
断点十三,org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
的exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
,也就是说,得到了ServletInvocableHandlerMethod
之后,就代表执行这个我们写的全局异常类里面的方法customParamValidException
。
执行完方法,就返回json格式的数据给前端。
至此,源码分析到此结束了。
源码简述如下
校验类所有的属性,根据属性上的我们写的
@QsmSpecifiedSelector
注解,得到了校验器QsmSpecifiedValidator
,校验器校验返回false,之后就抛出了异常。然后被异常解析器下面的我们写的全局异常处理类GlobalExceptionHandler
处理,执行响应异常处理的方法逻辑,最后返回前端ReturnResult
类了。
【完】
喜欢就点个赞呗
正在去bat的路上修行