关于 Java 类加载器的这一点,市面上没有任何一本图书讲到

1、一个程序员的思考

你们都知道,Tomcat 处理业务,靠什么?最终是靠咱们本身编写的 Servlet。你可能说你不写 servlet,你用 spring MVC,那也是人家帮你写好了,你只须要配置就行。在这里,有一个边界,Tomcat 算容器,容器的相关 jar 包都放在它本身的 安装目录的 lib 下面; 咱们呢,算是业务,算是webapp,咱们的 servlet ,不论是自定义的,仍是 spring mvc 的DispatcherServlet,都是放在咱们的 war 包里面 WEB-INF/lib下。 看过前面文章的同窗是晓得的, 这两者是由不一样的类加载器加载的。在 Tomcat 的实现中,会委托 webappclassloader 去加载WAR 包中的 servlet ,而后 反射生成对应的 servlet。后续有请求来了,调用生成的 servlet 的 service 方法便可。java

在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即负责 生成 servlet:mysql

org.apache.catalina.core.DefaultInstanceManager#newInstance(java.lang.String)
    @Override
    public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
        return newInstance(clazz.newInstance(), clazz);
    }
复制代码

在上图中,会利用 instanceManager 根据参数中指定的 servletClass 去生成 servlet 实例。newInstance 代码以下,主要就是用 当前 context 的classloader 去加载 该 servlet,而后 反射生成 servlet 对象。程序员

咱们重点关注的是那个红框圈出的强转:为何由 webappclassloader 加载的对象,能够转换 为 Tomcat common classloader 加载的 Servlet 呢? 按理说,两个不一样的类加载器加载的类都是互相隔离的啊,不该该抛一个 ClassCastException 吗?说真的,我翻了很多书,历来没提到这个,就连网上也很含糊。web

再来一个,关于SPI的问题。  在 SPI 中,主要是由 java 社区指定规范,好比 JDBC,厂家有那么多,mysql,oracle,postgre,你们都有本身的 jar包,要是没有 JDBC 规范,咱们估计就得针对各个厂家的实现类编程了,那迁移就麻烦了,你针对 mysql 数据库写的代码,换成 oracle 的话,代码不改是确定不能跑的。因此, JCP组织制定了 JDBC 规范,JDBC 规范中指定了一堆的 接口,咱们平时开发,只须要针对接口来编程,而实现怎么办,交给各厂家呗,由厂家来实现 JDBC 规范。这里以代码举例,oracle.jdbc.OracleDriver 实现了 java.sql.Driver,同时,在 oracle.jdbc.OracleDriver 的 static 初始化块中,有下面的代码:spring

static {
        try {
            if (defaultDriver == null) {
                defaultDriver = new oracle.jdbc.OracleDriver();
                DriverManager.registerDriver(defaultDriver);
            }
    // 省略
    }
复制代码

其中,标红这句,就是 Oracle Driver 要向 JDBC 接口注册本身,java.sql.DriverManager#registerDriver(java.sql.Driver)的实现以下:sql

java.sql.DriverManager#registerDriver(java.sql.Driver) 

public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }
复制代码

能够看到,registerDriver(java.sql.Driver) 方法的参数为 java.sql.Driver,而咱们传的参数为 oracle.jdbc.OracleDriver 类型,这两个类型,分别由不一样的类加载器加载(java.sql.Driver 由 jdk 的 启动类加载器加载,而 oracle.jdbc.OracleDriver ,若是为 web应用,则为 tomcat 的 webappclassloader 来加载,无论怎么说,反正不是由 jdk 加载的),这样的两个类型,连 类加载器都不同,怎么就能正常转换呢,为啥不抛 ClassCastException?数据库

2、不一样类加载器加载的类,能够转换的关键

通过上面两个例子的观察,不知道你们发现没, 咱们都是把一个实现,转换为一个接口。也许,这就是问题的关键。咱们能够大胆地推测,基于类的双亲委派机制,在 加载 实现类的时候,jvm 遇到 实现类中引用到的其余类,也会触发加载,加载的过程当中,会触发 loadClass,好比,加载 webappclassloader 在 加载 oracle.jdbc.OracleDriver 时,触发加载 java.sql.Driver,可是 webappclassloader 明显是不能去加载 java.sql.Driver 的,因而会委托给 jdk 的类加载,因此,最终,oracle.jdbc.OracleDriver 中 引用的 java.sql.Driver ,其实就是由 jdk 的类加载器去加载的。 而 registerDriver(java.sql.Driver driver) 中的 driver 参数的类型 java.sql.Driver 也是由 jdk 的类加载器去加载的,两者相同,因此天然能够相互转换。apache

