在《深度工作》中作者提出这么一个公式:高质量产出=时间*专注度。所以高质量的产出不是靠时间熬出来的,而是效率为王
【小家Java】深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Java】深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)
【小家Spring】详述Spring对Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean... <center>对Spring感兴趣可扫码加入wx群: Java高工、架构师3群
(文末有二维码)</center>
你在书写业务逻辑的时候,是否会经常书写大量的判空校验。比如 Service
层或者 Dao
层的 方法入参、入参对象、出参中 你是否都有自己的一套校验规则?比如有些字段必传,有的非必传;返回值中有些字段必须有值,有的非必须等等~
如上描述的校验逻辑,窥探一下你的代码,估摸里面有大量的 if else
吧。此部分逻辑简单(因为和业务关系不大)却看起来眼花缭乱(赶紧偷偷去喵一下你自己的代码吧,哈哈)。在攻城主键变大的时候,你会发现会有大量的重复代码出现,这部分就是你入职一个新公司的吐槽点之一: 垃圾代码 。
若你追求干净的代码,甚至有 代码洁癖
,如上众多 if else
的重复无意义劳动无疑是你的痛点,那么本文应该能够帮到你。
Bean Validation
校验其实是基于 DDD
思想设计的,我们虽然可以不完全的遵从这种思考方式编程,但是其优雅的优点还是可取的,本文将介绍 Spring
为此提供的解决方案~
在讲解之前,首先就来体验一把吧~
@Validated(Default.class) public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); } // 实现类如下 @Slf4j @Service public class HelloServiceImpl implements HelloService { @Override public Object hello(Integer id, String name) { return null; } }
向容器里注册一个处理器:
@Configuration public class RootConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }
测试:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private HelloService helloService; @Test public void test1() { System.out.println(helloService.getClass()); helloService.hello(1, null); } }
结果如图:
完美的校验住了方法入参。
注意此处的一个小细节:若你自己运行这个案例你得到的参数名称可能是 hello.args0
等,而我此处是形参名。是因为我使用Java8的编译参数: -parameters
(此处说一点: 若你的逻辑中强依赖于此参数,务必在你的maven中加入编译插件并且配置好此编译参数 )
若需要 校验方法返回值 ,改写如下:
@NotNull Object hello(Integer id); // 此种写法效果同上 //@NotNull Object hello(Integer id);
运行:
javax.validation.ConstraintViolationException: hello.<return value>: 不能为null ...
校验完成。就这样借助 Spring
+ JSR
相关约束注解,就非常简单明了,语义清晰的 优雅的
完成了方法级别(入参校验、返回值校验)的校验。
校验不通过的错误信息,再来个全局统一的异常处理,就能让整个工程都能尽显完美之势。(错误消息可以从异常 ConstraintViolationException
的 getConstraintViolations()
方法里获得的~)
它是 Spring
提供的来实现基于方法 Method
的 JSR
校验的核心处理器~它能让约束作用在方法 入参、返回值
上,如:
public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
官方说明:方法里写有JSR校验注解要想其生效的话, 要求类型级别上必须使用 @Validated
标注(还能指定验证的Group)
另外提示一点:这个处理器同处理 @Async
的处理器 AsyncAnnotationBeanPostProcessor
非常相似,都是继承自 AbstractBeanFactoryAwareAdvisingPostProcessor
的,所以若有兴趣再次也推荐@Async的分析博文,可以对比着观看和记忆: 【小家Spring】Spring异步处理@Async的使用以及原理、源码分析(@EnableAsync)
// @since 3.1 public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { // 备注:此处你标注@Valid是无用的~~~Spring可不提供识别 // 当然你也可以自定义注解(下面提供了set方法~~~) // 但是注意:若自定义注解的话,此注解只决定了是否要代理,并不能指定分组哦 so,没啥事别给自己找麻烦吧 private Class<? extends Annotation> validatedAnnotationType = Validated.class; // 这个是javax.validation.Validator @Nullable private Validator validator; // 可以自定义生效的注解 public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) { Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null"); this.validatedAnnotationType = validatedAnnotationType; } // 这个方法注意了:你可以自己传入一个Validator,并且可以是定制化的LocalValidatorFactoryBean哦~(推荐) public void setValidator(Validator validator) { // 建议传入LocalValidatorFactoryBean功能强大,从它里面生成一个验证器出来靠谱 if (validator instanceof LocalValidatorFactoryBean) { this.validator = ((LocalValidatorFactoryBean) validator).getValidator(); } else if (validator instanceof SpringValidatorAdapter) { this.validator = validator.unwrap(Validator.class); } else { this.validator = validator; } } // 当然,你也可以简单粗暴的直接提供一个ValidatorFactory即可~ public void setValidatorFactory(ValidatorFactory validatorFactory) { this.validator = validatorFactory.getValidator(); } // 毫无疑问,Pointcut使用AnnotationMatchingPointcut,并且支持内部类哦~ // 说明@Aysnc使用的也是AnnotationMatchingPointcut,只不过因为它支持标注在类上和方法上,所以最终是组合的ComposablePointcut // 至于Advice通知,此处一样的是个`MethodValidationInterceptor`~~~~ @Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } // 这个advice就是给@Validation的类进行增强的~ 说明:子类可以覆盖哦~ // @since 4.2 protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
它是个普通的 BeanPostProcessor
,为Bean创建的代理的时机是 postProcessAfterInitialization()
,也就是在Bean完成 初始化后 有必要的话用一个代理对象返回进而交给Spring容器管理~(同 @Aysnc
)
容易想到,关于校验方面的逻辑不在于它,而在于切面的通知: MethodValidationInterceptor
MethodValidationInterceptor
它是AOP联盟类型的通知,此处专门用于处理方法级别的 数据校验 。
注意理解方法级别:方法级别的入参有可能是各种平铺的参数、也可能是一个或者多个对象
// @since 3.1 因为它校验Method 所以它使用的是javax.validation.executable.ExecutableValidator public class MethodValidationInterceptor implements MethodInterceptor { // javax.validation.Validator private final Validator validator; // 如果没有指定校验器,那使用的就是默认的校验器 public MethodValidationInterceptor() { this(Validation.buildDefaultValidatorFactory()); } public MethodValidationInterceptor(ValidatorFactory validatorFactory) { this(validatorFactory.getValidator()); } public MethodValidationInterceptor(Validator validator) { this.validator = validator; } @Override @SuppressWarnings("unchecked") public Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton // 如果是FactoryBean.getObject() 方法 就不要去校验了~ if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class<?>[] groups = determineValidationGroups(invocation); // Standard Bean Validation 1.1 API ExecutableValidator是1.1提供的 ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; // 错误消息result 若存在最终都会ConstraintViolationException异常形式抛出 try { // 先校验方法入参 result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { // 此处回退了异步:找到bridged method方法再来一次 methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass())); result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } if (!result.isEmpty()) { // 有错误就抛异常抛出去 throw new ConstraintViolationException(result); } // 执行目标方法 拿到返回值后 再去校验这个返回值 Object returnValue = invocation.proceed(); result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } // 找到这个方法上面是否有标注@Validated注解 从里面拿到分组信息 // 备注:虽然代理只能标注在类上,但是分组可以标注在类上和方法上哦~~~~ protected Class<?>[] determineValidationGroups(MethodInvocation invocation) { Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); if (validatedAnn == null) { validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class); } return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]); } }
这个 Advice
的实现,简单到不能再简单了,稍微有点基础的应该都能很容易看懂吧(据我不完全估计这个应该是最简单的)。
文首虽然已经给了一个使用示例,但是那毕竟只是局部。在实际生产使用中,比如上面理论更重要的是一些使用细节(细节往往是区分你是不是高手的地方), 这里从我使用的经验中,总结如下几点供给大家参考(基本算是分享我躺过的坑) :
使用 @Validated
去校验方法 Method
,不管从使用上还是原理上,都是非常简单和简约的,建议大家在企业应用中多多使用。
一般情况下,我们对于 Service
层验证(Controller层一般都不给接口),大都是面向接口编程和使用,那么这种 @NotNull
放置的位置应该怎么放置呢?
看这个例子:
public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); } @Validated(Default.class) @Slf4j @Service public class HelloServiceImpl implements HelloService { @Override public Object hello(Integer id, String name) { return null; } }
约束条件都写在实现类上,按照我们 所谓的经验 ,应该是不成问题的。但运行:
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method HelloServiceImpl#hello(Integer) redefines the configuration of HelloService#hello(Integer). at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ...
重说三 :请务必注意请务必注意请务必注意这个异常是 javax.validation.ConstraintDeclarationException
,而不是错误校验错误异常 javax.validation.ConstraintViolationException
。请在做全局异常捕获的时候一定要区分开来~
异常信息是说 parameter constraint configuration
在校验 方法入参 的约束时,若是 @Override
父类/接口的方法, 那么这个入参约束只能写在父类/接口上面 ~~~
至于为什么只能写在接口处,这个具体原因其实是和 Bean Validation
的实现产品有关的,比如使用的Hibernate校验,原因可参考它的此类: OverridingMethodMustNotAlterParameterConstraints
还需注意一点:若实现类写的约束和接口 一模一样 ,那也是没问题的。比如上面若实现类这么写是没有问题能够完成正常校验的:
@Override public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) { return null; }
虽然能正常work完成校验,但需要深刻理解 一模一样
这四个字。简单的说把10改成9都会报 ConstraintDeclarationException
异常,更别谈移除某个注解了(不管多少字段多少注解,但凡只要写了一个就必须保证 一模一样
)。
关于 @Override
方法校验返回值方面: 即使写在实现类里也不会抛 ConstraintDeclarationException
另外 @Validated
注解它写在实现类/接口上均可~
最后你应该自己领悟到:若入参校验失败了,方法体是不会执行的。但倘若是返回值校验执行了(即使是失败了),方法体也肯定被执行了~~
提出这个细节的目的是: 约束注解并不是能用在所有类型上的 。比如若你把 @NotEmpty
让它去验证Object类型,它会报错如下:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Object'. Check configuration for 'hello.<return value>'
需要强调的是:若标注在方法上是验证返回值的,这个时候 方法体是已经执行了的 ,这个和 ConstraintDeclarationException
不一样~
对这两个注解依照官方文档做如下简要说明。 @NotEmpty
只能标注在如下类型
注意:""它是空的,但是" "就不是了
@NotBlank
只能使用在 CharSequence
上,它是Bean Validation 2.0新增的注解~ 这个问题有个隐含条件:只有校验方法返回值时才有这种可能性。
public interface HelloService { @NotEmpty String hello(@NotNull @Min(10) Integer id, @NotNull String name); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Override public @NotNull String hello(Integer id, String name) { return ""; } }
运行案例, helloService.hello(18, "fsx");
打印如下:
javax.validation.ConstraintViolationException: hello.<return value>: 不能为空 ...
到这里,可能有小伙伴就会早早下结论: 当同时存在时,以接口的约束为准 。
那么,我只把返回值稍稍修改,你再看一下呢???
@Override public @NotNull String hello(Integer id, String name) { return null; // 返回值改为null }
再运行:
javax.validation.ConstraintViolationException: hello.<return value>: 不能为空, hello.<return value>: 不能为null ...
透过打印的信息,结论就自然不必我多。 但是有个道理此处可说明:大胆猜测,小心求证
级联属性
? 在实际开发中,其实大多数情况下我们方法入参是个对象(甚至对象里面有对象),而不是单单平铺的参数,因此就介绍一个 级联属性校验的例子 :
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid // 让InnerChild的属性也参与校验 @NotNull private InnerChild child; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } } public interface HelloService { String cascade(@NotNull @Valid Person father, @NotNull Person mother); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Override public String cascade(Person father, Person mother) { return "hello cascade..."; } }
运行测试用例:
@Test public void test1() { helloService.cascade(null, null); }
输出如下:
cascade.father: 不能为null, cascade.mother: 不能为null
此处说明一点:若你 father
前面没加 @NotNull
,那打印的消息只有: cascade.mother: 不能为null
@Test public void test1() { Person father = new Person(); father.setName("fsx"); Person.InnerChild innerChild = new Person.InnerChild(); innerChild.setAge(-1); father.setChild(innerChild); helloService.cascade(father, new Person()); }
错误消息如下(请小伙伴仔细观察和分析缘由):
cascade.father.age: 不能为null, cascade.father.child.name: 不能为null, cascade.father.child.age: 必须是正数
思考:为何 mother
的相关属性以及子属性为何全都没有校验呢?
上面说了Spring对 @Validated
的处理和对 @Aysnc
的代理逻辑是差不多的,有了之前的经验,很容易想到它也存在着如题的问题:比如 HelloService
的A方法想调用本类的B方法,但是很显然我是希望 B方法的方法校验是能生效的 ,因此其中一个做法就是注入自己,使用自己的代理对象来调用:
public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); String cascade(@NotNull @Valid Person father, @NotNull Person mother); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Autowired private HelloService helloService; @Override public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) { helloService.cascade(null, null); // 调用本类方法 return null; } @Override public String cascade(Person father, Person mother) { return "hello cascade..."; } }
运行测试用例:
@Test public void test1() { helloService.hello(18, "fsx"); // 入口方法校验通过,内部调用cascade方法希望继续得到校验 }
运行报错:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'helloServiceImpl': Bean with name 'helloServiceImpl' has been injected into other beans [helloServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean ...
这个报错消息不可为不熟悉。关于此现象,之前做过非常非常详细的说明并且提供了多种解决方案,所以此处略过。
若关于此问的原因和解决方案不明白的,请移步此处: 【小家Spring】使用@Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析,以及提供解决方案
虽然我此处不说解决方案,但我提供问题解决后运行的打印输出情况,供给小伙伴调试参考,此举很暖心有木有:
javax.validation.ConstraintViolationException: cascade.mother: 不能为null, cascade.father: 不能为null ...
本文介绍了 Spring
提供给我们方法级别校验的能力,在企业应用中使用此种方式完成绝大部分的基本校验工作,能够让我们的代码更加简洁、可控并且可扩展,因此我是推荐使用和扩散的~
在文末有必要强调一点:关于上面级联属性的校验时使用的 @Valid
注解你使用 @Validated
可替代不了,不会有效果的。
至于有小伙伴私信我疑问的问题:为何他 Controller
方法中使用 @Valid
和 @Validated
均可,并且网上同意给的答案都是 都可用,差不多
???还是那句话:这是下篇文章的重点,请持续关注~
稍稍说一下它的弊端:因为校验失败它最终采用的是抛异常方式来中断,因此效率上有那么 一丢丢的损耗 。but,你的应用真的需要考虑这种极致性能问题吗?这才是你该思考的~
若文章格式混乱,可点击
: 原文链接-原文链接-原文链接-原文链接-原文链接
==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被 作者本人许可的~
==
**若对技术内容感兴趣可以加入wx群交流: Java高工、架构师3群
。
若群二维码失效,请加wx号: fsx641385712
(或者扫描下方wx二维码)。并且备注: "java入群"
字样,会手动邀请入群**