问题描述
在使用dubbo
调用接口的时候,莫名其妙出现java.lang.ClassCastException: java.util.HashMap cannot be cast to xxxx
异常 经过排查发现,是因为dubbo
接口返回的不是xxxx
对象,而是HashMap
源码分析
dubbo
的反序列化机制默认是hessian2
首先定位到SerializerFactory
类的getDeserializer()
方法
try {
// Class cl = Class.forName(type, false, _loader);Class cl = loadSerializedClass(type);deserializer = getDeserializer(cl);
} catch (Exception e) {log.warning("Hessian/Burlap: '" + type + "' is an unknown class in " + _loader + ":\n" + e);_typeNotFoundDeserializerMap.put(type, PRESENT);log.log(Level.FINER, e.toString(), e);_unrecognizedTypeCache.put(type, new AtomicLong(1L));
}
断点发现deserializer = getDeserializer(cl);
这一行代码抛了异常,所以能看到日志输出
log.warning("Hessian/Burlap: '" + type + "' is an unknown class in " + _loader + ":\n" + e);
然后手工试了Class.forName
,确实会报java.lang.ClassNotFoundException
异常 那问题就与类加载器有关了,看看hessian2
的类加载器是哪个 打开org.apache.dubbo.common.serialize.hessian2.Hessian2SerializerFactory
这个类
public class Hessian2SerializerFactory extends SerializerFactory { @Override public ClassLoader getClassLoader ( ) { return Thread . currentThread ( ) . getContextClassLoader ( ) ; }
}
Hessian2
重写了类加载器,为了观察这是哪个类加载器,增加日志输出
public class Hessian2SerializerFactory extends SerializerFactory { @Override public ClassLoader getClassLoader ( ) { ClassLoader classLoaderTest = Thread . currentThread ( ) . getContextClassLoader ( ) ; loadClass ( classLoaderTest, "com.xxxx" ) ; ClassLoader parent = classLoaderTest. getParent ( ) ; while ( parent!= null ) { loadClass ( classLoaderTest, "com.xxxx" ) ; parent = parent. getParent ( ) ; } return classLoaderTest; } private void loadClass ( ClassLoader classLoader, String className) { try { classLoader. loadClass ( className) ; log. info ( "load success: {}" , classLoader. getClass ( ) . getName ( ) ) ; } catch ( ClassNotFoundException e) { log. info ( "load fail: {}" , classLoader. getClass ( ) . getName ( ) ) ; } }
}
经过测试发现,能正常加载类的类加载器是org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
和org.springframework.boot.loader.LaunchedURLClassLoader
,两个类加载器都是Spring
的 而不可以加载到类的类加载器是jdk.internal.loader.ClassLoaders.AppClassLoader
所以,到了这里,问题就很明显了,dubbo
消费者在反序列化的时候,由于加载不到这个类,所以返回的对象按HashMap
处理
解决方案
参考org.springframework.util.ClassUtils
工具类的getDefaultClassLoader()
方法
@Nullable
public static ClassLoader getDefaultClassLoader() {ClassLoader cl = null;try {cl = Thread.currentThread().getContextClassLoader();}catch (Throwable ex) {// Cannot access thread context ClassLoader - falling back...}if (cl == null) {// No thread context class loader -> use class loader of this class.cl = ClassUtils.class.getClassLoader();if (cl == null) {// getClassLoader() returning null indicates the bootstrap ClassLoadertry {cl = ClassLoader.getSystemClassLoader();}catch (Throwable ex) {// Cannot access system ClassLoader - oh well, maybe the caller can live with null...}}}return cl;
}
如果Thread.currentThread().getContextClassLoader()
返回的是JDK类加载器 那么就使用ClassUtils.class.getClassLoader()
代替 修改后的代码如下
@Overridepublic ClassLoader getClassLoader() {ClassLoader classLoader = ClassUtils.getDefaultClassLoader();if(Objects.nonNull(classLoader) && classLoader.getClass().getName().contains("spring")) {return classLoader;}ClassLoader classLoaderNew = ClassUtils.class.getClassLoader();log.info("classLoader is wrong: {}, change to: {}", classLoader, classLoaderNew);return classLoaderNew;}
[2023-12-20 18:56:48.683] [INFO] [ForkJoinPool.commonPool-worker-84]c.m.v.i.d.h.xxxxHessian2SerializerFactory[56][]- classLoader is wrong: jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93, change to: org.springframework.boot.loader.LaunchedURLClassLoader@14514713
最终dubbo
接口可以正常返回Java
对象,而不是HashMap
了
利用dubbo的SPI机制
在resources
目录下新建文件META-INF/dubbo/org.apache.dubbo.common.serialize.hessian2.dubbo.Hessian2FactoryInitializer
文件内容如下
default=xxxx.xxxxHessian2FactoryInitializer
这样可以指定自定义的Hessian2FactoryInitializer 在这个Hessian2FactoryInitializer里面创建自定义的Hessian2SerializerFactory
@Slf4j
public class xxxxHessian2FactoryInitializer extends DefaultHessian2FactoryInitializer { @Override public SerializerFactory getSerializerFactory ( ) { Hessian2SerializerFactory hessian2SerializerFactory = new xxxxHessian2SerializerFactory ( ) ; hessian2SerializerFactory. getClassFactory ( ) . allow ( RuntimeException . class . getName ( ) ) ; hessian2SerializerFactory. setAllowNonSerializable ( Boolean . parseBoolean ( System . getProperty ( "dubbo.hessian.allowNonSerializable" , "false" ) ) ) ; return hessian2SerializerFactory; }
}
总结
虽然找到了问题的原因,也解决了问题,但依然有疑问,如果启动多次,有时候能正常转成Java
对象,是一个随机的状态,感觉启动的时候决定了这个hessian2
的类加载器 遇到一些莫名其妙的问题时,不要慌,耐心的断点,分析代码查找问题的原因 解决问题时,最好深刻理解源码的设计,利用原有框架的扩展性,进行问题的修复,这样的代码是最优雅的