自定义类加载器解决jar冲突问题

泥瓦匠攻城狮

共 8328字,需浏览 17分钟

 ·

2021-11-18 21:38

碎碎念

好吧,我是一个没有意志力,贪图安逸的人,懒惰、不思进取的人!好久没有发过文了,为啥?一个字:懒!

引言

目前维护的项目是一个非常非常原始的项目,这里可以发挥你的想象力,你能想到多原始就有多原始……

最近需要在原有环境中重新搭建一个新的工程,当然这个工程与原来其他七八十来个工程共用一个 JVM 环境。有经验的老铁看到这里脑海里肯定会有一堆的问题……好吧,其他暂不讨论,这次主要是解决 jar 冲突的问题。

因为原有工程中用到一个 jar 是低版本的,而新工程也用到这个 jar,但是需要高版本的。其他工程公用的低版本肯定是不能丢的,影响太大。那如果再引入高版本的 jar,肯定就会出现版本冲突问题。又因为时间紧迫,且为了安全高效,领导决定这个新工程按照原有模式进行开发,就是沿用原有工程的技术框架,能不改就不改,毕竟以后生产环境也是要在一起发布的,不能出现乱七八糟的问题。有些老铁就想到解决版本冲突 maven 不就可以帮我们管理引用的 jar 吗?那我要告诉你老铁,你真的还很年轻!俺们现在的技术架构,没有引入类似 maven 这项高级技术。别操……闭嘴!听我继续吹!

如果再为它单独申请一台服务器,明显也是不可能的,太不划算(但事实上他们真的要这么干!)。我个人认为也是没必要的。因为这么一个小的项目,而且技术上也没有提升,其他不说,就单单因为沿用了那么老的技术框架,我觉得它就不值得一台新服务器,内心感觉那样很浪费资源!

怎么办?!事情还是要继续做的,稍作思考后,我想到了自定义类加载器!用它来解决 jar 版本冲突应该可以的吧?

网上先搜搜了解下,但网上那些博客已经被资本侵蚀了,有用的不多,没用的一堆,但可以告诉你这个东西有人做过,可以试试!试试就试试,正好也好久没用复习了!

快速回顾

这里首先强烈推荐一本经典的关于系统了解 JVM 的书籍,想必你已经猜了,那就是周老板的《深入理解Java虚拟机》,现在已经出了第三版了,我之前看的是第二版,准准准准准准准备入手第三版看看,肯定也是一场盛宴,第二版每次想起都回味无穷!强烈推荐大家有条件的看看!!!好了,废话不多说,开撸!

7b03e9f8071e55283df3ca9a689d54a2.webp

类加载器是什么

类加载器负责实现类的加载动作,通过一个类的全限定名来获取描述此类的二进制字节流。

每一个类加载器,都有一个独立的类名称空间。即:在一个 JVM 中比较两个类是否“相等”,只有这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,这两个类就必定不相等。

简单一句话,在同一个 JVM 环境中,如果要指定一个类,需要加载它的类加载器和其全限定名一起。

类加载器分类

从虚拟机的角度来讲,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap Classloader):使用 C++ 语言实现,是虚拟机自身的一部分;

负责加载在 /lib 目录中,或者被 JVM 的 -Xbootclasspath 参数所指定的路径中,并且是 JVM 识别的(JVM 仅按照文件名识别,比如:rt.jar,名字不符合的类库即使放在 lib 目录下也不会被加载)类库加载到虚拟机内存中。此加载器无法被 Java 程序直接引用。

其他类加载器:由 Java 语言实现,独立于虚拟机外部,并且全都继承自 java.lang.ClassLoader 抽象类:

  1. 扩展类加载器(Extension Classloader):负责加载 /lib/ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。开发者可以直接使用此类加载器。

  2. 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载用户路径(ClassPath)下所指定的类库,也就是说我们平时所编写的代码都是通过此类加载器进行加载的。开发者可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,一般情况下,这个就是应用程序中默认的类加载器。