这里总结一句(不必定对),在同时知足如下几个条件的状况下:编程

  • 前置条件一、接口 jar包 中,定义一个接口 Testtomcat

  • 前置条件二、实现 jar 包中,定义 Test 的实现类,好比 TestImpl。(可是不要在该类中包含该 接口,你说无法编译,那就把接口 jar包放到 classpath)

  • 前置条件三、接口 jar 包由 interface_classLoader 加载,实现 jar 包 由 impl_classloader 加载,其中 impl_classloader 会在本身没法加载时,委派给 interface_classLoader

则,定义在 实现jar 中的Test 接口的实现类,反射生成的对象,能够转换为 Test 类型。

猜想说完了,就是求证过程。

3、求证

一、定义接口 jar
D:\classloader_interface\ITestSample.java  

/**
 * desc:
 *
 * @author : 
 * creat_date: 2019/6/16 0016
 * creat_time: 19:28
 **/
public interface ITestSample {
}
复制代码

cmd下,执行:

D:\classloader_interface>javac ITestSample.java
D:\classloader_interface>jar cvf interface.jar ITestSample.class
已添加清单
正在添加: ITestSample.class(输入 = 103) (输出 = 86)(压缩了 16%)
复制代码

此时,便可在当前目录下,生成 名为 interface.jar 的接口jar包。

二、定义接口的实现 jar

在不一样目录下,新建了一个实现类。

D:\classloader_impl\TestSampleImpl.java

/**
 * Created by Administrator on 2019/6/25.
 */
public class TestSampleImpl implements  ITestSample{

}
复制代码

编译,打包:

D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI
mpl.java
 
D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class
已添加清单
正在添加: TestSampleImpl.class(输入 = 221) (输出 = 176)(压缩了 20%)
复制代码

请注意上面的标红行,不加编译不过。

三、测试

测试的思路是,用一个urlclassloader 去加载 interface.jar 中的 ITestSample,用另一个 URLClassLoader 去加载 impl.jar 中的 TestSampleImpl ,而后用java.lang.Class#isAssignableFrom 判断后者是否能转成前者。

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * desc:
 *
 * @author : caokunliang
 * creat_date: 2019/6/14 0014
 * creat_time: 17:04
 **/
public class MainTest {


    public static void testInterfaceByOneAndImplByAnother()throws Exception{
        URL url = new URL("file:D:\\classloader_interface\\interface.jar");
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample");


        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");


        System.out.println("实现类能转否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass));

    }

    public static void main(String[] args) throws Exception {
        testInterfaceByOneAndImplByAnother();
    }

}
复制代码

打印以下:

四、延伸测试1

若是咱们作以下改动,你猜会怎样? 这里的主要差异是:

改以前,urlClassloader 做为 parentClassloader:

URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
复制代码

改以后,不传,默认会以 jdk 的应用类加载器做为 parent:

URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});
复制代码

打印结果是:

Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:23)
    at MainTest.main(MainTest.java:33)
Caused by: java.lang.ClassNotFoundException: ITestSample
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 13 more
复制代码

结果就是,第23行,Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); 这里报错了,提示找不到 ITestSample。

这就是由于,在加载了 implUrlClassLoader 后,触发了对 ITestSample 的隐式加载,这个隐式加载会用哪一个加载器去加载呢,没有默认指明的状况下,就是用当前的类加载器,而当前类加载器就是 implUrlClassLoader ,可是这个类加载器开始加载 ITestSample,它是遵循双亲委派的,它的parent 加载器 即为 appclassloader,(jdk的默认应用类加载器),但appclassloader 根本不能加载 ITestSample,因而仍是还给 implUrlClassLoader ,可是 implUrlClassLoader 也不能加载,因而抛出异常。

五、延伸测试2

咱们再作一个改动, 改动处和上一个测试同样,只是此次,咱们传入了一个特别的类加载器,做为其 parentClassLoader。 它的特殊之处在于,almostSameUrlClassLoader 和 前面加载 interface.jar 的类加载器如出一辙,只是是一个新的实例。

URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);
复制代码

此次,看看结果吧,也许你猜到了?

此次没报错了,毕竟 almostSameUrlClassLoader 知道去哪里加载 ITestSample,可是,最后的结果显示,实现类的 class 并不能 转成 ITestSample。

六、延伸测试3

说实话,有些同窗可能对 java.lang.Class#isAssignableFrom 不是很熟悉,咱们换个你更不熟悉的,如何?

URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
        Object o = testSampleImplClass.newInstance();
        Object cast = iTestSampleClass.cast(o); // 将 o 转成 接口的那个类
        System.out.println(cast);
复制代码

结果:

若是换成下面这样,就没啥问题:

URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
        Object o = testSampleImplClass.newInstance();
        Object cast = iTestSampleClass.cast(o);
        System.out.println(cast);
复制代码

执行:

相关文章
相关标签/搜索