每篇一句

在《深度工作》中作者提出这么一个公式:高质量产出=时间*专注度。所以高质量的产出不是靠时间熬出来的,而是效率为王

相关阅读

【小家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...


对Spring感兴趣可扫码加入wx群:`Java高工、架构师3群`(文末有二维码)


前言

你在书写业务逻辑的时候,是否会经常书写大量的判空校验。比如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()方法里获得的~)


MethodValidationPostProcessor

它是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,不管从使用上还是原理上,都是非常简单和简约的,建议大家在企业应用中多多使用。

1、约束注解(如@NotNull)不能放在实体类上

一般情况下,我们对于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注解它写在实现类/接口上均可~

最后你应该自己领悟到:若入参校验失败了,方法体是不会执行的。但倘若是返回值校验执行了(即使是失败了),方法体也肯定被执行了~~

2、@NotEmpty/@NotBlank只能哪些类型上?

提出这个细节的目的是:约束注解并不是能用在所有类型上的。比如若你把@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只能标注在如下类型

  1. CharSequence
  2. Collection
  3. Map
  4. Array

注意:""它是空的,但是" "就不是了

@NotBlank只能使用在CharSequence上,它是Bean Validation 2.0新增的注解~

3、接口和实现类上都有注解,以谁为准?

这个问题有个隐含条件:只有校验方法返回值时才有这种可能性。

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
...

透过打印的信息,结论就自然不必我多。但是有个道理此处可说明:大胆猜测,小心求证

4、如何校验级联属性?

在实际开发中,其实大多数情况下我们方法入参是个对象(甚至对象里面有对象),而不是单单平铺的参数,因此就介绍一个级联属性校验的例子:

@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的相关属性以及子属性为何全都没有校验呢?

5、循环依赖问题

上面说了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入群" 字样,会手动邀请入群

Spring方法级别数据校验:@Validated + MethodValidationPostProcessor的更多相关文章

  1. Spring官网阅读(十七)Spring中的数据校验

    文章目录 Java中的数据校验 Bean Validation(JSR 380) 使用示例 Spring对Bean Validation的支持 Spring中的Validator 接口定义 UML类图 ...

  2. Spring方法级别的验证

    设置验证点及验证方式(1)Spring方法级别的验证有多种验证方式,比较常用的有 @NotBlank:主要是对字符串的验证,不为null且去除空白符之后长度大于0 @NotNull:主要是对对象的验证 ...

  3. spring的后台数据校验

    数据校验对于开发项目来说是必须的.校验一般分为前台校验和后台校验,前台校验是必须要做的,后台校验是可选的.后台校验相对前台校验来说配置起来一般更复杂.前台校验通过js做,前台校验一般非常容易绕过.sp ...

  4. 【Spring学习笔记-MVC-10】Spring MVC之数据校验

    作者:ssslinppp       1.准备 这里我们采用Hibernate-validator来进行验证,Hibernate-validator实现了JSR-303验证框架支持注解风格的验证.首先 ...

  5. spring mvc 4数据校验 validator

    注解式控制器的数据验证.类型转换及格式化——跟着开涛学SpringMVC http://jinnianshilongnian.iteye.com/blog/1733708Spring4新特性——集成B ...

  6. 让Controller支持对平铺参数执行@Valid数据校验

    每篇一句 在金字塔塔尖的是实践,学而不思则罔,思而不学则殆(现在很多编程框架都只是教你碎片化的实践) 相关阅读 [小家Java]深入了解数据校验:Java Bean Validation 2.0(JS ...

  7. 从深处去掌握数据校验@Valid的作用(级联校验)

    每篇一句 NBA里有两大笑话:一是科比没天赋,二是詹姆斯没技术 相关阅读 [小家Java]深入了解数据校验:Java Bean Validation 2.0(JSR303.JSR349.JSR380) ...

  8. 详述Spring对数据校验支持的核心API:SmartValidator

    每篇一句 要致富,先修路.要使用,先...基础是需要垒砌的,做技术切勿空中楼阁 相关阅读 [小家Java]深入了解数据校验:Java Bean Validation 2.0(JSR303.JSR349 ...

  9. Spring MVC 数据校验@Valid

    先看看几个关键词 @Valid @Pattern @NotNull @NotBlank @Size BindingResult 这些就是Spring MVC的数据校验的几个注解. 那怎么用呢?往下看 ...

随机推荐

  1. 一个基于jQuery写的弹窗效果(附源码)

    最近项目中频繁遇到需要弹出窗口的功能,一直使用浏览器默认的Alert和Confirm弹窗,感觉视觉效果不是那么好,而从网上下载的话又找不到合适的,找到的话有些也是十分臃肿,有时候感觉学习配置的功夫自己 ...

  2. 9个WebGL的演示

    1.  WebGL Water This incredible demo is as fluid as you could believe.  Raise and drop the ball into ...

  3. asp.net core 系列之Startup

    这篇文章简单记录 ASP.NET Core中 ,startup类的一些使用. 一.前言 在 Startup类中,一般有两个方法: ConfigureServices 方法: 用来配置应用的 servi ...

  4. 关于linux网卡消失的问题

    首先我也是一名学生,在学习的过程Linux的过程中,因为老师要求配置hadoop集群所以就匆匆忙忙的上手了,但是在配置网络的问题时遇到了网卡消失的问题 我在网上询问了许多的人,但是还是没有找到一个很好 ...

  5. 初步接触 Java Net 网络编程

    本文目的是大概了解 Java 网络编程体系,需要一点点 Java IO 基础,推荐教程 系统学习 Java IO.主要参考 JavaDoc 和 Jakob Jenkov 的英文教程<Java N ...

  6. meta 详解

    对meta标签用处的介绍,meta常用于定义页面的说明,关键字,最后修改日期,和其它的元数据.这些元数据将服务于浏览器(如何布局或重载页面),搜索引擎和其它网络服务. 组成 meta标签共有两个属性, ...

  7. Python的空行

    函数之间或类的方法之间用空行分隔,表示一段新的代码的开始.类和函数入口之间也用一行空行分隔,以突出函数入口的开始. 空行与代码缩进不同,空行并不是Python语法的一部分.书写时不插入空行,Pytho ...

  8. Hive入门(四)查询优化

    1 本地模式 0.7版本后Hive开始支持任务执行选择本地模式(local mode). 大多数的Hadoop job是需要hadoop提供的完整的可扩展性来处理大数据的.不过,有时hive的输入数据 ...

  9. Java中实现线程的方式

    Java中实现线程的方式 Java中实现多线程的方式的方式中最核心的就是 run()方法,不管何种方式其最终都是通过run()来运行. Java刚发布时也就是JDK 1.0版本提供了两种实现方式,一个 ...

  10. redis 基础数据结构实现

    参考文献 redis数据结构分析 Skip List(跳跃表)原理详解 redis 源码分析之内存布局 Redis 基础数据结构与对象 Redis设计与实现-第7章-压缩列表 在redis中构建了自己 ...