我们应用程序都是由以上 3 种类加载器相互配合进行加载的,如果有必要,还可以自定义类加载器。通过继承 java.lang.ClassLoader 实现自己的类加载器。在程序运行期间动态加载 class 文件,体现了 Java 动态实时类装入特性。

这些类加载器之间的层次关系如下图:

d4e3ad96202f6cab46baa3afe2cfa5da.webp

双亲委派模型

不同类加载器之间的层次关系称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系并非继承,而是组合关系。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个类委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。但它并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的一种类加载器实现方式。

使用双亲委派模型对于保证 Java 程序的稳定运行很重要。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器都要加载这个类,但最终都会委派给启动类加载器来加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 ClassPath 中,那么系统中将出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将变得一片混乱。

在双亲委派模型下,如果编写一个与 rt.jar 类库中已有类重名的 Java 类,你将会发现它可以正常发现,但是永远无法被加载运行。

双亲委派模型的实现

虽然双亲委派模型非常重要,但是其实现还是比较简单,逻辑也比较清晰易懂。实现双亲委派模型的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中。

其首先检查当前类是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

protected Class loadClass(String name, boolean resolve)        throws ClassNotFoundException    {        synchronized (getClassLoadingLock(name)) {            /*             * 首先通过从已加载的类中查找当前类,来检查当前类是否已经被加载过,             * 在没有被加载过的情况下,执行加载逻辑             */            Class c = findLoadedClass(name);            if (c == null) {                long t0 = System.nanoTime();                try {                    // 父类加载器是否为空,如果不为空则委派父加载器加载,否则启动类加载器加载                    if (parent != null) {                        c = parent.loadClass(name, false);                    } else {                        c = findBootstrapClassOrNull(name);                    }                } catch (ClassNotFoundException e) {                    // 如果父类加载器抛出 ClassNotFoundException                     // 说明父类加载器无法完成加载请求                }                if (c == null) {                    /**                     * 在父类加载器无法完成加载的情况下,调用本身的 findClass 方法来进行加载。                     * findClass 是抽象类,需要在子类实现                     */                    long t1 = System.nanoTime();                    c = findClass(name);
// this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }

重点方法说明

类加载器中最重要的方法有以下三个,也是自定义类加载器时需要重点关注的:

  • loadClass:如上,该方法主要实现了双亲委派模型,是类加载器的模板方法,JVM 在加载类的时候都是通过 ClassLoader 的 loadClass 方法来加载的。如果要想打破双亲委派模型,需要重写 loadClass 方法。

  • findClass:类加载器加载类的具体实现方法。自定义类加载器通过实现这个方法来加载需要的类。

  • definedClass:在 findClass 中使用,通过调用传入的一个 Class 文件的字节数组,就可以在方法区生成一个 Class 对象,也就是 findClass 实现了类加载的功能。

特别提一下,URLClassLoader 是 JDK 已经实现的从指定URL地址来加载的类加载器,这个 URL 可以是本地目录,也可以是网络地址。扩展类加载器和应用类加载器就是继承自 URLClassLoader.java 这个类。

自定义类加载器

只说不练假把式,终于到本文的重点了。

快速回顾之后,我首先想到了一种自定义实现类加载器解决 jar 冲突的方法:

  1. 将引言中提到的新的高版本的 jar 放到磁盘的一个目录,而这个目录不属于启动类加载器所负责加载的目录;

  2. 编写一个自定义类加载器继承自 URLClassLoader 类,让其从我们指定的磁盘目录来加载;

  3. 使用自定义类加载器的时候,父类加载器设置为 null。

经过上面对 loadClass 方法的分析可以知道,这种情况下,当使用自定义类加载器加载一个类的时候,loadClass  方法中调用的应该是子类自己的 findClass 方法。并且在 URLClassLoader 中已经实现了从指定地址加载的机制,而我们需要做的就是指定加载的 URL 地址,事实是这样吗?一试便知!

/** * 自定义类加载器 */public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL[] urls) { // 父类加载器设置为 null super(urls,null); }}
/** * jar 包中的类 */public class D {
public static String innerInvoke(){ System.out.println("from D.class innerInvoke:" + System.currentTimeMillis()); return "from D.class innerInvoke:" + System.currentTimeMillis(); }}
public class DateUtils { public static String yyyy_MM_dd_HH_mm_ss = "yyyy-MM-dd HH:mm:ss"; public static String sysDateTime(){ System.out.println("CustomClassLoader invoke DateUtils.sysDateTime"); Date date = new Date(); SimpleDateFormat smf = new SimpleDateFormat(yyyy_MM_dd_HH_mm_ss); D.innerInvoke(); return smf.format(date); }}
/** * ClassPath 下的类,由 AplicationClassLoader 加载 */public class DateUtils { public static String yyyy_MM_dd_HH_mm_ss = "yyyy-MM-dd HH:mm:ss"; public static String sysDateTime(){ System.out.println("AplicationClassLoader invoke DateUtils.sysDateTime"); Date date = new Date(); SimpleDateFormat smf = new SimpleDateFormat(yyyy_MM_dd_HH_mm_ss); D.innerInvoke(); return smf.format(date); }}
/** * 测试类 */public class CustomClassLoaderTest {
private static final Logger logger = LogManager.getLogger();
private URL[] urls;
@Before public void init() throws Exception { urls = new URL[1]; File file = new File("E:\\jar\\commons-1.0-SNAPSHOT.jar"); urls[0] = file.toURI().toURL(); }
@Test public void methodInvokeTest() throws Exception{ CustomClassLoader customClassLoader = new CustomClassLoader(urls); Class aClass = customClassLoader.loadClass("com.syh.utils.DateUtils"); Method method = aClass.getMethod("sysDateTime"); System.out.println(method.invoke(aClass.newInstance())); System.out.println(DateUtils.sysDateTime()); }
}----------------------------------------运行结果-----------------------------------------CustomClassLoader Loading DateUtils.classCustomClassLoader invoke DateUtils.sysDateTimefrom D.class innerInvoke:16356972767912021-11-01 00:21:16ApplicationClassLoader invoke DateUtils.sysDateTime2021-11-01 00:21:16
Process finished with exit code 0


非常骚气,果然可以!!!!!!!!!!只需要在调用父类加载器的时候指定加载地址,并且将父类加载器设为 null !

但是程序中不可能只一次,或者只在一个地方使用我们这个自定义类加载器,但是每次都要重新创建吗?要知道类加载器,并不是一次性将目录下全部的类都加载到虚拟机,而是在需要使用某个类的时候才去加载,而且加载后会缓存起来,在下载执行加载的时候会先去缓存查看是不是已经加载过,如果加载过就直接返回而不是再次加载。这个在 loadClass 方法分析的时候已经明确了。

那么,我们可以对自定义的类加载器进行优化,使其成为一个单例。如下

public class CustomClassLoader extends URLClassLoader {
private static volatile CustomClassLoader customClassLoader = null;
// 私有化构造器,禁止在外部进行创建 private CustomClassLoader(URL[] urls){ super(urls,null); };
public static CustomClassLoader getInstance(URL[] urls){ if (customClassLoader == null){ synchronized (CustomClassLoader.class){ if (customClassLoader == null){ customClassLoader = new CustomClassLoader(urls); } } } return customClassLoader; }}


01e0d8eb490d2240defa8399a16bc27a.webp

小结

以上,对于理解类加载器和双亲委派模型还是很有帮助的。但虽然实现了自定义类加载器从指定目录加载 jar,但也是很有局限性的,想一下,如果自定目录下的 jar 还需要调用 ClassPath 目录下的类,以上实现还可以正常运行吗?换句话说,就是自定义类加载器需要用到另外一个类加载器加载的类对象,该怎么办?

其实整体梳理下来,类加载器其实并不是太难,实现逻辑也很清晰。事实上,应该实现一个比较健壮、支持更多场景的自定义类加载器,起码满足我上面的那个问题,可能比较复杂,但是可以支持更多的场景。

关注下,期待下一篇吧~


浏览 129
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报