【Dubbo】默认hession2反序列化机制导致dubbo接口返回HashMap

问题描述

  • 在使用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.TomcatEmbeddedWebappClassLoaderorg.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的类加载器
  • 遇到一些莫名其妙的问题时,不要慌,耐心的断点,分析代码查找问题的原因
  • 解决问题时,最好深刻理解源码的设计,利用原有框架的扩展性,进行问题的修复,这样的代码是最优雅的

你可能感兴趣的:(Spring,Boot,dubbo,hessian2,HashMap,反序列化,SpringBoot,类加载,ClassLoader)