每天一个摆脱if-else工程师的技巧——优雅的参数校验
阿里妹导读
一、前言
@RestControllerpublic class TestController {private static final Pattern ID_CARD_PATTERN = Pattern.compile("(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)");private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");@RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)public Result<Boolean> saveUser(UserRequest user) {if (StringUtils.isBlank(user.getUserName())) {throw new IllegalArgumentException("用户姓名不能为空");}if (Objects.isNull(user.getGender())) {throw new IllegalArgumentException("性别不能为空");}if (Objects.isNull(GenderType.getGenderType(user.getGender()))) {throw new IllegalArgumentException("性别错误");}if (Objects.isNull(user.getAge())) {throw new IllegalArgumentException("年龄不能为空");}if (user.getAge() < 0 || user.getAge() > 150) {throw new IllegalArgumentException("年龄必须在0-150之间");}if (StringUtils.isBlank(user.getIdCard())) {throw new IllegalArgumentException("身份证号不能为空");}if (!ID_CARD_PATTERN.matcher(user.getIdCard()).find()) {throw new IllegalArgumentException("身份证号格式错误");}if (StringUtils.isBlank(user.getMobilePhone())) {throw new IllegalArgumentException("手机号不能为空");}if (!MOBILE_PHONE_PATTERN.matcher(user.getIdCard()).find()) {throw new IllegalArgumentException("手机号格式错误");}// 省略其他业务代码return new ResponseEntity<>(HttpStatus.OK);}}
二、实践
2.1、Controller方法参数校验
2.1.1、效果示例
@Datapublic class UserRequest {@NotBlank(message = "用户ID不能为空")private String userId;@NotBlank(message = "电话号码不能为空")@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "电话号码格式错误")private String mobilePhone;@Min(message = "年龄必须大于0", value = 0)@Max(message = "年龄不能超过150", value = 150)private Integer age;@NotNull(message = "用户详情不能为空")@Validprivate UserDetail userDetail;//省略其他参数}
@RestControllerpublic class TestController {@RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)public ResponseEntity<BaseResult> saveUser(@Validated @RequestBody UserRequest user) {// 省略其他业务代码return new ResponseEntity<>(HttpStatus.OK);}}
{"timestamp": 1666777674977,"status": 400,"error": "Bad Request","exception": "org.springframework.web.bind.MethodArgumentNotValidException","errors": [{"codes": ["NotBlank.UserRequest.mobilePhone","NotBlank.mobilePhone","NotBlank.java.lang.String","NotBlank"],"arguments": [{"codes": ["UserRequest.mobilePhone","mobilePhone"],"arguments": null,"defaultMessage": "mobilePhone","code": "mobilePhone"}],"defaultMessage": "电话号码不能为空","objectName": "UserRequest","field": "mobilePhone","rejectedValue": null,"bindingFailure": false,"code": "NotBlank"}],"message": "Validation failed for object='UserRequest'. Error count: 1","path": "/api/saveUser"}
@RestControllerAdvicepublic class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public BaseResult handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {FieldError fieldError = e.getBindingResult().getFieldErrors().get(0);return new BaseResult(CommonResultCode.ILLEGAL_PARAMETERS.getErrorCode(),"入参中的" + fieldError.getField() + fieldError.getDefaultMessage(), EagleEye.getTraceId());}}
{"success": false,"errorCode": "ILLEGAL_PARAMETERS","errorMessage": "入参中的mobilePhone电话号码不能为空","traceId": "1ef9749316674663696111017d73c9","extInfo": {}}
2.1.2、@Valid 和 @Validated
@Valid [3]注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated [4]注解,是 Spring Validation 所定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。
2.1.3、分组校验
@Datapublic class UserRequest {@NotBlank(message = "用户ID不能为空", groups = {UpdateUser.class})private String userId;@NotBlank(message = "电话号码不能为空", groups = {UpdateUser.class, InsertUser.class})@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "电话号码格式错误")private String mobilePhone;@Min(message = "年龄必须大于0", value = 0, groups = {UpdateUser.class, InsertUser.class})@Max(message = "年龄不能超过150", value = 150, groups = {UpdateUser.class, InsertUser.class})private Integer age;@NotNull(message = "用户详情不能为空", groups = {UpdateUser.class, InsertUser.class})@Validprivate UserDetail userDetail;//省略其他参数}
@RestControllerpublic class TestController {@RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)public ResponseEntity<BaseResult> saveUser(@Validated(value = InsertUser.class) @RequestBody UserRequest user) {// 省略其他业务代码return new ResponseEntity<>(HttpStatus.OK);}}
2.1.4、自定义校验注解
由 @Constraint 注解的注解。
实现了 javax.validation.ConstraintValidator 的 validator。
public enum GenderEnum implements BasicEnum {male("male", "男"),female("female", "女");private String code;private String desc;// 省略其他}
@Target({ElementType.METHOD, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = InEnumValidator.class)public @interface InEnum {/*** 枚举类型** @return*/Class<? extends BasicEnum> enumType();String message() default "枚举类型不匹配";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };}
public class InEnumValidator implements ConstraintValidator<InEnum, Object> {private Class<? extends BasicEnum> enumType;@Overridepublic void initialize(InEnum inEnum) {enumType = inEnum.enumType();}@Overridepublic boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {if (object == null) {return true;}if (enumType == null || !enumType.isEnum()) {return false;}for (BasicEnum basicEnum : enumType.getEnumConstants()) {if (basicEnum.getCode().equals(object)) {return true;}}return false;}}
@Datapublic class UserRequest {@InEnum(enumType = GenderEnum.class, message = "用户性别不在枚举范围内")private String gender;//省略其他参数}
{"success": false,"errorCode": "ILLEGAL_PARAMETERS","errorMessage": "入参中的gender用户性别不在枚举范围内","traceId": "1ef9749316674663696111017d73c9","extInfo": {}}
2.1.5、校验原理
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {private Class<? extends Annotation> validatedAnnotationType = Validated.class;// 这个是javax.validation.Validatorprivate Validator validator;// 可以传入自定义注解public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");this.validatedAnnotationType = validatedAnnotationType;}// 默认情况下,使用LocalValidatorFactoryBean进行校验,也可以传入自定义的Validatorpublic void setValidator(Validator validator) {// Unwrap to the native Validator with forExecutables supportif (validator instanceof LocalValidatorFactoryBean) {this.validator = ((LocalValidatorFactoryBean) validator).getValidator();}else if (validator instanceof SpringValidatorAdapter) {this.validator = validator.unwrap(Validator.class);}else {this.validator = validator;}}// 可以传入自定义ValidatorFactorypublic void setValidatorFactory(ValidatorFactory validatorFactory) {this.validator = validatorFactory.getValidator();}@Overridepublic void afterPropertiesSet() {Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));}// 使用MethodValidationInterceptor切面protected Advice createMethodValidationAdvice(Validator validator) {return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());}}
public class MethodValidationInterceptor implements MethodInterceptor {//xxx@Override@SuppressWarnings("unchecked")public Object invoke(MethodInvocation invocation) throws Throwable {// 如果是FactoryBean.getObject() 方法,就不要校验了if (isFactoryBeanMetadataMethod(invocation.getMethod())) {return invocation.proceed();}// 获取方法上@Validated注解里的分组信息Class<?>[] groups = determineValidationGroups(invocation);if (forExecutablesMethod != null) {// Standard Bean Validation 1.1 APIObject execVal;try {execVal = ReflectionUtils.invokeMethod(forExecutablesMethod, this.validator);}catch (AbstractMethodError err) {Validator nativeValidator = this.validator.unwrap(Validator.class);execVal = ReflectionUtils.invokeMethod(forExecutablesMethod, nativeValidator);this.validator = nativeValidator;}Method methodToValidate = invocation.getMethod();Set<ConstraintViolation<?>> result;// 对方法的参数进行验证,验证结果保存在 result 中,如果验证失败,result 不为空,// 此时会抛出异常 ConstraintViolationExceptiontry {result = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateParametersMethod,execVal, invocation.getThis(), methodToValidate, invocation.getArguments(), groups);}catch (IllegalArgumentException ex) {// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011// Let's try to find the bridged method on the implementation class...methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));result = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateParametersMethod,execVal, invocation.getThis(), methodToValidate, invocation.getArguments(), groups);}if (!result.isEmpty()) {throw new ConstraintViolationException(result);}// 调用目标方法Object returnValue = invocation.proceed();// 对方法执行的返回值进行验证 ,验证结果保存在 result 中,如果验证失败,result 不为空,// 此时会抛出异常 ConstraintViolationExceptionresult = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateReturnValueMethod,execVal, invocation.getThis(), methodToValidate, returnValue, groups);if (!result.isEmpty()) {throw new ConstraintViolationException(result);}return returnValue;}else {// Hibernate Validator 4.3's native APIreturn HibernateValidatorDelegate.invokeWithinValidation(invocation, this.validator, groups);}}//xxx}
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {// 约束注解必要的三个成员属性private static final Set<String> internalAnnotationAttributes = new HashSet<String>(3);static {// 命中约束的错误提示信息internalAnnotationAttributes.add("message");// 分组校验使用internalAnnotationAttributes.add("groups");// 负载internalAnnotationAttributes.add("payload");}private javax.validation.Validator targetValidator;// 创建一个适配器public SpringValidatorAdapter(javax.validation.Validator targetValidator) {Assert.notNull(targetValidator, "Target Validator must not be null");this.targetValidator = targetValidator;}SpringValidatorAdapter() {}void setTargetValidator(javax.validation.Validator targetValidator) {this.targetValidator = targetValidator;}@Overridepublic boolean supports(Class<?> clazz) {return (this.targetValidator != null);}// 调用校验器校验目标对象,把Validator校验的结果-ConstraintViolations错误信息,都放在Errors的BindingResult里// 总之,就是把失败信息对象转换成spring内部的验证失败信息对象@Overridepublic void validate(Object target, Errors errors) {if (this.targetValidator != null) {processConstraintViolations(this.targetValidator.validate(target), errors);}}// 省略其他}
2.2、Service方法参数校验
2.2.1、效果示例
如果入参是平铺的参数
public interface SchedulerServiceClient {/*** 获取应用不同环境的所有定时任务* @param appName 应用名称* @param env 环境* @param status 任务状态* @param userId 用户工号* @return*/List<JobConfigInfo> queryJobList(@NotBlank(message = "应用名称不能为空")String appName,@NotBlank(message = "环境不能为空")String env,Integer status,@NotBlank(message = "用户id不能为空")String userId);}
@Component@Slf4j(topic = "BIZ-SERVICE")@HSFProvider(serviceInterface = SchedulerServiceClient.class, clientTimeout = 3000)@Validatedpublic class SchedulerServiceClientImpl implements SchedulerServiceClient {@Override@Log(type = LogSourceEnum.SERVICE)public List<JobConfigInfo> queryJobList(String appName, String env, Integer status, String userId) {// 省略业务代码}
如果入参是对象
@Datapublic class CreateDingNotificationRequest extends ToString {/*** 通知类型*/@NotNull(message = "通知类型不能为空")@InEnum(enumType = ProcessControlDingTypeEnum.class, message = "通知类型不在枚举值范围内")private String dingType;// 省略其他}
public interface ProcessControlDingService {/*** 发送钉钉通知* @param request* @return**/void createDingNotification(@Valid CreateDingNotificationRequest request);}
@Component@HSFProvider(serviceInterface = ProcessControlDingService.class, clientTimeout = 5000)@Validatedpublic class ProcessControlDingServiceImpl implements ProcessControlDingService {private static final Logger LOGGER = LoggerFactory.getLogger(LoggerNames.BIZ_SERVICE);@Autowiredprivate ProcessControlTaskService processControlTaskService;@Override@Log(type = LogSourceEnum.SERVICE)public void createDingNotification(CreateDingNotificationRequest request) {// 省略业务代码}}
2.2.2、较简洁的方式-FastValidatorUtils
// 返回 bean 中所有约束违反约束校验结果Set<ConstraintViolation<T>> violationSet = FastValidatorUtils.validate(bean);
@Aspect@Component@Slf4j(topic = "BIZ-SERVICE")public class RequestValidAspect {@Around("@annotation(requestValid)")public Object around(ProceedingJoinPoint joinPoint, RequestValid requestValid) throws Throwable {// 获取方法入参、入参类型、出参类型Object[] args = joinPoint.getArgs();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Class<?>[] parameterTypes = signature.getParameterTypes();Class<?> returnType = signature.getMethod().getReturnType();// 调用前校验每个入参for (Object arg : args) {if (arg == null) {continue;}try {if (arg instanceof List && ((List<?>) arg).size() > 0) {for (int j = 0; j < ((List<?>) arg).size(); j++) {validate(((List<?>) arg).get(j));}} else {validate(arg);}} catch (AlscBoltBizValidateException e) {// 将异常处理为需要的格式返回}}// 方法运行后校验是否有入参约束Object result;try {result = joinPoint.proceed();} catch (ConstraintViolationException e) {// 将异常处理为需要的格式返回}return result;}public static <T> void validate(T t) {try {Set<ConstraintViolation<T>> res = FastValidatorUtils.validate(t);if (!res.isEmpty()) {ConstraintViolation<T> constraintViolation = res.iterator().next();FastValidatorHelper.throwFastValidateException(constraintViolation);}LoggerUtil.info(log, "validator,校验成功");} catch (FastValidatorException e) {LoggerUtil.error(log, "validator,校验报错,request=[{}],result=[{}]", JSON.toJSONString(t),e.getMessage());throw new AlscBoltBizValidateException(CommonResultCode.ILLEGAL_PARAMETERS, e.getMessage());}}}
@Override@RequestValidpublic boolean saveCheckResult(List<CheckResultInfoModel> models) {//xxxx}
三、最后
参考链接:
[1]https://beanvalidation.org/specification/
[2]https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation-beanvalidation
[3]https://docs.oracle.com/javaee/7/api/javax/validation/Valid.html
[4]https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java