在《深度工做》中做者提出这么一个公式:高质量产出=时间*专一度。因此高质量的产出不是靠时间熬出来的,而是效率为王java
【小家Java】深刻了解数据校验:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例【小家Java】深刻了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)【小家Spring】详述Spring对Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean...spring
对Spring感兴趣可扫码加入wx群:Java高工、架构师3群
(文末有二维码)
你在书写业务逻辑的时候,是否会常常书写大量的判空校验。好比Service
层或者Dao
层的方法入参、入参对象、出参中你是否都有本身的一套校验规则?好比有些字段必传,有的非必传;返回值中有些字段必须有值,有的非必须等等~编程
如上描述的校验逻辑,窥探一下你的代码,估摸里面有大量的if else
吧。此部分逻辑简单(由于和业务关系不大)却看起来眼花缭乱(赶忙偷偷去喵一下你本身的代码吧,哈哈)。在攻城主键变大的时候,你会发现会有大量的重复代码出现,这部分就是你入职一个新公司的吐槽点之一:垃圾代码。markdown
若你追求干净的代码,甚至有代码洁癖
,如上众多if else
的重复无心义劳动无疑是你的痛点,那么本文应该可以帮到你。Bean Validation
校验实际上是基于DDD
思想设计的,咱们虽然能够不彻底的听从这种思考方式编程,可是其优雅的优势仍是可取的,本文将介绍Spring
为此提供的解决方案~架构
在讲解以前,首先就来体验一把吧~app
@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();
}
}复制代码
测试:maven
@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);
}
}复制代码
结果如图:完美的校验住了方法入参。ide
注意此处的一个小细节:若你本身运行这个案例你获得的参数名称多是
hello.args0
等,而我此处是形参名。是由于我使用Java8的编译参数:-parameters
(此处说一点:若你的逻辑中强依赖于此参数,务必在你的maven中加入编译插件而且配置好此编译参数)oop
若须要校验方法返回值,改写以下:
@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入群"
字样,会手动邀请入群**