简介
这里是tomcat源码阅读系列的第二篇文章,目标是读懂并介绍tomcat如何打破java默认的双亲委派类加载模型~
前情提要
关于tomcat源码的基本信息可以看我的上篇文章;
这里实践验证一下tomcat打破双亲委派的必要性[1]:
我们在同一个tomcat中运行两个命名完全相同的webapp,将他们的context分别设置为根和
/2/
;
- 如果采用传统双亲委派,这两个webapp中的
HelloWorldServlet
类不能同时存在 - 预期结果是他们同时存在,访问两个contex可以定位到两个类
实验结果:
验证成功,说明tomcat使用的确实不是传统的双亲委派断点调试
web类装载(打破委派)
如果要实现这个功能肯定是要自定义Classloader的,所以直接定位到
org.apache.catalina.loader
包,在WebappClassLoaderBase
的findClass
方法上打一个断点调试
从方法栈中可以看出:
-
在
StandarContext
的startInternal
方法中发射[2]了一个event,激活了ContextConfig中的监听逻辑;ContextConfig context组件的启动事件侦听器,配置该context的属性,以及关联的定义的servlet。
-
监听发现是一个
Lifecycle.CONFIGURE_START_EVENT
,会执行非常复杂的逻辑(几乎两百行代码的一个方法),和我们的研究目标有关的大概有两部分-
从webxml中读取servlet并标记为context的child(还未加载)
private void configureContext(WebXml webxml) { //省略大量无关代码………… for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); //wrapper是servlet中函数的包装,将wrapper放入context的child数组 context.addChild(wrapper); } //…………
-
调用
loadApplicationAnnotations(Context context)
加载context的child
从方法名看出,context的child就是javaWeb三大件。
-
-
在步骤2的
loadApplicationAnnotations(Context context)
方法中会使用所属context的ClassLoader.loadClass()加载servlet类;这里context的默认加载器是ParallelWebappClassLoader,且不同context的loader不同
其loadClass方法相较于传统有所不同,步骤如下:- 加载过程会首先尝试使用缓存查看类是否被加载过,最上层缓存是
WebappClassLoader
中的一个简单的ConcurrentHashMap,key是class文件的路径;第二层缓存是jvm中ClassLoader中自带的[3] - 确认未命中缓存后,尝试使用javase默认的classloader加载这个类,目的是防止打破双亲委派后出现webapp应用覆盖javase应用的风险
这里使用了getResource方法来加载,因为他不会触发昂贵的ClassNotFoundException[4]
- 如果这个类是合法的就进入正式加载过程;首先判断是否需要走传统的委派模式[5]
重头戏来了这里就是打破双亲委派的重点,也就是每个context自己直接加载这个Class而不是委派给双亲加载器[6],从而使得包名重复的类可以同时加载boolean delegateLoad = delegate || filter(name, true); //filter()会根据name对该类是否属于webapp进行判断
- 调用
findClass()
查找class文件[7] - 调用
Class.forName()
正式加载;
- 加载过程会首先尝试使用缓存查看类是否被加载过,最上层缓存是
-
至此,两个servlet就无冲突的加载成功了,web可以正常运行
总结
虽然文章看起来很长,实际上核心逻辑就是tomcat自定义了一个classLoader(WebappClassLoader),并重写了他的loadClass方法,在其中不再将servlet类进行委派而是直接加载;而不同的context持有不同的webClassLoader实例,所以即使不同context中有相同包名的类也不会相互影响。
陆续写了小一周,源码读的还不是很熟练~
(当然也是事太多
总摸鱼,只能断断续续写)