问题描述
- 在使用
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 ClassLoader
try {
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()
代替 - 修改后的代码如下
@Override
public 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
的类加载器 - 遇到一些莫名其妙的问题时,不要慌,耐心的断点,分析代码查找问题的原因
- 解决问题时,最好深刻理解源码的设计,利用原有框架的扩展性,进行问题的修复,这样的代码是最优雅的