准备工作
引入相关依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
约束性注解(简单)说明
@AssertFalse | 可以为null,如果不为null的话必须为false |
@AssertTrue | 可以为null,如果不为null的话必须为true |
@DecimalMax | 设置不能超过最大值 |
@DecimalMin | 设置不能超过最小值 |
@Digits | 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
@Future | 日期必须在当前日期的未来 |
@Past | 日期必须在当前日期的过去 |
@Max | 最大不得超过此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能为null,可以是空 |
@Null | 必须为null |
@Pattern | 必须满足指定的正则表达式 |
@Size | 集合、数组、map等的size()值必须在指定范围内 |
必须是email格式 | |
@Length | 长度必须在指定范围内 |
@NotBlank | 字符串不能为null,字符串trim()后也不能等于“” |
@NotEmpty | 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“” |
@Range | 值必须在指定范围内 |
@URL | 必须是一个URL |
param:
@Data
public class User {@NotNull(message = "Name cannot be null")private String name;@Min(value = 18, message = "Age must be at least 18")private int age;}
test:
@Testvoid test() {ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();Validator validator = validatorFactory.getValidator();User user = new User();user.setName(null); // 故意设置为 nulluser.setAge(15); // 故意设置为小于 18Set<ConstraintViolation<User>> validate = validator.validate(user);for (ConstraintViolation<User> violation : validate) {System.out.println(violation.getMessage());}}
ValidatorFactory
-
ValidatorFactory
是一个工厂类,用于创建Validator
实例。 -
它负责管理验证器的生命周期,并提供默认的验证器配置。
2. Validation.buildDefaultValidatorFactory()
-
Validation
是一个静态类,提供了构建默认ValidatorFactory
的方法。 -
buildDefaultValidatorFactory()
方法会根据默认的配置(如hibernate-validator
或其他实现)创建一个ValidatorFactory
。
3. validatorFactory.getValidator()
-
通过
ValidatorFactory
获取一个Validator
实例。 -
Validator
是实际执行验证操作的接口,用于验证 Java Bean 中的约束(如@NotNull
、@Size
等注解)。
@Validated的使用时机
@Validated的使用位置较多(可详见源码),但其主流的使用位置却是以下两种:
1、在Controller层中,放在模型参数对象前。
当Controller层中参数是一个对象模型时,只有将@Validated直接放在该模型前,该模型内部的字段才会被校验(如果有对该模型的字段进行约束的话)。
2、在Controller层中,放在类上。
当一些约束是直接出现在Controller层中的参数前时,只有将@Validated放在类上时,参数前的约束才会生效
import com.aspire.model.ValidationBeanModel;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.validation.constraints.DecimalMax;/*** Controller层 --- 初步简单测试 @Validated 的使用位置** 对比测试过程:* 方案一 : 不在类上加@Validated注解,访问这六个接口* 方案二 : 在类上加@Validated注解,再次访问这六个接口** 对比方案一和方案二,可初步得出@Validated的使用时机:* 1.当我们是在模型里面对模型字段添加约束注解,在Controller中使用模型接收数* 据时,@Validated要直接放在该模型参数前才有效。 如: "/test/one"* 2.当我们是直接在Controller层中的参数前,使用约束注解时,@Validated要直接放在类上,* 才会有效。如: /test/six*** @author JustryDeng* @date 2019/1/18 22:22*/
@RestController
@Validated
public class JustryDengController {@RequestMapping(value = "/test/one")public String validatioOne(@Validated ValidationBeanModel.AbcDecimalMax myDecimalMax) {System.out.println(myDecimalMax.getMyDecimalMax());return "one pass!";}@RequestMapping(value = "/test/two")@Validatedpublic String validatioTwo(ValidationBeanModel.AbcDecimalMax myDecimalMax) {System.out.println(myDecimalMax.getMyDecimalMax());return "two pass!";}@RequestMapping(value = "/test/three")public String validatioThree(ValidationBeanModel.AbcDecimalMax myDecimalMax) {System.out.println(myDecimalMax.getMyDecimalMax());return "three pass!";}@RequestMapping(value = "/test/four")public String validatioFour(@Validated @DecimalMax(value = "12.3") String myDecimalMax) {System.out.println(myDecimalMax);return "four pass!";}@RequestMapping(value = "/test/five")@Validatedpublic String validatioFive(@DecimalMax(value = "12.3") String myDecimalMax) {System.out.println(myDecimalMax);return "five pass!";}@RequestMapping(value = "/test/six")@Validatedpublic String validatioSix(@DecimalMax(value = "12.3") String myDecimalMax) {System.out.println(myDecimalMax);return "six pass!";}
}
@Validated与@Valid的简单对比说明
@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:@Valid属于javax下的,而@Validated属于spring下;@Valid支持嵌套校验、而@Validated不支持,@Validated支持分组,而@Valid不支持。笔者这里只简单介绍@Validated的使用时机。
自定义注解
虽然Bean Validation和Hibernate Validator已经提供了非常丰富的校验注解,但是在实际业务中,难免会碰到一些现有注解不足以校验的情况;这时,我们可以考虑自定义Validation注解。
第一步:创建自定义注解
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.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint;import static java.lang.annotation.ElementType.FIELD;/*** 自定义校验注解* 提示:* 1、message、contains、payload是必须要写的* 2、还需要什么方法可根据自己的实际业务需求,自行添加定义即可** 注:当没有指定默认值时,那么在使用此注解时,就必须输入对应的属性值** @author JustryDeng* @date 2019/1/15 1:17*/
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
// 指定此注解的实现,即:验证器
@Constraint(validatedBy ={MyValidationImpl.class})
public @interface MyValidation {// 当验证不通过时的提示信息String message() default "校验失败";// 根据实际需求定的方法String contains() default "";// 约束注解在验证时所属的组别Class<?>[] groups() default { };// 负载Class<? extends Payload>[] payload() default { };
}
第二步:编写(第一步中的校验器实现类)该注解
import org.hibernate.validator.internal.engine.ValidationContext;
import org.hibernate.validator.internal.engine.ValueContext;
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree;import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;/*** ConstraintsJustryDeng注解 校验器 实现* <p>* 注:验证器需要实现ConstraintValidator<U, V>, 其中 U为对应的注解类, V为被该注解标记的字段的类型(或其父类型)** 注: 当项目启动后,会(懒加载)创建ConstraintValidator实例,在创建实例后会初始化调* 用{@link ConstraintValidator#initialize}方法。* 所以, 只有在第一次请求时,会走initialize方法, 后面的请求是不会走initialize方法的。** 注: (懒加载)创建ConstraintValidator实例时, 会走缓存; 如果缓存中有,则直接使用相* 同的ConstraintValidator实例; 如果缓存中没有,那么会创建新的ConstraintValidator实例。* 由于缓存的key是能唯一定位的, 且 ConstraintValidator的实例属性只有在* {@link ConstraintValidator#initialize}方法中才会写;在{@link ConstraintValidator#isValid}* 方法中只是读。* 所以不用担心线程安全问题。** 注: 如何创建ConstraintValidator实例的,可详见源码* @see ConstraintTree#getInitializedConstraintValidator(ValidationContext, ValueContext)** @author JustryDeng* @date 2019/1/15 1:19*/
public class MyValidationImpl implements ConstraintValidator<MyValidation, Object> {/** 错误提示信息 */private String contains;/*** 初始化方法, 在(懒加载)创建一个当前类实例后,会马上执行此方法** 注: 此方法只会执行一次,即:创建实例后马上执行。** @param constraintAnnotation* 注解信息模型,可以从该模型中获取注解类中定义的一些信息,如默认值等* @date 2019/1/19 11:27*/@Overridepublic void initialize(MyValidation constraintAnnotation) {System.out.println(constraintAnnotation.message());this.contains = constraintAnnotation.contains();}/*** 校验方法, 每个需要校验的请求都会走这个方法** 注: 此方法可能会并发执行,需要根据实际情况看否是需要保证线程安全。** @param value* 被校验的对象* @param context* 上下文** @return 校验是否通过*/@Overridepublic boolean isValid(Object value, ConstraintValidatorContext context) {if (value == null) {return false;}if (value instanceof String) {String strMessage = (String) value;return strMessage.contains(contains);} else if (value instanceof Integer) {return contains.contains(String.valueOf(value));}return false;}}
第三步:自定义注解简单使用测试
@Testvoid test2() {ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();Validator validator = validatorFactory.getValidator();User user = new User();user.setName("ddd"); // 故意设置为 nulluser.setAge(15); // 故意设置为小于 18Set<ConstraintViolation<User>> validate = validator.validate(user);for (ConstraintViolation<User> violation : validate) {System.out.println(violation.getMessage());}}
@Data
public class User {@MyValidation(message = "校验测试",contains = "a")private String name;@Min(value = 18, message = "Age must be at least 18")private int age;}
效果:
对注解抛出的异常进行处理
说明:当注解校验不通过时,直接将异常信息返回给前端其实并不友好,我们可以将异常包装一下,返回给前端。
情况一:使用BindingResult类来容纳异常信息,当校验不通过时,不影响正常程
序往下走。我们只需要处理BindingResult中的异常信息即可。
处理前:
参数模型是这样的:
Controller是这样的
使用postman测试,当校验不通过时显示如下:
处理后:
参数模型是这样的(没有变):
Controller是这样的(在@Validated注解的参数后,紧接着加上BindingResult):
再次使用postman测试,当校验不通过时显示如下:
postman中返回了数据,说明虽然参数错误,但是不影响程序的执行。
程序在控制台输出了如下信息:
可见,后台已经获悉了错误,至于怎么处理,这就看伟大的程序员们了。
情况二(推荐):通过SpringMVC全局异常处理器来处理异常。
描述:如果不采用BindingResult来容纳异常信息时,那么异常会被向外抛出。注解校验不通过时,可能抛出的异常有BindException异常、ValidationException异常(或其子类异常)、
MethodArgumentNotValidException异常。
处理前:
示例一:Controller和对应的参数模型是这样的:
使用postman测试,当校验不通过时显示如下:
示例二:Controller是这样的:
使用postman测试,当校验不通过时显示如下:
注:ConstraintViolationException异常是ViolationException异常的子异常。
示例三:Controller是这样的:
注:此处的User模型与情况一里给出的模型是一样的,这里就不再给出了。
使用postman测试,当校验不通过时显示如下:
进行处理:加入全局异常处理器:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;/*** SpringMVC统一异常处理** @author JustryDeng* @date 2019/10/12 16:28*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 处理Validated校验异常* <p>* 注: 常见的ConstraintViolationException异常, 也属于ValidationException异常** @param e* 捕获到的异常* @return 返回给前端的data*/@ResponseStatus(code = HttpStatus.BAD_REQUEST)@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})public Map<String, Object> handleParameterVerificationException(Exception e) {log.error(" handleParameterVerificationException has been invoked", e);Map<String, Object> resultMap = new HashMap<>(4);resultMap.put("code", "100001");String msg = null;/// MethodArgumentNotValidExceptionif (e instanceof MethodArgumentNotValidException) {BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();// getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)FieldError fieldError = bindingResult.getFieldError();if (fieldError != null) {msg = fieldError.getDefaultMessage();}/// BindException} else if (e instanceof BindException) {// getFieldError获取的是第一个不合法的参数(P.S.如果有多个参数不合法的话)FieldError fieldError = ((BindException) e).getFieldError();if (fieldError != null) {msg = fieldError.getDefaultMessage();}/// ValidationException 的子类异常ConstraintViolationException} else if (e instanceof ConstraintViolationException) {/** ConstraintViolationException的e.getMessage()形如* {方法名}.{参数名}: {message}* 这里只需要取后面的message即可*/msg = e.getMessage();if (msg != null) {int lastIndex = msg.lastIndexOf(':');if (lastIndex >= 0) {msg = msg.substring(lastIndex + 1).trim();}}/// ValidationException 的其它子类异常} else {msg = "处理参数时异常";}resultMap.put("msg", msg);return resultMap;}}
处理后,使用postman再次进行上述两个请求:
可见,异常处理成功!
经常存在加参数校验后就不加触发注解的情况,前一段公司在做国际化的时候就发现了两个服务一个参数注解都没触发的情况,让人痛心疾首,那么有没有可以不加参数注解就能自动为我们触发参数方法呢?
AOP实现触发参数校验
公司部分服务是这样做的
/**** @Description:参数效验拦截器* Author Version Date Changes* zjf 1.0 2020年11月30日 Created*/
@Aspect
@Component
@Slf4j
public class ValidateParameterAspectAdvice implements Ordered {@Autowiredprivate ParamCheckSpringUtils paramCheckSpringUtils;/*** (non-Javadoc)* @see Ordered#getOrder()*/@Overridepublic int getOrder() {return 1000;}@Before("execution(* com.echronos.iform.feign.*.*(..)) || execution(* com.echronos.iform.controller.*.*(..))")public void before(JoinPoint pjd) {Object[] args = pjd.getArgs();if (args.length > 0) {Object oneParam = args[0];MethodSignature signature = (MethodSignature) pjd.getSignature();Method method = signature.getMethod();Annotation[][] parameterAnnotations = method.getParameterAnnotations();List<Class<?>> list = new ArrayList<>();for (Annotation[] annotations : parameterAnnotations) {for (Annotation annotation : annotations) {if (annotation instanceof ConvertGroup) {list.add(Default.class);ConvertGroup convertGroup = (ConvertGroup) annotation;Class<?> curClass = convertGroup.to();list.add(curClass);}else if (annotation instanceof ConvertGroup.List) {ConvertGroup.List convertGroupList = (ConvertGroup.List) annotation;ConvertGroup[] groups = convertGroupList.value();for (ConvertGroup convertGroup : groups) {Class<?> curClass = convertGroup.to();list.add(curClass);}}}}Class<?> targetClass = method.getDeclaringClass();log.info("{}方法,入参为:{}", targetClass.getName() + "#" + method.getName(), FastJsonUtils.toJSONNoFeatures(oneParam));//异常处理String check = null;if(list.size() > 1){Class[] arrClass = list.toArray(new Class[list.size()]);check = paramCheckSpringUtils.checkParam(oneParam,arrClass);}else{check = paramCheckSpringUtils.checkParam(oneParam);}if (check != null) {throw new ParamsValidateException(CommonResultCode.CommonResultEnum.BAD_REQUEST.getCode(), check);}}}
}
@Component
public class ParamCheckSpringUtils {@Autowiredprivate Validator validator;public ParamCheckSpringUtils() {}public <T> String checkParam(final T obj, final Class<?>... group) {List<String> list = new ArrayList();Set<ConstraintViolation<T>> constraintViolations = this.validator.validate(obj, (Class[])group);StringBuilder strBuilder = new StringBuilder();if (constraintViolations != null && constraintViolations.size() > 0) {Iterator var6 = constraintViolations.iterator();while(var6.hasNext()) {ConstraintViolation<T> cv = (ConstraintViolation)var6.next();if (!list.contains(String.valueOf(cv.getPropertyPath()))) {list.add(String.valueOf(cv.getPropertyPath()));strBuilder.append(cv.getMessage()).append(";");}}return strBuilder.toString();} else {return null;}}public <T> String checkParam(final T obj, final Class<?> group, final String... args) {StringBuilder strBuilder = new StringBuilder("");if (args != null) {String[] var5 = args;int var6 = args.length;for(int var7 = 0; var7 < var6; ++var7) {String param = var5[var7];Set<ConstraintViolation<T>> constraintViolations = this.validator.validateProperty(obj, param, new Class[]{group});if (constraintViolations != null && constraintViolations.size() > 0) {Iterator var10 = constraintViolations.iterator();while(var10.hasNext()) {ConstraintViolation<T> cv = (ConstraintViolation)var10.next();strBuilder.append(cv.getMessage()).append(";");}}}}return strBuilder.toString().equals("") ? null : strBuilder.toString();}
}