Hibernate-validator校验含javax.validation.constraints注解的对象其首次校验长耗时问题
前段时间对老项目做性能优化时,发现用hibernate-validator校验数据约束,首次检验某个实体类耗时较长,本文探讨其中的原因,并给出优化建议。
1. 校验测试
ValidateTest1DTO.java代码如下
package com.mingo.exp.validate;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 被校验类
*
* @author doflamingo
*/
@Data
@NoArgsConstructor
public class ValidateTest1DTO {
@NotBlank(message = "name不能为空")
private String name;
@NotNull(message = "score不能为空")
private Double score;
public ValidateTest1DTO(String name, Double score) {
this.name = name;
this.score = score;
}
}
HibernateValidateTest.java测试类如下
package com.mingo.exp.validate;
import org.springframework.util.StopWatch;
import javax.validation.Validation;
import javax.validation.Validator;
/**
* 运行方式:
* 1、执行main
*
* @author doflamingo
*/
public class HibernateValidateTest {
// 1 ms = 1000000 ns
public void test() {
// org.hibernate.validator.internal.engine.ValidatorFactoryImpl
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
ValidateTest1DTO t1 = new ValidateTest1DTO("A", 3.1415);
ValidateTest1DTO t2 = new ValidateTest1DTO("B", 3.1415);
ValidateTest1DTO t3 = new ValidateTest1DTO("C", null);
ValidateTest1DTO t4 = new ValidateTest1DTO("", null);
StopWatch stopWatch = new StopWatch("时间测试");
// t1
stopWatch.start("t1");
validator.validate(t1);
stopWatch.stop();
// t2
stopWatch.start("t2");
validator.validate(t2);
stopWatch.stop();
// t3
stopWatch.start("t3");
validator.validate(t3);
stopWatch.stop();
// t4
stopWatch.start("t4");
validator.validate(t4);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
public static void main(String[] args) {
new HibernateValidateTest().test();
}
}
运行多次 HibernateValidateTest.main() 结果都一致,取其中一个结果
StopWatch '时间测试': running time = 93152700 ns
---------------------------------------------
ns % Task name
---------------------------------------------
089612800 096% t1
000115200 000% t2
003283800 004% t3
000140900 000% t4
t1、t2、t3和t4对象类型和校验都是一样,但运行结果显示"t1"(首次校验)耗时较长,下面看下具体原因
2. 校验耗时原因分析
检验方法org.hibernate.validator.internal.engine.ValidatorImpl.validate(T object, Class<?>... groups)源码如下
分析了该方法所有代码,在红色框处代码调用有些不同,进入org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanMetaData(Class
红色方框处有一个逻辑是根据被校验类的Class对象从一个Map中获取BeanMetaData对象,如果Map中没有就创建BeanMetaData对象并存入Map中,下次同样的Class对象传进来直接从Map获取到即可。t1与t2~t4的Class对象一样,最大的不同是校验t1时调用了createBeanMetaData( normalizedBeanClass )方法,用于生成BeanMetaData对象,该方法是个耗时操作。
下面对上述耗时原因做了时间验证
3. validate(T object, Class<?>... groups)方法内部代码时间测试
源代码在启动时会生成org.hibernate.validator.internal.engine.ValidatorFactoryImpl对象,采用工厂方法getValidator()生成Validator校验对象,源码如下
@Override
public Validator getValidator() {
return createValidator(
constraintCreationContext.getConstraintValidatorManager().getDefaultConstraintValidatorFactory(),
constraintCreationContext,
validatorFactoryScopedContext,
methodValidationConfiguration
);
}
所以我只需代理该方法生成自己的Validator对象。
总体思路
1. 复制org.hibernate.validator.internal.engine.ValidatorImpl代码为类com.mingo.exp.validate.MyCopyValidatorImpl,只是构造器不一样;
2. 修改MyCopyValidatorImpl.validate(T object, Class<?>... groups)方法,加入时间测试代码;
3. 用JDK动态代理ValidatorFactoryImpl类,处理getValidator()方法,生成MyCopyValidatorImpl对象;
MyCopyValidatorImpl.validate(T object, Class<?>... groups)方法
@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
// 将源代码分成了五部分测试
StopWatch stopWatch = new StopWatch("hibernate.validator.validate(T object, Class<?>... groups)时间测试");
stopWatch.start("第一段");
Contracts.assertNotNull(object, MESSAGES.validatedObjectMustNotBeNull());
sanityCheckGroups(groups);
stopWatch.stop();
stopWatch.start("第二段");
@SuppressWarnings("unchecked")
Class<T> rootBeanClass = (Class<T>) object.getClass();
BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData(rootBeanClass);
stopWatch.stop();
stopWatch.start("第三段");
if (!rootBeanMetaData.hasConstraints()) {
return Collections.emptySet();
}
BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate(rootBeanClass, rootBeanMetaData, object);
stopWatch.stop();
stopWatch.start("第四段");
ValidationOrder validationOrder = determineGroupValidationOrder(groups);
stopWatch.stop();
stopWatch.start("第五段");
BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
stopWatch.stop();
// 打印测试结果
System.out.println(stopWatch.prettyPrint());
return validateInContext(validationContext, valueContext, validationOrder);
}
MyCopyValidatorImpl类其余方法与ValidatorImpl一样,这里不给出
com.mingo.exp.validate.ValidatorFactoryImplProxy代理类
package com.mingo.exp.validate;
import org.hibernate.validator.internal.engine.ConstraintCreationContext;
import org.hibernate.validator.internal.engine.MethodValidationConfiguration;
import org.hibernate.validator.internal.engine.ValidatorFactoryScopedContext;
import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator;
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager;
import org.hibernate.validator.internal.metadata.BeanMetaDataManager;
import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl;
import org.hibernate.validator.internal.metadata.provider.MetaDataProvider;
import org.hibernate.validator.internal.properties.javabean.JavaBeanHelper;
import org.hibernate.validator.internal.util.ExecutableHelper;
import org.hibernate.validator.internal.util.ExecutableParameterNameProvider;
import org.hibernate.validator.metadata.BeanMetaDataClassNormalizer;
import javax.validation.ValidatorFactory;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 代理基于javax.validation.ValidatorFactory接口
* 实际上代理org.hibernate.validator.internal.engine.ValidatorFactoryImpl类
*
* @author doflamingo
*/
public class ValidatorFactoryImplProxy implements InvocationHandler {
/**
* 被代理对象
*/
private ValidatorFactory target;
/**
* 被代理类的Class对象
*/
private Class<? extends ValidatorFactory> targetClass;
// 以下是要通过反射拿到的值
private ValidationOrderGenerator validationOrderGenerator;
private ConstraintCreationContext constraintCreationContext;
private ValidatorFactoryScopedContext validatorFactoryScopedContext;
private BeanMetaDataManager beanMetaDataManager;
/**
* 生成代理对象
*
* @param target
* @return
*/
public ValidatorFactory proxy(ValidatorFactory target) throws Throwable {
this.target = target;
this.targetClass = target.getClass();
// 将targetClass相关私有属性通过反射机制拿到
this.init();
// 生成代理对象
return (ValidatorFactory) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
/**
* 将targetClass相关私有属性通过反射机制拿到
*
* @throws Throwable
*/
private void init() throws Throwable {
Field executableHelperField = targetClass.getDeclaredField("executableHelper");
Field javaBeanHelperField = targetClass.getDeclaredField("javaBeanHelper");
Field beanMetadataClassNormalizerField = targetClass.getDeclaredField("beanMetadataClassNormalizer");
Field validationOrderGeneratorField = targetClass.getDeclaredField("validationOrderGenerator");
Field constraintCreationContextField = targetClass.getDeclaredField("constraintCreationContext");
Field validatorFactoryScopedContextField = targetClass.getDeclaredField("validatorFactoryScopedContext");
Field methodValidationConfigurationField = targetClass.getDeclaredField("methodValidationConfiguration");
// 访问打开
executableHelperField.setAccessible(true);
javaBeanHelperField.setAccessible(true);
beanMetadataClassNormalizerField.setAccessible(true);
validationOrderGeneratorField.setAccessible(true);
constraintCreationContextField.setAccessible(true);
validatorFactoryScopedContextField.setAccessible(true);
methodValidationConfigurationField.setAccessible(true);
ExecutableHelper executableHelper = (ExecutableHelper) executableHelperField.get(target);
JavaBeanHelper javaBeanHelper = (JavaBeanHelper) javaBeanHelperField.get(target);
BeanMetaDataClassNormalizer beanMetadataClassNormalizer = (BeanMetaDataClassNormalizer) beanMetadataClassNormalizerField.get(target);
ValidationOrderGenerator validationOrderGenerator = (ValidationOrderGenerator) validationOrderGeneratorField.get(target);
ConstraintCreationContext constraintCreationContext = (ConstraintCreationContext) constraintCreationContextField.get(target);
ValidatorFactoryScopedContext validatorFactoryScopedContext = (ValidatorFactoryScopedContext) validatorFactoryScopedContextField.get(target);
MethodValidationConfiguration methodValidationConfiguration = (MethodValidationConfiguration) methodValidationConfigurationField.get(target);
// copy Map属性
Field beanMetaDataManagersField = targetClass.getDeclaredField("beanMetaDataManagers");
beanMetaDataManagersField.setAccessible(true);
ConcurrentMap<BeanMetaDataManagerKey, BeanMetaDataManager> beanMetaDataManagers = this.beanMetaDataManager(beanMetaDataManagersField);
Method buildMetaDataProviders = targetClass.getDeclaredMethod("buildMetaDataProviders");
buildMetaDataProviders.setAccessible(true);
List<MetaDataProvider> metaDataProviders = (List<MetaDataProvider>) buildMetaDataProviders.invoke(target);
// 生成beanMetaDataManager
BeanMetaDataManager beanMetaDataManager = beanMetaDataManagers.computeIfAbsent(
new BeanMetaDataManagerKey(validatorFactoryScopedContext.getParameterNameProvider(), constraintCreationContext.getValueExtractorManager(), methodValidationConfiguration),
key -> new BeanMetaDataManagerImpl(
constraintCreationContext,
executableHelper,
validatorFactoryScopedContext.getParameterNameProvider(),
javaBeanHelper,
beanMetadataClassNormalizer,
validationOrderGenerator,
metaDataProviders,
methodValidationConfiguration
)
);
this.validationOrderGenerator = validationOrderGenerator;
this.constraintCreationContext = constraintCreationContext;
this.validatorFactoryScopedContext = validatorFactoryScopedContext;
this.beanMetaDataManager = beanMetaDataManager;
}
/**
* 只处理getValidator()方法
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只处理了getValidator方法
if ("getValidator".equals(method.getName())) {
// 复制ValidatorImpl类命名为MyCopyValidatorImpl
return new MyCopyValidatorImpl(
constraintCreationContext.getConstraintValidatorManager().getDefaultConstraintValidatorFactory(),
beanMetaDataManager,
this.constraintCreationContext.getValueExtractorManager(),
this.constraintCreationContext.getConstraintValidatorManager(),
validationOrderGenerator,
this.validatorFactoryScopedContext
);
}
return method.invoke(this.target, args);
}
/**
* copy私有内部类对象作为key的Map
*
* @param field
* @return
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private ConcurrentMap<BeanMetaDataManagerKey, BeanMetaDataManager> beanMetaDataManager(Field field)
throws IllegalArgumentException, IllegalAccessException {
field.setAccessible(true);
Map obj = (Map) field.get(target);
// 放copy后的结果
ConcurrentMap<BeanMetaDataManagerKey, BeanMetaDataManager> beanMetaDataManagers = new ConcurrentHashMap<>(obj.size());
// 复制
obj.forEach((k, v) -> {
try {
Class s = k.getClass();
Field parameterNameProviderF = s.getDeclaredField("parameterNameProvider");
Field valueExtractorManagerF = s.getDeclaredField("valueExtractorManager");
Field methodValidationConfigurationF = s.getDeclaredField("methodValidationConfiguration");
parameterNameProviderF.setAccessible(true);
valueExtractorManagerF.setAccessible(true);
methodValidationConfigurationF.setAccessible(true);
ExecutableParameterNameProvider parameterNameProvider = (ExecutableParameterNameProvider) parameterNameProviderF.get(s);
ValueExtractorManager valueExtractorManager = (ValueExtractorManager) valueExtractorManagerF.get(s);
MethodValidationConfiguration methodValidationConfiguration = (MethodValidationConfiguration) methodValidationConfigurationF.get(s);
BeanMetaDataManagerKey beanMetaDataManagerKey = new BeanMetaDataManagerKey(parameterNameProvider, valueExtractorManager, methodValidationConfiguration);
beanMetaDataManagers.put(beanMetaDataManagerKey, (BeanMetaDataManager) v);
} catch (Exception e) {
e.printStackTrace();
}
});
return beanMetaDataManagers;
}
/**
* ValidatorFactoryImpl中的内部类
*/
private static class BeanMetaDataManagerKey {
private final ExecutableParameterNameProvider parameterNameProvider;
private final ValueExtractorManager valueExtractorManager;
private final MethodValidationConfiguration methodValidationConfiguration;
private final int hashCode;
public BeanMetaDataManagerKey(ExecutableParameterNameProvider parameterNameProvider, ValueExtractorManager valueExtractorManager, MethodValidationConfiguration methodValidationConfiguration) {
this.parameterNameProvider = parameterNameProvider;
this.valueExtractorManager = valueExtractorManager;
this.methodValidationConfiguration = methodValidationConfiguration;
this.hashCode = buildHashCode(parameterNameProvider, valueExtractorManager, methodValidationConfiguration);
}
private static int buildHashCode(ExecutableParameterNameProvider parameterNameProvider, ValueExtractorManager valueExtractorManager, MethodValidationConfiguration methodValidationConfiguration) {
final int prime = 31;
int result = 1;
result = prime * result + ((methodValidationConfiguration == null) ? 0 : methodValidationConfiguration.hashCode());
result = prime * result + ((parameterNameProvider == null) ? 0 : parameterNameProvider.hashCode());
result = prime * result + ((valueExtractorManager == null) ? 0 : valueExtractorManager.hashCode());
return result;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BeanMetaDataManagerKey other = (BeanMetaDataManagerKey) obj;
return methodValidationConfiguration.equals(other.methodValidationConfiguration) &&
parameterNameProvider.equals(other.parameterNameProvider) &&
valueExtractorManager.equals(other.valueExtractorManager);
}
@Override
public String toString() {
return "BeanMetaDataManagerKey [parameterNameProvider=" + parameterNameProvider + ", valueExtractorManager=" + valueExtractorManager
+ ", methodValidationConfiguration=" + methodValidationConfiguration + "]";
}
}
}
测试类
package com.mingo.exp.validate;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* 运行方式:
* 1、执行main
*
* @author doflamingo
*/
public class HibernateValidateTest2 {
// 1 ms = 1000000 ns
public void test() throws Throwable {
// 代理org.hibernate.validator.internal.engine.ValidatorFactoryImpl.getValidator()方法
ValidatorFactory factory = new ValidatorFactoryImplProxy().proxy(Validation.buildDefaultValidatorFactory());
// 得到的是MyCopyValidatorImpl类的对象
Validator validator = factory.getValidator();
// 只用一个对象作为测试
ValidateTest1DTO t1 = new ValidateTest1DTO("A", 3.1415);
validator.validate(t1);
}
public static void main(String[] args) throws Throwable {
new HibernateValidateTest2().test();
}
}
HibernateValidateTest2.main()运行结果
StopWatch 'hibernate.validator.validate(T object, Class<?>... groups)时间测试': running time = 92530900 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000042500 000% 第一段
079955700 086% 第二段
004400700 005% 第三段
000042200 000% 第四段
008089800 009% 第五段
测试结果可以看出“第二段(也就是创建BeanMetaData对象)”比较耗时,验证了前面分析的原因。
可见同一个Validator对象校验相同的实体类对象时,首次校验较为耗时。下面我测试下同一个Validator对象检验不同实体类对象
测试下同一个Validator对象检验不同实体类对象耗时
测试代码
package com.mingo.exp.validate;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* 运行方式:
* 1、执行main
*
* @author doflamingo
*/
public class HibernateValidateTest2 {
// 1 ms = 1000000 ns
public void test() throws Throwable {
// 代理org.hibernate.validator.internal.engine.ValidatorFactoryImpl.getValidator()方法
ValidatorFactory factory = new ValidatorFactoryImplProxy().proxy(Validation.buildDefaultValidatorFactory());
// 得到的是MyCopyValidatorImpl类的对象
Validator validator = factory.getValidator();
// ValidateTest1DTO 两个对象
ValidateTest1DTO t1 = new ValidateTest1DTO("A", 3.1415);
ValidateTest1DTO t2 = new ValidateTest1DTO("B", 3.1415);
validator.validate(t1);
validator.validate(t2);
// ValidateTest2DTO 两个对象
ValidateTest2DTO t21 = new ValidateTest2DTO("A", 3.1415);
ValidateTest2DTO t22 = new ValidateTest2DTO("B", 3.1415);
validator.validate(t21);
validator.validate(t22);
}
public static void main(String[] args) throws Throwable {
new HibernateValidateTest2().test();
}
}
HibernateValidateTest2.main()运行结果
// t1对象
StopWatch 'hibernate.validator.validate(T object, Class<?>... groups)时间测试': running time = 94788800 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000031200 000% 第一段
084693200 089% 第二段
003510600 004% 第三段
000038400 000% 第四段
006515400 007% 第五段
// t2对象
StopWatch 'hibernate.validator.validate(T object, Class<?>... groups)时间测试': running time = 21900 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000001900 009% 第一段
000007500 034% 第二段
000004600 021% 第三段
000002900 013% 第四段
000005000 023% 第五段
// t21对象
StopWatch 'hibernate.validator.validate(T object, Class<?>... groups)时间测试': running time = 8290300 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000003200 000% 第一段
008255900 100% 第二段
000019600 000% 第三段
000003800 000% 第四段
000007800 000% 第五段
// t22对象
StopWatch 'hibernate.validator.validate(T object, Class<?>... groups)时间测试': running time = 43400 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000003000 007% 第一段
000004300 010% 第二段
000027600 064% 第三段
000003400 008% 第四段
000005100 012% 第五段
可见同一个Validator对象检验不同实体类对象首次都比较耗时,是按照Class对象来分类。
结语
- 如果要用hibernate.validator包校验数据约束,要注意只需生成一个Validator对象来作校验。
- 对于实体类的首次校验耗时问题,一种解决思路是提前将数据写入org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.beanMetaDataCache缓存中。具体怎样写入可采用反射或者修改字节码方式,后文将实践。还有一种就是将要校验的对象类在项目启动后预先加载。