类加载器第 7 弹:
实战分析 Tomcat 的类加载器结构 (使用 Eclipse MAT 验证) 还是 Tomcat, 关于类加载器的趣味实验了不得, 我可能发现了 Jar 包冲突的秘密重写类加载器, 实现简单的热替换 @Java web 程序员, 我们一起给程序开个后门吧: 让你在保留现场, 服务不重启的情况下, 执行我们的调试代码 @Java Web 程序员, 在保留现场, 服务不重启的情况下, 执行我们的调试代码(JSP 方式)
一, 一个程序员的思考
大家都知道, 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 中(有兴趣的同学可以自行查询, 网上很多, 我随便找了一篇: 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 初始化块中, 有下面的代码:
- 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, 它是遵循双亲委派的, 它的 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 并不能 转成 ITestSample.
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);
执行:
四, 总结
大家将就看吧, 第三章的测试如果仔细看下来, 基本就能理解了. 其实, 除了 接口这种方式, 貌似 继承 的方式也是可以的, 改天再试验下. 这一块, 不知道为啥, 我是真的在网上书上没找到, 但其实很重要, 改天找找虚拟机层面的实现代码吧. 大家如果觉得有帮助, 麻烦点个推荐, 对于写作的人来说, 这莫过于最大的奖励了.
来源: https://www.cnblogs.com/grey-wolf/p/11084379.html