当前位置: 首页 > 科技观察

类隔离自定义类加载器实现,你学会了吗?

时间:2023-03-22 17:23:25 科技观察

前言由于微服务的快速迭代和持续集成,越来越多的团队对其青睐有加。但是,也存在一些问题。比如在基础设施建设过程中,需要降低通用功能,将现有的大而全的基础设施按领域拆分,考虑需要兼容现有的生产服务,这会产生不同的依赖。版本,有时候稍不注意就会出问题。比如本文遇到的依赖包版本冲突的问题,以及如何使用类隔离技术解决的分析。什么是阶级隔离?类隔离是通过类加载器加载需要的类的一种实现方式,使不同版本的类隔离,避免使用冲突问题,最终的效果是不同模块的内容由不同的类加载器加载,满足同一个环境,同时兼容不同的接口实现类。业务服务A和业务服务B等使用场景都需要消息通知等,都依赖消息中间件,但是引用的版本不一致,导致最后JVM只加载了一个版本,NoSuchMethodError或者NoSuchClassError会在调用某个服务时发生。这很难发现,而且可能会影响项目的进度,最终月度业绩(“鸡腿”)将无法保证。服务Apom.xml:com.lgyspring-common-message1.0.0服务Bpom.xml:com.lgyspring-common-message2.0.0业务调用流程://业务A调用微信服务通知MessageUtil.sendMessage(content,peopleId,templateId,"wechat");//业务B调用微信服务通知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(Stringname)重写loadClass(Stringname)涉及的知识点JVM加载过程:Loading-》链接-》初始化(具体后续介绍)双亲委托机制:委托父加载器查询;如果父加载器无法查询,则调用自己的findClass加载,重写findClass:importjava.io.*;importjava.util.HashMap;importjava.util.Map;publicclassCustomerFindClassextendsClassLoader{privateMapclassPathMap=newHashMap<>();publicCustomerFindClass(){//业务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加载类的方式*/@OverrideprotectedClassfindClass(Stringname)throwsClassNotFoundException{StringclassPath=classPathMap.get(name);文件文件=新文件(类路径);如果(!文件.exists()){抛出新的ClassNotFoundException();}byte[]bytes=getClassData(文件);if(null==bytes||0==bytes.length){thrownewClassNotFoundException();}returndefineClass(bytes,0,bytes.length);}privatebyte[]getClassData(Filefile){try(InputStreamins=newFileInputStream(file);ByteArrayOutputStreambaos=newByteArrayOutputStream()){byte[]buffer=newbyte[4096];intbytesNumRead=0;while((bytesNumRead=ins.read(buffer))!=-1){baos.write(buffer,0,bytesNumRead);}返回baos.toByteArray();}catch(FileNotFoundExceptione){e.打印堆栈跟踪();}catch(IOExceptione){e.printStackTrace();}返回新字节[]{};最终结果与预期结果不一致:预期结果:业务A的MessageServiceImpl和MessageUtil由CustomerFindClass加载实际结果:业务A的MessageServiceImpl由CustomerFindClass加载,MessageUtil由sun.misc.AppClassLoader加载分析:由于JVM类加载的父委托机制,当业务A调用消息工具类时,类加载器(CustomerFindClass)会委托父类加载器(AppClassLoader)加载该类。如果存在,则不再执行自己的findClass方法加载。导致不满意的结果。(main方法类默认由JDK自带的AppClassLoader加载)。重写loadClassprivateClassLoader类加载器;/***ReloadClass方法*/@OverrideprotectedClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{Classresult=null;try{//这里使用JDK的类加载器加载java.lang包中的类result=classLoader.loadClass(name);}catch(Exceptione){//忽略错误}if(null!=result){returnresult;}StringclassPath=classPathMap.get(name);文件文件=新文件(类路径);如果(!file.exists()){抛出新的ClassNotFoundException();}byte[]bytes=getClassData(文件);if(null==bytes||0==bytes.length){thrownewClassNotFoundException();}returndefineClass(bytes,0,bytes.length);}满足业务A的MessageServiceImpl和MessageUtil由CustomerFindClass加载注:该方法破坏了双亲委托机制,但由于重写了loadClass方法,所有的类都会被CustomerFindClass加载器加载,需要过滤掉不需要的类被隔离,比如java.lang包下的类需要ExtClassLoader加载。总结这篇文章的方式就是从类加载器的方向入手,实现最终的类隔离,避免不同模块之间不同类的冲突。顺便说一下,jvm也简单地包括在内。类加载相关的知识点也算是多劳多得。