前言,传递的DTO带有hibernate-validator的注解校验会报错。
Dubbo可以集成jsr303标准规范的验证框架,作为验证框架不二人选的hibernate-validator是大家都会经常在项目中使用的,但是在Dubbo使用是会发生下面这个问题。
问题描述
背景:使用springmvc做restful,使用dubbo做rpc,restful中调用大量的rpc,数据验证会在这两个地方,一个是restful层面,一个是rpc层面,restful层面使用springmvc默认的集成hibernate-validator来实现,参数开启验证只需要加入@Validated param。
rpc层面也使用hibernate-validator实现,dubbo中开启validation也有两个方式,一个是在consumer端,一个是在provider端。
当我们在consumer端开启验证时:
<dubbo:reference id="serviceName" interface="com.domain.package.TestService" registry="registry" validation="true"/>
没有任何问题,可以拿到所有的数据校验失败数据。
当我们在provider端开启验证时:
<dubbo:service interface="com.domain.package.TestService" ref="serviceName" validation="true" />
会发生如下异常:
1com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method: sayHello, provider: 2 3dubbo://127.0.0.1:20831/com.domain.package.TestService?application=dubbo-test- 4 5rest&default.check=false&default.cluster=failfast&default.retries=0&default.timeout=1200000&default.version=1.0 6 7.0&dubbo=2.6.1&interface=com.domain.package.TestService&methods=sayHello&pid=29268®ister.ip=192. 8 9168.6.47&side=consumer×tamp=1524453157718, cause: com.alibaba.com.caucho.hessian.io.HessianFieldException: 10 11org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 12 13'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated 14com.alibaba.com.caucho.hessian.io.HessianFieldException: 15 16org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 17 18'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated 19 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.logDeserializeError(JavaDeserializer.java:167) 20 at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize 21 22(JavaDeserializer.java:408) 23 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273) 24 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200) 25 at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525) 26 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791) 27 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731) 28 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260) 29 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705) 30 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260) 31 at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList 32 33(CollectionDeserializer.java:119) 34 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2186) 35 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057) 36 at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize 37 38(JavaDeserializer.java:404) 39 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273) 40 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200) 41 at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525) 42 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791) 43 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731) 44 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260) 45 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705) 46 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260) 47 at com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject 48 49(Hessian2ObjectInput.java:74) 50 at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:90) 51 at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110) 52 at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:88) 53 at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:121) 54 at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:82) 55 at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:44) 56 at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived 57 58(NettyCodecAdapter.java:133) 59 at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream 60 61(SimpleChannelUpstreamHandler.java:70) 62 at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564) 63 at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559) 64 at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268) 65 at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255) 66 at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88) 67 at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109) 68 at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312) 69 at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90) 70 at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178) 71 at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108) 72 at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42) 73 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 74 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) 75 at java.lang.Thread.run(Thread.java:744) 76Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 77 78'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated 79 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:313) 80 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:198) 81 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2789) 82 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2128) 83 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057) 84 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2101) 85 at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057) 86 at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize 87 88(JavaDeserializer.java:404) 89 ... 43 more 90Caused by: java.lang.reflect.InvocationTargetException 91 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 92 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) 93 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) 94 at java.lang.reflect.Constructor.newInstance(Constructor.java:526) 95 at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:309) 96 ... 50 more 97Caused by: java.lang.NullPointerException 98 at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init> 99 100(ConstraintDescriptorImpl.java:158) 101 at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init> 102 103(ConstraintDescriptorImpl.java:211) 104 ... 55 more
问题分析
上面的问题从异常面来看已经很直观了,'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated,这个类无法实例化,那是什么原因导致它无法实例化呢?
Dubbo的序列化协议,默认是hessian,如果没有进行其他协议配置的话,默认使用的就是hessian,hessian在反序列化时有个特点需要注意一下,它会在反序列化时取参数最少的构造器来创建对象,有的时候会有很多重载的构造器,因此会有一些参数直接给null,因此可能就会造成一些莫名其妙的问题,就像我们这个问题一样。
那这个问题如何解决呢?接着往下看
解决方案
由于这个是Hessian反序列化问题,因此与Dubbo的版本关系不大,为了验证这个我还专门使用apache dubbo 2.6.1版本测试了一下,问题依旧存在。
方法一:使用无参构造方法来创建对象
既然是hessian反序列化问题,而且它在反序列化时根据构造函数参数个数优先级来取参数最少的,那我们就可以增加一个无参的构造方法来解决这个问题。
但是有的时候我们使用的是第三方的包,不太好增加无参的构造方法,那怎么办的,我们能不能使用其他方法,继续往下看。
方法二:替换jsr303实现框架
既然hibernate-validator的org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl这个类在使用hessian反序列化存在问题,那我们使用其他jsr303的框架来试试。
jsr303的实现框架有哪些?
- org.hibernate : hibernate-validator : 5.2.4.Final
- org.apache.bval : bval-jsr303 : 0.5
- jersery
bval是apache的一个bean validator的实现,jersery是一个restful的框架为了满足自身的数据验证功能因此增加了jsr303的实现。
由于我们使用的springmvc构建restful因此这里就不考虑jersery,我们就从bval下手来试一试。
在进行了一番配置后(都有哪些配置?)
-
增加bval包,现在版本是:0.5
<dependency> <groupId>org.apache.bval</groupId> <artifactId>bval-jsr303</artifactId> <version>0.5</version> </dependency> -
将bval集成到spring框架中,作为spring的验证框架
这里有两种方式,一种xml配置,一种java config
xml方式:
1<mvc:annotation-driven validator="validator"/> 2 3<!-- 数据验证 Validator bean --> 4<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> 5 <property name="providerClass" value="org.apache.bval.jsr.ApacheValidationProvider" /> 6</bean>
java config方式: 重写mvcValidator方法
1 @Override 2 public Validator mvcValidator() { 3 Validator validator = super.mvcValidator(); 4 if (validator instanceof LocalValidatorFactoryBean) { 5 LocalValidatorFactoryBean lvfb = (LocalValidatorFactoryBean) validator; 6 try { 7 String className = "org.apache.bval.jsr303.ApacheValidationProvider"; 8 Class<?> clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader()); 9 lvfb.setProviderClass(clazz); 10 } 11 catch (ClassNotFoundException e) { 12 //没有找到bval验证框架,走spring默认整合的验证框架:hibernate-validator 13 //这里异常没有必要跑出去,直接吃掉 14 } 15 } 16 return validator; 17 }
启动后验证功能
但是不好的事情发生了,无法启动报错,错误如下:
1java.lang.AbstractMethodError: org.apache.bval.jsr303.ConfigurationImpl.getDefaultParameterNameProvider() 2 3Ljavax/validation/ParameterNameProvider;
经过对spring的资料查找,发现spring从4.0版本往后不在支持集成其他jsr303的框架了,只能使用hibernate-validator,我擦这个有点暴力了。即使自己实现一个jsr303框架也无法再spring中使用,除非不使用spring validator功能,直接使用自己的验证框架来进行验证,这样就无法使用@Validated param方式。
那这种方法只能放弃了。
方法三:修改hibernate-validator的原声类,修改Dubbo ValidationFilter,这也是我最终采用的方法
其实替换jsr303框架不能成功,替换序列化协议应该也可以避免这个问题,只不过替换协议这个一般在维护的项目中不太会选择这样的方式来动刀子,现在开发很多都是分布式服务,序列化反序列化已经无处不在了,因此我建议编写代码时都增加一个无参数的构造方法,养成这样的一个好习惯可以避免很多序列化反序列化框架的坑。而且还有那些有匿名内部类的这种在序列化反序列化也需要注意,不是所有的序列化反序列化框架都支持有匿名类,gson是支持的这个为测试过,我前面也写过一篇博文里面就主要说这个问题,可以查看:《Java中内部类使用注意事项,内部类对序列化与反序列化的影响》
有兴趣的可以看一下我们常用的序列化反序列化类库的一些使用中的注意事项,可以参考这篇文章:《java常用JSON库注意事项总结》
回归话题,上面的问题我们如何解决,最终我们采用重写javax.validation.ConstraintViolation<T>的实现类,替换掉hibernate-validation的org.hibernate.validator.internal.engine.ConstraintViolationImpl,因为ConstraintViolationImpl中有部分对象无法通过hessian反序列化。
我们最终的目标是不管是validation开启在provider端还是consumer端,调用方接收到的参数校验异常数据是一致的。
修改的代码已经提交到apache dubbo,具体查看Pull request:https://github.com/apache/incubator-dubbo/pull/1708
大概的代码如下:
增加类:DubboConstraintViolation实现javax.validation.ConstraintViolation接口
1import java.io.Serializable; 2import javax.validation.ConstraintViolation; 3import javax.validation.Path; 4import javax.validation.ValidationException; 5import javax.validation.metadata.ConstraintDescriptor; 6import com.alibaba.dubbo.common.logger.Logger; 7import com.alibaba.dubbo.common.logger.LoggerFactory; 8 9 10public class DubboConstraintViolation<T> implements ConstraintViolation<T>, Serializable { 11 12 static final Logger logger = LoggerFactory.getLogger(DubboConstraintViolation.class.getName()); 13 14 private static final long serialVersionUID = -8901791810611051795L; 15 16 private String interpolatedMessage; 17 private Object value; 18 private Path propertyPath; 19 private String messageTemplate; 20 private Object[] executableParameters; 21 private Object executableReturnValue; 22 private int hashCode; 23 24 public DubboConstraintViolation() { 25 } 26 27 public DubboConstraintViolation(ConstraintViolation<T> violation) { 28 this(violation.getMessageTemplate(), violation.getMessage(), violation.getInvalidValue(), violation.getPropertyPath(), 29 violation.getExecutableParameters(), violation.getExecutableReturnValue()); 30 } 31 32 public DubboConstraintViolation(String messageTemplate, 33 String interpolatedMessage, 34 Object value, 35 Path propertyPath, 36 Object[] executableParameters, 37 Object executableReturnValue) { 38 this.messageTemplate = messageTemplate; 39 this.interpolatedMessage = interpolatedMessage; 40 this.value = value; 41 this.propertyPath = propertyPath; 42 this.executableParameters = executableParameters; 43 this.executableReturnValue = executableReturnValue; 44 // pre-calculate hash code, the class is immutable and hashCode is needed often 45 this.hashCode = createHashCode(); 46 } 47 48 @Override 49 public final String getMessage() { 50 return interpolatedMessage; 51 } 52 53 @Override 54 public final String getMessageTemplate() { 55 return messageTemplate; 56 } 57 58 @Override 59 public final T getRootBean() { 60 return null; 61 } 62 63 @Override 64 public final Class<T> getRootBeanClass() { 65 return null; 66 } 67 68 @Override 69 public final Object getLeafBean() { 70 return null; 71 } 72 73 @Override 74 public final Object getInvalidValue() { 75 return value; 76 } 77 78 @Override 79 public final Path getPropertyPath() { 80 return propertyPath; 81 } 82 83 @Override 84 public final ConstraintDescriptor<?> getConstraintDescriptor() { 85 return null; 86 } 87 88 @Override 89 public <C> C unwrap(Class<C> type) { 90 if ( type.isAssignableFrom( ConstraintViolation.class ) ) { 91 return type.cast( this ); 92 } 93 throw new ValidationException("Type " + type.toString() + " not supported for unwrapping."); 94 } 95 96 @Override 97 public Object[] getExecutableParameters() { 98 return executableParameters; 99 } 100 101 @Override 102 public Object getExecutableReturnValue() { 103 return executableReturnValue; 104 } 105 106 @Override 107 // IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF) 108 109 // Do not take expressionVariables into account here. If everything else matches, the two CV should be considered 110 // equals (and because of the scary comment above). After all, expressionVariables is just a hint about how we got 111 // to the actual CV. (NF) 112 public boolean equals(Object o) { 113 if ( this == o ) { 114 return true; 115 } 116 if ( o == null || getClass() != o.getClass() ) { 117 return false; 118 } 119 120 DubboConstraintViolation<?> that = (DubboConstraintViolation<?>) o; 121 122 if ( interpolatedMessage != null ? !interpolatedMessage.equals( that.interpolatedMessage ) : that.interpolatedMessage != null ) { 123 return false; 124 } 125 if ( propertyPath != null ? !propertyPath.equals( that.propertyPath ) : that.propertyPath != null ) { 126 return false; 127 } 128 if ( messageTemplate != null ? !messageTemplate.equals( that.messageTemplate ) : that.messageTemplate != null ) { 129 return false; 130 } 131 if ( value != null ? !value.equals( that.value ) : that.value != null ) { 132 return false; 133 } 134 135 return true; 136 } 137 138 @Override 139 public int hashCode() { 140 return hashCode; 141 } 142 143 @Override 144 public String toString() { 145 final StringBuilder sb = new StringBuilder(); 146 sb.append( "DubboConstraintViolation" ); 147 sb.append( "{interpolatedMessage='" ).append( interpolatedMessage ).append( '\'' ); 148 sb.append( ", propertyPath=" ).append( propertyPath ); 149 sb.append( ", messageTemplate='" ).append( messageTemplate ).append( '\'' ); 150 sb.append( ", value='" ).append( value ).append( '\'' ); 151 sb.append( '}' ); 152 return sb.toString(); 153 } 154 155 // Same as for equals, do not take expressionVariables into account here. 156 private int createHashCode() { 157 int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0; 158 result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 ); 159 result = 31 * result + ( value != null ? value.hashCode() : 0 ); 160 result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 ); 161 return result; 162 } 163 164} 165
修改com.alibaba.dubbo.validation.filter.ValidationFilter异常处理的部分
这里的变更为捕捉javax.validation.ConstraintViolationException异常,对异常中的Set<ConstraintViolation<String>>数据进行转换,去掉无法反序列化的对象,具体代码如下:
1public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { 2 if (validation != null && !invocation.getMethodName().startsWith("$") 3 && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.VALIDATION_KEY))) { 4 try { 5 Validator validator = validation.getValidator(invoker.getUrl()); 6 if (validator != null) { 7 validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()); 8 } 9 } catch (ConstraintViolationException e) { 10 Set<ConstraintViolation<?>> set = null; 11 //验证set中如果是hibernate-validation实现的类就处理,其他的实现类放过 12 Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations(); 13 for (ConstraintViolation<?> v : constraintViolations) { 14 if (!v.getClass().getName().equals("org.hibernate.validator.internal.engine.ConstraintViolationImpl")) { 15 return new RpcResult(e); 16 } else { 17 if (set == null) set = new HashSet<ConstraintViolation<?>>(); 18 set.add(new DubboConstraintViolation<>(v)); 19 } 20 } 21 return new RpcResult(new ConstraintViolationException(e.getMessage(), set)); 22 } catch (RpcException e) { 23 throw e; 24 } catch (Throwable t) { 25 return new RpcResult(t); 26 } 27 } 28 return invoker.invoke(invocation); 29}
使用这个方法后,在provider端设置validation=true,consumer端可以正常拿到所有校验数据的异常信息。
总结
我觉得这个方法并不是完美的方法,虽然这个问题是hibernate-validator框架的问题,hibernate-validator出生的年代分布式还不是特别的完善因此没有充分的考虑序列化反序列化问题也很正常,但是作为Dubbo框架在集成jsr303的时候也需要考虑这些问题。具体可以查看Apache Dubbo的Pull Request:https://github.com/apache/incubator-dubbo/pull/1708