导包和配置
导入 JSR 303 的包、hibernate valid 的包
org.hibernate.validator hibernate-validator 6.0.5.Final javax.validation validation-api 2.0.0.Final
springboot 配置
resources/application.yml
消息资源文件国际化处理配置
spring: messages: basename: base,todo # 资源文件 base.properties 和 todo.properties,多个用逗号隔开 encoding: UTF-8 # 必须指定解析编码,否则中文乱码
在 springboot 启动类里面配置
@SpringBootApplicationpublic class Application extends WebMvcConfigurerAdapter { @Value("${spring.messages.basename}") private String basename; public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean @Primary public MessageSource messageSource() { ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource(); resourceBundleMessageSource.setUseCodeAsDefaultMessage(false); resourceBundleMessageSource.setDefaultEncoding("UTF-8"); // 重复定义 resourceBundleMessageSource.setBasenames(basename.split(",")); return resourceBundleMessageSource; } @Bean @Primary public LocalValidatorFactoryBean validator() { LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean(); validatorFactoryBean.setProviderClass(HibernateValidator.class); validatorFactoryBean.setValidationMessageSource(messageSource()); return validatorFactoryBean; } @Override public Validator getValidator() { return validator(); } /** * 方法级别的单个参数验证开启 */ @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }}
我们对于校验参数通过不了抛出的异常进行处理,是通过统一异常捕捉。
@ControllerAdvice@Componentpublic class BindValidExceptionHandler { @ResponseStatus(value = HttpStatus.OK) @ExceptionHandler(ConstraintViolationException.class) public @ResponseBody Msg handleConstraintViolationException(ConstraintViolationException e) { String messageTemplate = e.getConstraintViolations().iterator().next().getMessageTemplate(); return Msg.error(messageTemplate); } @ResponseStatus(value = HttpStatus.OK) @ExceptionHandler(BindException.class) public @ResponseBody Msg handleBindException(BindException e) { BindingResult bindingResult = e.getBindingResult(); String className = bindingResult.getTarget().getClass().getName(); FieldError next = bindingResult.getFieldErrors().iterator().next(); String fieldName = next.getField(); String defaultMessage = next.getDefaultMessage(); if (Pattern.compile("IllegalArgumentException: No enum").matcher(defaultMessage).find()) { Matcher matcher = Pattern.compile("for value '(.*?)'").matcher(defaultMessage); if (matcher.find()) { defaultMessage = "找不到枚举类型【" + matcher.group(1) + "】"; } } return Msg.error(defaultMessage); } @ResponseStatus(value = HttpStatus.OK) @ExceptionHandler(ValidError.class) public @ResponseBody Msg handleValidError(ValidError e) { return Msg.error(e.getMessage()); }}
Msg 结果返回集
public class Msg { private boolean success = true; //是否成功 private Object data; //数据 private String message; //信息 private long code; //错误代码 public Object getData() { return this.data; } public String getMessage() { return this.message; } public long getCode() { return this.code; } public Msg() { } public Msg(int status) { this.code = status; } public Msg(String msg, Object data) { this.message = msg; this.data = data; } public Msg(boolean success, String msg, Object data) { this.success = success; this.message = msg; this.data = data; } public Msg(int status, String msg, Object data) { this.code = status; this.message = msg; this.data = data; } public Msg(boolean success, int status, String msg, Object data) { this.success = success; this.code = status; this.message = msg; this.data = data; } public boolean isSuccess() { return this.success; } public static Msg.BodyBuilder status(boolean success, int code) { return new Msg.DefaultBuilder(success, code); } public static Msg.BodyBuilder status(boolean success) { return new Msg.DefaultBuilder(success); } /* 快捷输出 start */ public static Msg.BodyBuilder ok() { return status(true); } public static Msg.BodyBuilder ok(int code) { return status(true, code); } public static Msg ok(Object data) { Msg.BodyBuilder builder = ok(); return builder.body(data); } public static Msg ok(String msg) { Msg.BodyBuilder builder = ok(); return builder.msg(msg).build(); } public static Msg ok(String msg, Object data) { Msg.BodyBuilder builder = ok(); return builder.msg(msg).body(data); } public static Msg ok(int code, String msg, Object data) { Msg.BodyBuilder builder = ok(code); return builder.msg(msg).body(data); } public static Msg.BodyBuilder fail() { return status(false); } public static Msg.BodyBuilder fail(int code) { return status(false, code); } public static Msg fail(Object data) { Msg.BodyBuilder builder = fail(); return builder.body(data); } public static Msg fail(String msg) { Msg.BodyBuilder builder = fail(); return builder.msg(msg).build(); } public static Msg fail(String msg, Object data) { Msg.BodyBuilder builder = fail(); return builder.msg(msg).body(data); } public static Msg fail(int code, String msg, Object data) { Msg.BodyBuilder builder = fail(code); return builder.msg(msg).body(data); } public static Msg error(Object data) { Msg.BodyBuilder builder = fail(); return builder.body(data); } public static Msg error(String msg) { Msg.BodyBuilder builder = fail(); return builder.msg(msg).build(); } public static Msg error(String msg, Object data) { Msg.BodyBuilder builder = fail(); return builder.msg(msg).body(data); } public static Msg error(int code, String msg, Object data) { Msg.BodyBuilder builder = fail(code); return builder.msg(msg).body(data); } /* 快捷输出 end */ private static class DefaultBuilder implements Msg.BodyBuilder { private boolean success; private int code; private String message; public DefaultBuilder(boolean success) { this.success = success; } public DefaultBuilder(boolean success, int code) { this.success = success; this.code = code; } public DefaultBuilder(boolean success, String message) { this.success = success; this.message = message; } @Override public Msg body(Object data) { Msg msg = new Msg(); msg.success = this.success; msg.message = this.message; msg.code = this.code; if (data instanceof Number) { return new Msg(this.success, this.message, data); } msg.data = data; if (msg.data == null) { msg.data = new Object(); } return msg; } @Override public Msg.BodyBuilder msg(String message) { this.message = message; return this; } @Override public Msg build() { return new Msg(this.success, this.code, this.message, ""); } } public interface BodyBuilder { Msg body(Object var1); Msg.BodyBuilder msg(String message); Msg build(); }
resources/base.propertie
creatorId=创建者 id 不能为小于 {value}。modifierId=修改者 id 不能为小于 {value}。
resources/todo.properties
todo.privateId.min=私有 id 不能为小于 {value}。
在 bean 字段上使用注解,其中 group 中的 C 和 S 接口是指 Controller 和 Service 的叫法简称,里面分别有 Insert 接口、Update 接口等等,都是自定义约定的东西。
public interface C { interface Insert {} interface Query {} interface Update {} interface UpdateStatus {}}
public interface S { interface Insert {} interface Query {} interface Update {} interface UpdateStatus {}}
/** * 私有 id,是代表项目任务/非项目任务/风险/问题/评审待办问题等多张表的外键 */@Min(value = 1, message = "{todo.privateId.min}", groups = {C.Insert.class, C.Update.class, S.Insert.class, S.Update.class})private long privateId;/** * 创建者id */@Min(value = 1, message = "{creatorId}", groups = {S.Insert.class})private long creatorId;
Controller 控制层验证
@Validated@RestController@RequestMapping("todo")public class TodoController { @Autowired private TodoService todoService; @GetMapping("getVo") public Msg getVo( @Min(value = 1, message = "待办 id 不能小于 1。") @RequestParam(required = false, defaultValue = "0") long id ) { return this.todoService.getVo(id); } @PostMapping("add") public Msg add(@Validated({C.Insert.class}) Todo todo) { return this.todoService.add(todo); }}
@Validated({C.Insert.class})
声明启用 bean 注解上的验证组,其他验证组不会进行验证,这样可以区别开来进行单独验证。
而像没有实体,只有一个基础数据类型的,可以进行验证,但是需要满足三个条件:
- 在启动类配置方法级别验证启用类
- 在 Controller 类上注解
@Validated
- 在方法参数里使用验证注解如
@Min
,@NotNull
等等
自行验证。
Service 服务层 AOP 验证
ValidUtil 工具类
需要被 springboot 扫描并注册为单例
@Componentpublic class ValidUtil { @Autowired private Validator validator; publicSet > validate(T object, Class ... groups) { return validator.validate(object, groups); } public Set > validateValue(Class beanType, String propertyName, Object value, Class ... groups) { return validator.validateValue(beanType, propertyName, value, groups); } /** * 校验参数,并返回第一个错误提示 * @param t 验证的对象 * @param groups 验证的组别 * @param 对象擦除前原类型 * @return 第一个错误提示 */ public void validAndReturnFirstErrorTips(T t, Class ... groups) { Set > validate = validator.validate(t, groups); if (validate.size() > 0) { ConstraintViolation next = validate.iterator().next(); String message = next.getRootBeanClass().getName() + "-" + next.getPropertyPath() + "-" + next.getMessage(); throw new ValidError(message); } } /** * 校验参数,并返回第一个错误提示 * @param targetClass 验证的对象的 class 类型 * @param fieldName 需要验证的名字 * @param obj 需要属性值 * @param groups 验证的组别 * @param 对象擦除前原类型 * @return 第一个错误提示 */ public void validAndReturnFirstErrorTips(Class targetClass, String fieldName, Object obj, Class ... groups) { Set > validate = validator.validateValue(targetClass, fieldName, obj, groups); if (validate.size() > 0) { String message = targetClass.getName() + "-" + fieldName + "-" + validate.iterator().next().getMessage(); throw new ValidError(message); } }}
AOP 配置
主要原理是利用 aop 拦截方法执行参数,对参数获取注解。再利用工具类来验证参数,如果验证不通过,直接抛出自定义错误,自定义错误已经全局统一处理了。
@Aspect@Componentpublic class ValidatorAOP { @Autowired private ValidUtil validUtil; /** * 定义拦截规则:拦截 com.servic 包下面的所有类中,有 @Service 注解的方法。 */ @Pointcut("execution(* com.service..*(..)) and @annotation(org.springframework.stereotype.Service)") public void controllerMethodPointcut() { } /** * 拦截器具体实现 */ @Around("controllerMethodPointcut()") // 指定拦截器规则;也可以直接把 “execution(* com.xjj.........)” 写进这里 public Object Interceptor(ProceedingJoinPoint pjp) { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); Annotation[][] argAnnotations = method.getParameterAnnotations(); Object[] args = pjp.getArgs(); for (int i = 0; i < args.length; i++) { for (Annotation annotation : argAnnotations[i]) { if (Validated.class.isInstance(annotation)) { Validated validated = (Validated) annotation; Class [] groups = validated.value(); validUtil.validAndReturnFirstErrorTips(args[i], groups); } } } try { return pjp.proceed(args); } catch (Throwable throwable) { throwable.printStackTrace(); } return true; }}
验证注解 @Min @NotNull 使用方法
不能写在实现类上,只能在接口中使用注解
与 Controller 使用方式基本一样
@Validatedpublic interface TodoService { /** * 查询 单个待办 * @param id 序号 * @return 单个待办 */ Msg getVo(@Min(value = 1, message = "待办 id 不能小于 1。") long id); /** * 添加数据 * @param todo 对象 */ Msg add(@Validated({S.Insert.class}) Todo todo);}
分享几个自定义验证注解
字符串判空验证
package javax.validation.constraints;import javax.validation.Constraint;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import javax.validation.Payload;import java.lang.annotation.*;/** * 字符串判空验证,hibernate 自带的可能有问题,使用不了,需要重写,package 是不能变的。 */@Documented@Constraint( validatedBy = {NotBlank.NotBlankValidator.class})@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public @interface NotBlank { Class [] groups() default {}; String message() default "{notBlank}"; Class [] payload() default {}; class NotBlankValidator implements ConstraintValidator{ public NotBlankValidator() { } @Override public void initialize(NotBlank constraintAnnotation) { } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { return value != null && !value.toString().isEmpty(); } }}
类型判断,判断 type 是否为其中一个值,可以根据验证组自定义判断
resources/todo.properties
todo.todoType.insert=新增时,待办类型只能是 非项目任务、项目任务、问题 之中一。todo.todoType.update=修改时,待办类型只能是风险、评审待办问题 之中一。
bean
/** * 待办类型0非项目任务1项目任务2问题3风险4评审待办问题 */@TodoTypeValid(value = {"0", "1", "2"}, message = "{todo.todoType.insert}", groups = {C.Insert.class, S.Insert.class})@TodoTypeValid(value = {"3", "4"}, message = "{todo.todoType.update}", groups = {C.Update.class, S.Update.class})private String todoType;
自定义注解
@Documented@Constraint(validatedBy = {TodoTypeValid.TodoTypeValidFactory.class})@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Repeatable(TodoTypeValid.List.class)public @interface TodoTypeValid { String message() default "请输入正确的类型"; String[] value() default {}; Class [] groups() default {}; Class [] payload() default {}; class TodoTypeValidFactory implements ConstraintValidator{ private String[] annotationValue; @Override public void initialize(TodoTypeValid todoStatusValid) { this.annotationValue = todoStatusValid.value(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (Arrays.asList(annotationValue).contains(value)) return true; return false; } } @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { TodoTypeValid[] value(); }}
@Repeatable(TodoTypeValid.List.class)
是 JDK8 支持的同一注解多次特性。
根据上面的同样也可以用在枚举类上
resources/todo.properties
todo.todoStatus.insert=新增时,状态只能是未开始。todo.todoStatus.update=修改时,状态只能是进行中或已完成。
bean
/** * 待办状态0未开始1进行中2已完成 */@TodoStatusValid(enums = {TodoStatus.NOT_STARTED}, message = "{todo.todoStatus.insert}", groups = {C.Insert.class, S.Insert.class})@TodoStatusValid(enums = {TodoStatus.PROCESSING, TodoStatus.COMPLETED}, message = "{todo.todoStatus.update}", groups = {C.Update.class, S.Update.class})private TodoStatus todoStatus;
自定义注解
@Documented@Constraint(validatedBy = {TodoStatusValid.TodoStatusValidFactory.class})@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Repeatable(TodoStatusValid.List.class)public @interface TodoStatusValid { String message() default "请输入正确的状态"; TodoStatus[] enums() default {}; Class [] groups() default {}; Class [] payload() default {}; class TodoStatusValidFactory implements ConstraintValidator{ private TodoStatus[] enums; @Override public void initialize(TodoStatusValid todoStatusValid) { this.enums = todoStatusValid.enums(); } @Override public boolean isValid(TodoStatus value, ConstraintValidatorContext context) { TodoStatus[] values = TodoStatus.values(); if (enums != null && enums.length != 0) { values = enums; } if (Arrays.asList(values).contains(value)) return true; return false; } } @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { TodoStatusValid[] value(); }}