参数校验
需求
参数校验代码繁复
P.throwMessageExceptionIf(null == planId, 200, "planId 不能为空");
P.throwMessageExceptionIf(null == contentType, 200, "内容类型不能为空");
P.throwMessageExceptionIf(null == jumpType, 200, "跳转类型不能空");
P.throwMessageExceptionIf(StringUtils.isEmpty(fireworkName), 200, "弹屏命名不能为空");
P.throwMessageExceptionIf(null == h5Type && StringUtil.isEmpty(jumpUrl), 200, "跳转链接不能为空");
已有轮子
javax.validation
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
org.springframework.validation
spring自带
spring boot 中使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果有了spring-boot-starter-web就不需要spring-boot-starter-validation
基本使用
1. 字段注解
注解修饰字段,表示一种校验方式
部分注解功能
//修饰的字段在验证时必须不为null,否则验证无法通过。
@NotNull(message = "can't be null")
private String name;
////修饰的字段在验证时必须不为空,否则验证无法通过。
@NotEmpty(message = "can't be empty")
private String name;
//如下代码表示,修饰的字段长度不能超过5或者低于。
@Size(min = 1, max = 5)
private List<String> names;
//如下代码表示,该字段的最大值为19,否则无法通过验证。
@Max(value = 19)
private Integer age;
//同理,被该注解修饰的字段的最小值,不能低于某个值。
@Min(value = 1)
private Integer age;
//验证数字的最大值。
@DecimalMax(value = "12.35")
private double money;
//验证数字的最小值。
@DecimalMin(value = "0.3")
private double money;
//用于验证字段是否与给定的正则相匹配。
@Pattern(regexp = "[abc]")
private String name;
注解通用属性
- message, string, 校验失败后的报错信息
- group, interface.class, 分组功能(默认为Default.class) 即可以在校验时指定分组,针对不同的校验需求,采取不同策略。如下面代码,指定TestGroup.class进行校验时, 只会对appId进行校验。
-
一个简单的校验实体类
@Data public class AppPublish { @NotNull(message = "appId is null", groups = TestGroup.class) private Integer appId; @NotNull(message = "version is null") @Pattern(regexp = "\\d+(\\.\\d+)*", message = "version is error, please check!") private String version; @Max(value = 100, message = "num is to big!") @Min(value = 1, message = "num is to small!") @NotNull(message = "percent can't be null!") private Integer percent; @Valid private InnerPublish publish; @Size(min = 1, max = 4, message = "list.size must be between 1 and 4") @NotNull(message = "list can't be null!") private List<String> list; }
自定义校验方式
除了以上的注解,也可以自己实现注解
-
自定义注解
@Documented @Constraint(validatedBy = { PasswordEqualsValidator.class }) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Password { String message() default "Password is not the same"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
-
校验器
public class PasswordEqualsValidator implements ConstraintValidator<Password, RegisterForm> { @Override public void initialize(Password anno) { } @Override public boolean isValid(RegisterForm form, ConstraintValidatorContext context) { String confirm = form.getConfirm(); String password = form.getPassword(); boolean match = password != null && password.equals(confirm); if (match) { return true; } String messageTemplate = context.getDefaultConstraintMessageTemplate(); // disable default violation rule context.disableDefaultConstraintViolation(); // assign error on password Confirm field context.buildConstraintViolationWithTemplate(messageTemplate).addPropertyNode("confirm") .addConstraintViolation(); return false; } }
-
实体类
@Data @Password(message = "password is not same!") public class RegisterForm { @NotNull(message = "name can't be null") private String name; @NotNull(message = "password can't be null") private String password; @NotNull(message = "confirm can't be null") private String confirm; }
2. 注解加异常处理器使用
注解
@Valid, @Validated
-
controller层直接使用,可以使用@Validated和@Valid两个注解,后者无法使用分组功能,抛出BindException(表单数据), MethodArgumentNotValidException(json数据)
@PostMapping("/controller") public BaseResult controller(@Validated(TestGroup.class) @RequestBody AppPublish appPublish{ return new DataResult("success"); } //or @PostMapping("/controller") public BaseResult controller(@Valid @RequestBody AppPublish appPublish{ return new DataResult("success"); }
-
任意地方使用,抛出ConstraintViolationException
@Service @Validated public class SignatureServiceImpl implements ISignatureService { @Override //如果默认分组,可以省略以下注解 @Validated(TestGroup.class) public void testCheck(@Valid AppPublish appPublish, @NotNull(message = "desc can't be null") String desc){ } }
异常处理
实现异常处理器即可对校验失败的结果进行报警 这种方法有一个问题,不同地方校验所产生的异常不同
-
一个简单的异常处理器
@Slf4j public class BindExceptionResolver extends AbstractHandlerExceptionResolver { private Charset charset = Charset.forName(Constant.UTF8); private static String JSON_CONTENT_TYPE = "text/plain;charset=UTF-8"; @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if(ex instanceof BindException){ //controller params } if(ex instanceof ConstraintViolationException){ //exception in others } if(ex instanceof MethodArgumentNotValidException){ //controller json } return null; } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
-
注意getOrder()方法,这里将该异常处理器的优先级设置为最高,以免被其他默认的异常处理器(如spring自带的异常处理器)处理报出其他不想要的结果。
util主动调用
不依赖spring 需要主动调用
-
实现方式
ValidateUtils.validate(appPublish, Default.class); //utils public class ValidateUtils { //默认校验器 private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); public static <T> void validate(T obj, Class group){ //校验过程 Set<ConstraintViolation<T>> validates = validator.validate(obj, group); if(CollectionUtils.isEmpty(validates)){ return; } List<String> messages = validates.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList()); String message = splice(messages); throw new MessageException(message, 400); } private static String splice(List<String> str){ StringBuilder sb = new StringBuilder(); str.forEach(s -> sb.append(s).append(", ")); String result = sb.toString(); return result.substring(0, result.length() - 3); } }
- 当然一般也是要实现异常处理的,只不过自定义可以控制抛出什么异常,比如mobile的MessageException
-
无法实现该情况下对单一参数的校验,如String desc
@Service @Validated public class SignatureServiceImpl implements ISignatureService { @Override //如果默认分组,可以省略以下注解 @Validated(TestGroup.class) public void testCheck(@Valid AppPublish appPublish, @NotNull(message = "desc can't be null") String desc){ } }
总结
使用框架校验参数的优势
- 代码简洁
- 容易实现错误信息的整合
-
原来的校验方式,如果contentType,jumpType两个都出错,只能返回第一个错误(当然也可以整合,只是比较麻烦,而且代码更乱了)
P.throwMessageExceptionIf(null == contentType, 200, "内容类型不能为空"); P.throwMessageExceptionIf(null == jumpType, 200, "跳转类型不能空");
-
采用校验框架是可以在一个地方实现信息的整合,因为一个异常包含了这次校验所有错误的信息
Set<ConstraintViolation<T>> validates = validator.validate(obj, group); if(CollectionUtils.isEmpty(validates)){ return; } List<String> messages = validates.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList()); String message = splice(messages);
-
-
框架校验方式对比
方式 @Validated Util 代码整洁 注解更为整洁 需要主动调用 单参数校验 可以实现 无法实现 实现 需实现spring指定的多歌异常处理 对结果进行统一的异常处理