关于 Java 类加载器的这一点,市面上没有任何一本图书讲到
一、一个程序员的思考
大家都知道,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:
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 吗?说真的,我翻了不少书,从来没提到这个,就连网上也很含糊。
再来一个,关于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 初始化块中,有下面的代码:
static { try { if (defaultDriver == null) { defaultDriver = new oracle.jdbc.OracleDriver(); DriverManager.registerDriver(defaultDriver); } // 省略 }
其中,标红这句,就是 Oracle Driver 要向 JDBC 接口注册自己,java.sql.DriverManager#registerDriver(java.sql.Driver)的实现如下:
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?
二、不同类加载器加载的类,可以转换的关键
经过上面两个例子的观察,不知道大家发现没, 我们都是把一个实现,转换为一个接口。也许,这就是问题的关键。我们可以大胆地推测,基于类的双亲委派机制,在 加载 实现类的时候,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
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包。
2、定义接口的实现 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%)
请注意上面的标红行,不加编译不过。
3、测试
测试的思路是,用一个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(); } }
打印如下:
4、延伸测试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 573 ,它是遵循双亲委派的,它的parent 加载器 即为 appclassloader,(jdk的默认应用类加载器),但appclassloader 根本不能加载 ITestSample,于是还是还给 implUrlClassLoader ,但是 implUrlClassLoader 也不能加载,于是抛出异常。
5、延伸测试2
我们再做一个改动, 改动处和上一个测试一样,只是这次,我们传入了一个特别的类加载器,作为其 parentClassLoader。 它的特殊之处在于,almostSameUrlClassLoader 和 前面加载 interface.jar 的类加载器一模一样,只是是一个新的实例。
URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);
这次,看看结果吧,也许你猜到了?
这次没报错了,毕竟 almostSameUrlClassLoader 知道去哪里加载 ITestSample,但是,最后的结果显示,实现类的 class 并不能 转成 ITestSamp 281d le。
6、延伸测试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);
执行:
- 关于JAVA的内省JavaBean、类加载器、类加载器的委托机制以及代理
- 在任何一个java工程下(即便是j2ee工程)都可以直接对某一个java文件run as Java Application,但是为什么有的java文件右击,没有“Java Application”
- java中使用堆外内存,关于内存回收需要注意的事和没有解决的遗留问题(等大神解答)
- Java —— 关于图书馆图书存放的操作小程序
- 关于Eclipse安装插件后没有任何反应的解决
- Java(其实是计算机系统的通病,而不单单是Java的问题,C、C++等任何语言都有这个问题)关于小数的运算结果,不正确不精确,原因剖析,及解决办法
- Java中,既然double类型比float类型,表示的数值范围更大,大家都用double类型不就行了,Java还发明float类型干什么?因为占内存少于double,其他没有任何优势了
- 关于Java小项目——图书管理系统的总结
- 关于JAVA 类加载器的研究
- 关于java中成员变量有默认初始化而局部变量没有
- 关于javaweb连接数据库无任何报错信息的问题
- 《Java程序员面试笔试宝典》之为什么Java中有些接口没有任何方法
- Java程序突然退出,没有任何提示!
- 关于C#更新Access数据库时发现的一个没有任何技术含量的问题,给大家分享,希望能给大家帮助
- Android开发:关于导入ApiDemos后没有R.java文件的问题
- 关于Eclipse安装hadoop插件后没有任何反应的解决
- 一个关于“OLE DB 提供程序 'sqloledb' 指出该对象中没有任何列”错误的解决方法
- 一个关于javaweb项目名处出现红叉,而项目下的目录和文件没有红叉的问题
- 学习Java的第一步是安装好JDK,写一个Hello World, 其实JDK的学习没有那么简单,关于JDK有两个问题是很容易一直困扰Java程序员的地方:一个是CLASSPATH的问题,其实从原理上来说,是要搞清楚JRE的ClassLoader是如何加
- 关于 sql中 group by 后跟主键是没有任何意义的