【Java高级】类加载器核心技术,从自定义加载外部jar说起

本文为原创文章,转载请注明出处
查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443

我们先举个例子,假如我们有如下的类:

package com.codelifeliwan;

public class Test {

    public void test() {
        System.out.println("----------------------BEGIN test--------------------");
        System.out.println("this is thread:" + Thread.currentThread().getId() +
                "\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
                "\nClassLoader:" + this.getClass().getClassLoader());
    }

    // 注意这里是静态方法
    public static void staticTest() {
        System.out.println("----------------------BEGIN staticTest--------------------");
        System.out.println("this is thread:" + Thread.currentThread().getId() +
                "\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
                "\nClassLoader:" + Test.class.getClassLoader());
    }
}

我们将这个类打成jar包test.jar,那么我们如何在另外一个程序里面加载这个jar包并使用其中的程序?

双亲委托类加载器

一个比较通用的方法,是自定义一个类加载器来加载这个jar包,这里我们使用URLClassLoader类加载器:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {
    // 全局共享的外部类加载器
    private static ClassLoader classLoader = null;

    // 初始化类加载器
    synchronized public static void setClassLoader() throws Exception {
        if (classLoader != null) return;
        classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
        Thread.currentThread().setContextClassLoader(classLoader);
    }

    // 测试类加载器
    public static void doTest() throws Exception {
        Class clazz = classLoader.loadClass("com.codelifeliwan.Test"); // 使用URLClassLoader类加载器加载jar中的类
        clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法

        Object obj = clazz.getConstructor(null).newInstance();
        clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法

        System.out.println("=================================================\n");
    }

    public static void main(String[] args) throws Exception {
        setClassLoader(); // 初始化类加载器
        doTest(); // 测试类加载器
    }
}

注意,这里测试类加载器使用的是反射的方式。

程序输出:

----------------------BEGIN staticTest--------------------
this is thread:1
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:1
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================

可以看到,我们能够加载对应的jar中的类,并进行实例化和调用等。

我们主要看输出的信息:

ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d

可以看到,虽然我们用的是自己定义的URLClassLoader类加载器来进行加载外部的程序的,但是这里实际使用的是应用类加载器AppClassLoader(在创建classLoader的时候自动设置这个对象的类加载器为应用类加载器)。我们跟踪进入ClassLoader类的protected Class loadClass(String name, boolean resolve)方法中查看,发现在加载的时候执行了:

c = parent.loadClass(name, false);

这里,parent就是应用类加载器,加载完的c不为空则直接返回相应的类,对于每一级类加载器,系统会优先使用父级类加载器,只有当父级类加载器加载失败的时候才使用当前的类加载器。这就是著名的类加载的【双亲委托模型】,相关资料请自行查阅。

线程上下文类加载器

现在,我们将测试的程序稍微改一下:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {
    // 全局共享的外部类加载器
    private static ClassLoader classLoader = null;

    // 初始化类加载器
    synchronized public static void setClassLoader() throws Exception {
        if (classLoader != null) return;
        classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});

        // 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
        Thread.currentThread().setContextClassLoader(classLoader);
    }

    // 测试类加载器
    public static void doTest() throws Exception {
        Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
        clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法

        Object obj = clazz.getConstructor(null).newInstance();
        clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法

        System.out.println("=================================================\n");
    }

    public static void main(String[] args) throws Exception {
        new Thread(new MyRunnable()).start();
        Thread.sleep(1000); // 为了避免打印混乱
        new Thread(new MyRunnable()).start();
        Thread.sleep(1000); // 为了避免打印混乱
    }

    public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            try {
                setClassLoader();
                doTest();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

我们先来看输出结果:

----------------------BEGIN staticTest--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@24912ef7
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@24912ef7
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================

----------------------BEGIN staticTest--------------------
this is thread:15
ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:15
ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================

我们分别开了14和15两个线程,在14线程中设置了线程的上下文类加载器,那么可以看到,两个线程的ContextClassLoader结果是不一样的,线程设置的上下文类加载器可以遗传到子线程中,然后在子线程中可以使用对应的类加载器来加载其他的程序。

注意:其实,线程上下文类加载器更应该叫做线程下文类加载器,加载器只会遗传给子线程,对父线程和兄弟线程没有影响。上面的输出结果说明了这一点。

为了说明线程上下文类加载器会遗传到子线程,我们把初始化类加载器的工作放到主线程中:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {
    // 全局共享的外部类加载器
    private static ClassLoader classLoader = null;

    // 初始化类加载器
    synchronized public static void setClassLoader() throws Exception {
        if (classLoader != null) return;
        classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});

        // 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
        Thread.currentThread().setContextClassLoader(classLoader);
    }

    // 测试类加载器
    public static void doTest() throws Exception {
        Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
        clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法

        Object obj = clazz.getConstructor(null).newInstance();
        clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法

        System.out.println("=================================================\n");
    }

    public static void main(String[] args) throws Exception {
        setClassLoader(); // 初始化类加载器放在主线程

        new Thread(new MyRunnable()).start();
        Thread.sleep(1000); // 为了避免打印混乱
        new Thread(new MyRunnable()).start();
        Thread.sleep(1000); // 为了避免打印混乱
    }

    public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            try {
                doTest();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

然后会输出结果:

----------------------BEGIN staticTest--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================

----------------------BEGIN staticTest--------------------
this is thread:15
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:15
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================

JDBC的驱动就是使用线程上下文类加载器,读者可自行查阅资料

你可能感兴趣的:(【Java高级】类加载器核心技术,从自定义加载外部jar说起)