类加载器第7弹:

实战分析Tomcat的类加载器结构(使用Eclipse MAT验证)

还是Tomcat,关于类加载器的趣味实验

了不得,我可能发现了Jar 包冲突的秘密

一、一个程序员的思考

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

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

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

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

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

再来一个,关于SPI的问题。  在 SPI 中(有兴趣的同学可以自行查询,网上很多,我随便找了一篇:https://www.jianshu.com/p/46b42f7f593c),主要是由 java 社区指定规范,比如 JDBC,厂家有那么多,mysql,oracle,postgre,大家都有自己的 jar包,要是没有 JDBC 规范,我们估计就得针对各个厂家的实现类编程了,那迁移就麻烦了,你针对 mysql 数据库写的代码,换成 oracle 的话,代码不改是肯定不能跑的。所以, JCP组织制定了 JDBC 规范,JDBC 规范中指定了一堆的 接口,我们平时开发,只需要针对接口来编程,而实现怎么办,交给各厂家呗,由厂家来实现 JDBC 规范。这里以代码举例,oracle.jdbc.OracleDriver 实现了 java.sql.Driver,同时,在 oracle.jdbc.OracleDriver 的 static 初始化块中,有下面的代码:

  1. static {
  2. try {
  3. if (defaultDriver == null) {
  4. defaultDriver = new oracle.jdbc.OracleDriver();
  5. DriverManager.registerDriver(defaultDriver);
  6. }
  7. // 省略
  8. }

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

  1. java.sql.DriverManager#registerDriver(java.sql.Driver)
  2.  
  3. public static synchronized void registerDriver(java.sql.Driver driver)
  4. throws SQLException {
  5.  
  6. registerDriver(driver, null);
  7. }

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

二、不同类加载器加载的类,可以转换的关键

经过上面两个例子的观察,不知道大家发现没, 我们都是把一个实现,转换为一个接口。也许,这就是问题的关键。我们可以大胆地推测,基于类的双亲委派机制,在 加载 实现类的时候,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 的类加载器去加载的,二者相同,所以自然可以相互转换。

这里总结一句(不一定对),在同时满足以下几个条件的情况下:

前置条件1、接口 jar包 中,定义一个接口 Test

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

前置条件3、接口 jar 包由 interface_classLoader 加载,实现 jar 包 由 impl_classloader 加载,其中 impl_classloader 会在自己无法加载时,委派给 interface_classLoader

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

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

三、求证

1、定义接口 jar

  1. D:\classloader_interface\ITestSample.java
  1. /**
  2. * desc:
  3. *
  4. * @author :
  5. * creat_date: 2019/6/16 0016
  6. * creat_time: 19:28
  7. **/
  8. public interface ITestSample {
  9. }

cmd下,执行:

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

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

2、定义接口的实现 jar

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

  1. D:\classloader_impl\TestSampleImpl.java
  2.  
  3. /**
  4. * Created by Administrator on 2019/6/25.
  5. */
  6. public class TestSampleImpl implements ITestSample{
  7.  
  8. }

编译,打包:

  1. D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI
  2. mpl.java
  3.  
  4. D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class
  5. 已添加清单
  6. 正在添加: TestSampleImpl.class(输入 = 221) (输出 = 176)(压缩了 20%)

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

3、测试

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

  1. import java.lang.reflect.Method;
  2. import java.net.URL;
  3. import java.net.URLClassLoader;
  4.  
  5. /**
  6. * desc:
  7. *
  8. * @author : caokunliang
  9. * creat_date: 2019/6/14 0014
  10. * creat_time: 17:04
  11. **/
  12. public class MainTest {
  13.  
  14. public static void testInterfaceByOneAndImplByAnother()throws Exception{
  15. URL url = new URL("file:D:\\classloader_interface\\interface.jar");
  16. URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
  17. Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample");
  18.  
  19. URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
  20. URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
  21. Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
  22.  
  23. System.out.println("实现类能转否?:" + iTestSampleClass.isAssignableFrom(testSampleImplClass));
  24.  
  25. }
  26.  
  27. public static void main(String[] args) throws Exception {
  28. testInterfaceByOneAndImplByAnother();
  29. }
  30.  
  31. }

打印如下:

4、延伸测试1

如果我们做如下改动,你猜会怎样? 这里的主要差别是:

改之前,urlClassloader 作为 parentClassloader:

  1. URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);

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

  1. URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});

打印结果是:

  1. Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample
  2. at java.lang.ClassLoader.defineClass1(Native Method)
  3. at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
  4. at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
  5. at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
  6. at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
  7. at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
  8. at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
  9. at java.security.AccessController.doPrivileged(Native Method)
  10. at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
  11. at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
  12. at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
  13. at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:)
  14. at MainTest.main(MainTest.java:33)
  15. Caused by: java.lang.ClassNotFoundException: ITestSample
  16. at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
  17. at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
  18. at java.security.AccessController.doPrivileged(Native Method)
  19. at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
  20. at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
  21. at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
  22. ... 13 more

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

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

5、延伸测试2

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

  1. URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
  2. URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);

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

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

6、延伸测试3

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

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

结果:

如果换成下面这样,就没啥问题:

  1. URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
  2. URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
  3. URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
  4. Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
  5. Object o = testSampleImplClass.newInstance();
  6. Object cast = iTestSampleClass.cast(o);
  7. System.out.println(cast);

