• 类隔离(自定义类加载器实现)


    前言

    由于微服务的快速迭代、持续集成等特性,越来越多的团队更倾向于它。但是也体现出了一些问题,比如在基础设施建设过程中,需要把通用功能下沉,把现有大而全的基础设施按领域拆分,考虑需要兼容现有生产服务,会产生不同的依赖版本,有时不注意就可以引发问题。比如本文遇到的依赖包版本冲突问题,以及如何利用类隔离技术解决的分析。

    类隔离是什么?

    类隔离是一种通过类加载器实现加载所需类的实现方式,使得不同版本类间隔离,避免了使用冲突问题,最终的效果就是不同模块的内容被不同的类加载器加载,满足同一环境下同时兼容不同接口实现类。

    使用场景

    比如业务服务A和业务服务B均需要消息通知等,均依赖消息中间件,但所引用版本不一致,导致最终只有一个版本加载到JVM,在某一个服务调用时会出现 NoSuchMethodError或NoSuchClassError问题,这就很难排查出来,没准会影响项目进度,最终月度的绩效(“鸡腿”)不保。

    服务A pom.xml:

    1.   
    2.         <dependency>
    3.             <groupId>com.lgygroupId>
    4.             <artifactId>spring-common-messageartifactId>
    5.             <version>1.0.0<version>
    6.         dependency>

    服务B pom.xml:

    1.   
    2.         <dependency>
    3.             <groupId>com.lgygroupId>
    4.             <artifactId>spring-common-messageartifactId>
    5.             <version>2.0.0<version>
    6.         dependency>

    业务调用流程:

    1.  // 业务A调用微信服务通知
    2.  MessageUtil.sendMessage(content,peopleId,templateId,"wechat");
    3.  // 业务B调用微信服务通知
    4.  MessageUtil.sendToWechat(content,peopleId,templateId);

    JVM最终加载的为 2.0.0 版本的依赖,导致业务A在调用时抛异常java.lang.NoSuchMethodError。

    解决方案

    大体的解决思路就是,在不改变业务代码的前提下, 业务A调用 1.0.0 版本的消息工具类, 业务B调用2.0.0版本的消息工具类,因此需要JVM能够利用自定义类加载器加载所需的类或关联的类。

    实现思路

    • 重写类加载器,实现自定义类加载(java.lang.ClassLoader)

    • 重写类加载函数

      • 重写 findClass(String name)

      • 重写 loadClass(String name)

    涉及的知识点

    • JVM加载过程:加载-》链接-》初始化(具体后续介绍)

    • 双亲委派机制:委托父加载器查询;如果父加载器查询不到,则调用自身的findClass加载

    重写findClass:

    1.  import java.io.*;
    2.  import java.util.HashMap;
    3.  import java.util.Map;
    4.  public class CustomerFindClass extends ClassLoader {
    5.   private Map classPathMap = new HashMap<>();
    6.   public CustomerFindClass() {
    7.    // 业务A的自定义类加载器
    8.    classPathMap.put("com.lgy.businessA.service.impl.MessageServiceImpl""E:/dataway-demo/example/target/classes/com/lgy/businessA/service/impl/MessageServiceImpl.class");
    9.    classPathMap.put("com.lgy.v1.message.util.MessageUtil""E:/dataway-demo/example/target/classes/com/lgy/v1/message/util/MessageUtil.class");
    10.   }
    11.   
    12.   /**
    13.   * findClass方式加载类
    14.   */
    15.   @Override
    16.   protected Class findClass(String name) throws ClassNotFoundException {
    17.    String classPath = classPathMap.get(name);
    18.    File file = new File(classPath);
    19.    if (!file.exists()) {
    20.     throw new ClassNotFoundException();
    21.    }
    22.    byte[] bytes = getClassData(file);
    23.    if (null == bytes || 0 == bytes.length) {
    24.     throw new ClassNotFoundException();
    25.    }
    26.    return defineClass(bytes, 0, bytes.length);
    27.   }
    28.   
    29.   private byte[] getClassData(File file) {
    30.    try (InputStream ins = new FileInputStream(file); 
    31.      ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    32.     byte[] buffer = new byte[4096];
    33.     int bytesNumRead = 0;
    34.     while ((bytesNumRead = ins.read(buffer)) != -1) {
    35.      baos.write(buffer, 0, bytesNumRead);
    36.     }
    37.     return baos.toByteArray();
    38.    } catch (FileNotFoundException e) {
    39.     e.printStackTrace();
    40.    } catch (IOException e) {
    41.     e.printStackTrace();
    42.    }
    43.    return new byte[]{};
    44.   }

    最终结果与预期的结果不一致

    • 预期结果:业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载

    • 实际结果:业务A的MessageServiceImpl由CustomerFindClass加载,而MessageUtil由sun.misc.AppClassLoader加载。

    • 分析:由于JVM类加载的双亲委托机制,业务A调用消息工具类时,类加载器(CustomerFindClass)会委托父类加载器(AppClassLoader)加载类,如果存在,则不再执行自身的findClass方法加载,导致结果不理想。(main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的)。

    重写loadClass

    1.  private ClassLoader classLoader;
    2.  
    3.  /**
    4.  * 重新loadClass方法
    5.  */
    6.  @Override
    7.     protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
    8.         Class result = null;
    9.         try {
    10.             //这里要使用 JDK 的类加载器加载 java.lang 包里面的类
    11.             result = classLoader.loadClass(name);
    12.         } catch (Exception e) {
    13.             // ignore error
    14.         }
    15.         if (null != result) {
    16.             return result;
    17.         }
    18.         String classPath = classPathMap.get(name);
    19.         File file = new File(classPath);
    20.         if (!file.exists()) {
    21.             throw new ClassNotFoundException();
    22.         }
    23.         byte[] bytes = getClassData(file);
    24.         if (null == bytes || 0 == bytes.length) {
    25.             throw new ClassNotFoundException();
    26.         }
    27.         return defineClass(bytes, 0, bytes.length);
    28.     }

    满足业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载

    注意:这种方式破坏了双亲委托机制,但由于重写了loadClass方法,所有类均会有CustomerFindClass加载器加载,需要过滤出不需要隔离的类,如java.lang包下的类,需要由ExtClassLoader 来加载。

    总结

    本文分享的方式是从类加载器方向出发,实现最终的类隔离,避免了不同模块间不同类的冲突,其中顺便也简单带过了jvm类加载相关的知识点,也算是一劳多得,后续会结合实际使用场景进一步分析。

    当然也有很多其它方式可以实现,有时间、有想法的朋友可以留言探讨交流。

  • 相关阅读:
    uniapp 打包小程序体积优化思路、优先排查优化项参考
    C和指针 第13章 高级指针话题 13.6 总结
    SQLite 3.37.0 发布,支持严格的字段数据类型
    搜索与图论篇——图的最短路
    邻接表的链表实现——链式前向星
    156 - Ananagrams (UVA)
    bind、apply、call 的区别
    在C语言中,堆和栈是两种不同的内存分配机制
    Python 基础合集2:字符串格式化
    2023年智能家居占消费电子出货量28%,蓝牙Mesh照明占据重要位置
  • 原文地址:https://blog.csdn.net/weixin_60227714/article/details/126226441