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



    前言

    目前微服务多采用分层模式,其中基础设施建设尤为重要,相当于系统或平台的根基,比较庞大,但随之而来的问题也逐渐暴露,比如依赖包版本冲突问题,本次介绍的类隔离技术就可以解决这个问题。


    一、类隔离是什么?

    类隔离是一种通过类加载器实现加载所需类的实现方式,使得不同版本类间隔离,避免了加载冲突问题。

    二、使用场景

    • 比如业务服务A和业务服务B均需要消息通知等,均依赖消息中间件,但所引用版本不一致,导致最终只有一个版本加载到JVM,在某一个服务调用时会出现 NoSuchMethodError或NoSuchClassError问题,这就很难排查出来,没准会影响项目进度,最终月度的绩效(“鸡腿”)不保。
      服务A pom.xml:
    		<!-- common-message-->
            <dependency>
                <groupId>com.lgy</groupId>
                <artifactId>spring-common-message</artifactId>
                <version>1.0.0<version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    服务B pom.xml:

    		<!-- common-message-->
            <dependency>
                <groupId>com.lgy</groupId>
                <artifactId>spring-common-message</artifactId>
                <version>2.0.0<version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // 业务A调用微信服务通知
    MessageUtil.sendMessage(content,peopleId,templateId,"wechat");
    // 业务B调用微信服务通知
    MessageUtil.sendToWechat(content,peopleId,templateId);
    
    • 1
    • 2
    • 3
    • 4
    • 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加载

    • 1、 重写findClass:
    import java.io.*;
    import java.util.HashMap;
    import java.util.Map;
    
    public class CustomerFindClass extends ClassLoader {
        private Map<String, String> classPathMap = new HashMap<>();
        public CustomerFindClass() {
        	// 业务A的自定义类加载器
            classPathMap.put("com.lgy.businessA.service.impl.MessageServiceImpl", "E:/dataway-demo/example/target/classes/com/lgy/businessA/service/impl/MessageServiceImpl.class");
            classPathMap.put("com.lgy.v1.message.util.MessageUtil", "E:/dataway-demo/example/target/classes/com/lgy/v1/message/util/MessageUtil.class");
        }
    	
    	/**
    	* findClass方式加载类
    	*/
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            String classPath = classPathMap.get(name);
            File file = new File(classPath);
            if (!file.exists()) {
                throw new ClassNotFoundException();
            }
            byte[] bytes = getClassData(file);
            if (null == bytes || 0 == bytes.length) {
                throw new ClassNotFoundException();
            }
            return defineClass(bytes, 0, bytes.length);
        }
    	
        private byte[] getClassData(File file) {
            try (InputStream ins = new FileInputStream(file); 
            		ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[4096];
                int bytesNumRead = 0;
                while ((bytesNumRead = ins.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesNumRead);
                }
                return baos.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return new byte[]{};
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 最终结果与预期的结果不一致
      • 预期结果:业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载
      • 实际结果:业务A的MessageServiceImpl由CustomerFindClass加载,而MessageUtil由sun.misc.AppClassLoader加载。
      • 分析:由于JVM类加载的双亲委托机制,业务A调用消息工具类时,类加载器(CustomerFindClass)会委托父类加载器(AppClassLoader)加载类,如果存在,则不再执行自身的findClass方法加载,导致结果不理想。(main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的)。
    • 2、重写loadClass
    	private ClassLoader classLoader;
    	
    	/**
    	* 重新loadClass方法
    	*/
    	@Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class result = null;
            try {
                //这里要使用 JDK 的类加载器加载 java.lang 包里面的类
                result = classLoader.loadClass(name);
            } catch (Exception e) {
                // ignore error
            }
            if (null != result) {
                return result;
            }
            String classPath = classPathMap.get(name);
            File file = new File(classPath);
            if (!file.exists()) {
                throw new ClassNotFoundException();
            }
            byte[] bytes = getClassData(file);
            if (null == bytes || 0 == bytes.length) {
                throw new ClassNotFoundException();
            }
            return defineClass(bytes, 0, bytes.length);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 满足业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载
      • 注意:这种方式破坏了双亲委托机制,但由于重写了loadClass方法,所有类均会有CustomerFindClass加载器加载,需要过滤出不需要隔离的类,如java.lang包下的类,需要由ExtClassLoader 来加载。

    总结

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

    • 当然也有很多其它方式可以实现,有时间可以探讨交流。

    • 参考链接: 肖汉松-如何实现Java类隔离加载?

  • 相关阅读:
    Spark Dataset 快速上手
    发布以太坊测试网络中的第一笔交易
    直播预告丨中高频多因子库存储的最佳实践
    Spring Batch -配置步骤 (XML/Java)
    docker基本管理
    CSDN 五一创作勋章Lv4 勋章_(标签-ar)
    决策树与随机森林
    Java基础学习总结(195)—— 关于 Java 8 中的日期处理总结
    3D虚拟数字人定制,推动传统文化传播新高度
    nacos
  • 原文地址:https://blog.csdn.net/qq_39486758/article/details/125487016