spring入参为指定值,校验java入参的值为规定的值,利用Validator指定值校验注解——一看就会

2021年9月20日 23点热度 0条评论 来源: 可乐多点冰

我们在使用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#invokeAndHandleObject returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);处,这里的意思就是去处理入参,入参处理很多步骤,比如类型转化,数据复制,参数校验等。我们这里关注参数校验。

断点二、org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicablebinder.validate(validationHints);,注意到WebDataBinder binder这个类重要,作用就是处理参数的。

断点三,org.springframework.validation.DataBinder#validate(java.lang.Object...)((SmartValidator) validator).validate(target, bindingResult, validationHints);处,这里代表入参开始规则校验。

断点四,org.hibernate.validator.internal.engine.ValidatorImpl#validateConstraintsForSingleDefaultGroupElementfor ( MetaConstraint<?> metaConstraint : metaConstraints ) { ,这个地方我们发现有3处。查看可以得到,第一个就是nation字段,并且可以看到根据属性上面QsmSpecifiedSelector注解,得到了对应的校验器QsmSpecifiedValidator
其中boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );处开始执行校验的规则了。


断点五,org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#createAndInitializeValidatorconstraintValidator = 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#doResolveHandlerMethodExceptionexceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);,也就是说,得到了ServletInvocableHandlerMethod之后,就代表执行这个我们写的全局异常类里面的方法customParamValidException

执行完方法,就返回json格式的数据给前端。

至此,源码分析到此结束了。
源码简述如下

校验类所有的属性,根据属性上的我们写的@QsmSpecifiedSelector注解,得到了校验器QsmSpecifiedValidator,校验器校验返回false,之后就抛出了异常。然后被异常解析器下面的我们写的全局异常处理类GlobalExceptionHandler处理,执行响应异常处理的方法逻辑,最后返回前端ReturnResult类了。

【完】
喜欢就点个赞呗

正在去bat的路上修行

    原文作者:可乐多点冰
    原文地址: https://blog.csdn.net/u013541707/article/details/110863527
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。