执行:

四、总结

大家将就看吧,第三章的测试如果仔细看下来,基本就能理解了。 其实,除了 接口这种方式,貌似 继承 的方式也是可以的,改天再试验下。 这一块,不知道为啥,我是真的在网上书上没找到,但其实很重要,改天找找虚拟机层面的实现代码吧。 大家如果觉得有帮助,麻烦点个推荐,对于写作的人来说,这莫过于最大的奖励了。

参考:

深入探讨 Java 类加载器

https://blog.csdn.net/conquer0715/article/details/51283632

 

不吹不黑,关于 Java 类加载器的这一点,市面上没有任何一本图书讲到的更多相关文章

  1. java笔记--理解java类加载器以及ClassLoader类

    类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...

  2. java类加载器深入研究

    看了下面几篇关于类的加载器的文章,豁然开朗.猛击下面的地址开始看吧. Java类加载原理解析      深入探讨 Java 类加载器 分析BootstrapClassLoader/ExtClassLo ...

  3. 深入探讨 Java 类加载器

    转自:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/ 类加载器(class loader)是 Java™中的一个很重要的概念.类 ...

  4. java类加载器

    1.什么是类加载器?类加载器实现什么功能? 类加载器(Class Loader)是用来加载java类到java虚拟机(JVM)中,加载步骤: java编译器编译java源文件(*.java文件)成字节 ...

  5. java类加载器学习2——自定义类加载器和父类委托机制带来的问题

    一.自定义类加载器的一般步骤 Java的类加载器自从JDK1.2开始便引入了一条机制叫做父类委托机制.一个类需要被加载的时候,JVM先会调用他的父类加载器进行加载,父类调用父类的父类,一直到顶级类加载 ...

  6. JAVA 类加载器 第14节

    JAVA 类加载器 第14节 今天我们将类加载机制5个阶段中的第一个阶段,加载,又叫做装载.为了阅读好区分,以下都叫做装载. 装载的第一步就是要获得二进制的字节流,它可以从读.class文件获得,也可 ...

  7. 深入探讨 Java 类加载器[转]

    原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html 类加载器(class loader)是 Java™ ...

  8. 转载:深入探讨 Java 类加载器

    转载地址 : http://www.ibm.com/developerworks/cn/java/j-lo-classloader/ 深入探讨 Java 类加载器 类加载器(class loader) ...

  9. Java类加载器详解

    title: Java类加载器详解date: 2015-10-20 18:16:52tags: JVM--- ## JVM三种类型的类加载器- 我们首先看一下JVM预定义的三种类型类加载器,当一个 J ...

随机推荐

  1. ReactiveCocoa概念解释进阶篇

    1.ReactiveCocoa常见操作方法介绍 1.1 ReactiveCocoa操作须知 所有的信号(RACSignal)都可以进行操作处理,因为所有操作方法都定义在RACStream.h中,因此只 ...

  2. C# 调用腾讯地图WebService API获取距离(一对多)

    官方文档地址:https://lbs.qq.com/webservice_v1/guide-distance.html 代码: /// <summary> /// 获取距离最近的点的经纬度 ...

  3. webpack开始一个项目的步骤

    这几天在学习Vue  用到了webpack打包工具  开始一个项目的时候  需要配置很多项  刚开始写的时候  配置文件总是缺什么再去配置什么  创建项目就用了半个小时  后来觉得应该有个步骤  这样 ...

  4. Flask-基本原理与核心知识

    虚拟环境 使用pipenv创建一个虚拟环境和项目绑定,安装:E:\py\qiyue\flask>python3 -m pip install pipenv 和项目绑定:到项目的目录中pipenv ...

  5. Node项目实战-静态资源服务器

    打开github,在github上创建新项目: Repository name: anydoor Descripotion: Tiny NodeJS Static Web server 选择:publ ...

  6. CentOS 系统下Gitlab搭建与基本配置 以及代码备份迁移过程

    GitLab 是一个开源的版本管理系统,提供了类似于 GitHub 的源代码浏览,管理缺陷和注释等功能,你可以将代码免费托管到 GitLab.com,而且不限项目数量和成员数.最吸引人的一点是,可以在 ...

  7. laravel中redis各方法的使用

    在laravel中使用redis自带方法的时候会发现许多原生的方法都不存在了,laravel对其进行了重新的封装但是在文档中并没有找到相关的资料最后在 \vendor\predis\predis\sr ...

  8. 反爬虫之搭建IP代理池

    反爬虫之搭建IP代理池 听说你又被封 ip 了,你要学会伪装好自己,这次说说伪装你的头部.可惜加了header请求头,加了cookie 还是被限制爬取了.这时就得祭出IP代理池!!! 下面就是requ ...

  9. shell中变量字符串的截取 与 带颜色字体、背景输出

    字符串截取 假设我们定义了一个变量为:file=/dir1/dir2/dir3/my.file.txt 可以用${ }分别替换得到不同的值:${file#*/}:删掉第一个 /及其左边的字符串:dir ...

  10. Centos7 install Openstack Juno (RDO) (转载)

    原文地址:http://www.hdume.com/centos-7-0%E5%AE%89%E8%A3%85openstack/ 1.安装系统,Centos7镜像采用CentOS-7.0-1406-x ...