一言难尽,在收集了一周的资料后,今早因为CSDN的操作不熟练导致全给删了,我。。。。缓了一个礼拜,从新开始。
虽然一年多前就开始尝试阅读Tomcat源码源码,但Tomcat源码还是比较庞大的,所以对Tomcat的理解一直比较零散,没有形成体系。而最近碰到一些ClassLoader导致的问题,所以决定趁机研究下Tomcat中对ClassLoader的应用。本次研究我们按照时间线来进行讲解。
Bootstrap
类Bootstrap
作为Tomcat启动逻辑的入口,其中涉及到ClassLoader的代码主要有两处,分别是 init
和initClassLoaders
方法:
Bootstrap.init
方法
initClassLoaders(); // 重点关注这个
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
Bootstrap.initClassLoaders
方法
这里初始化完毕的URLClassLoader是遵从ClassLoader的 级委托接待机制的。
// 该方法负责给类级字段`commonLoader`, `catalinaLoader`, `sharedLoader` 赋值。
private void initClassLoaders() {
try {
// 这里的common.loader,类型为URLClassLoader
// 其parent classloader为 sun.misc.Launcher$AppClassLoader;
// 注意该ClassLoader是遵从ClassLoader的 上级委托接待机制 的
// 就是这个loader负责加载Server和Context
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
// 默认情况下 catalinaLoader, sharedLoader等于上面的commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
我们通过JVISUALVM来看看Tomcat运行时的环境变量
sun.misc.Launcher$AppClassLoader
负责加载的是 System.getProerty(“java.class.path”)
下的Class。Catalina.createStartDigester
方法conf/server.xml
)的Digester 组件。Catalina.SetParentClassLoaderRule
。 负责为Engine
设置 parentClassLoader
(该字段定义在容器基类ContainerBase
中)。HostRuleSet
中的CopyParentClassLoaderRule
。设置Host
的 parentClassLoader
(该字段定义在容器基类ContainerBase
中)。parentClassLoader
值来源的是加载Catalina的类 —— sun.misc.Launcher$AppClassLoader
。Context
其覆写了继承自ContainerBase
的getParentClassLoader
方法。digester.setUseContextClassLoader(true);
,结合上面的 Thread.currentThread().setContextClassLoader(catalinaLoader);
我们可以得出结论:Server,Context等Class都是使用上面的catalinaLoader (URLClassLoader类型,且父ClassLoader为加载Catalina
的类 —— sun.misc.Launcher$AppClassLoader
)来进行加载。StandardContext
类其覆写了继承自基类ContainerBase
的getParentClassLoader
方法。
@Override
public ClassLoader getParentClassLoader() {
// 如果自身显式定义了, 则直接返回
if (parentClassLoader != null)
return (parentClassLoader);
// 如果显式设置了privileged为true, 则返回加载自身的ClassLoader,也就是common.loader代表的URLClassLoader,注意该URLClassLoader的parent ClassLoader为sun.misc.Launcher$AppClassLoader。
if (getPrivileged()) {
return this.getClass().getClassLoader();
} else if (parent != null) { // 如果parent不为null, 在StandardContext的情况下, 这是肯定满足的。所以这里的parent为Host,因此这里的返回值也是 sun.misc.Launcher$AppClassLoader
return (parent.getParentClassLoader());
}
return (ClassLoader.getSystemClassLoader());
}
其中涉及到的是Tomcat自身的三个相关类
WebappLoader
。 实现了接口Loader
WebappClassLoaderBase
。 继承自 ClassLoader
DefaultInstanceManager
。 实现了接口InstanceManager
我们首先要看的是startInternal
方法
//
// 代码实在太多了,故进行了非必要的删减
//
// ------------------------------------ 初始化Loader
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
...
// Binding thread
// 将Loader关联的ClassLoader绑定到上下文
// 这里返回的应该是在Bootstrap.java的init()方法中绑定的catalinaLoader[默认加载路径在conf/catalina.properties的common.loader对应的值]
ClassLoader oldCCL = bindThread();
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
// since the loader just started, the webapp classloader is now created.
// By calling unbindThread and bindThread in a row, we setup the current Thread CCL to be the webapp classloader
unbindThread(oldCCL); // 将common.loader的CCL绑定回当前线程
// setup the current Thread CCL to be the webapp classloader [说得已经比较清楚了,将Loader相关的CCL再次关联当前线程]
// 返回common.loader
oldCCL = bindThread();
...
// Unbinding thread
// 再次将common.loader的CCL绑定回当前线程
unbindThread(oldCCL);
// Binding thread
oldCCL = bindThread(); // 再再一次将Loader关联的ClassLoader绑定到上下文
// ------------------------------------ 初始化InstanceManager
if (getInstanceManager() == null) {
javax.naming.Context context = null;
if (isUseNaming() && getNamingContextListener() != null) {
context = getNamingContextListener().getEnvContext();
}
Map<String, Map<String, String>> injectionMap = buildInjectionMap(
getIgnoreAnnotations() ? new NamingResources(): getNamingResources());
setInstanceManager(new DefaultInstanceManager(context,
injectionMap, this, this.getClass().getClassLoader()));
getServletContext().setAttribute(
InstanceManager.class.getName(), getInstanceManager());
}
// 回调ServletContainerInitializer相关类(@HandlesTypes标注的实现类)
//使用StandardContext的InstanceManager字段来负责实例化了Servlet,Filter,Listener
// listenerStart()
// filterStart()
// loadOnStartup(findChildren())
// 相关的解释, 我们放到InstanceManager节点中
...
// Unbinding thread
unbindThread(oldCCL); // 再再次将common.loader的CCL绑定回当前线程
...
// Close all JARs right away to avoid always opening a peak number of files on startup
if (getLoader() instanceof WebappLoader) {
((WebappLoader) getLoader()).closeJARs(true);
}
现在让我们来具体看看WebappClassLoaderBase
,WebappLoader
,InstanceManager
。
WebappLoader
类Loader
接口的定义显示其并不负责实际的Class加载工作。Lifecycle
接口, 也正是在 startInternal
方法实例化并配置WebappClassLoader
。Loader
的主要作用还是进行相关配置; 对Class进行加载的逻辑还是在 WebappClassLoader
中。WebappClassLoader
类WebappClassLoaderBase
中。loadClass
,findClass
(可以翻阅下《深入分析Java Web技术内幕(修订版)》P157)。loadClass
和findClass
方法中的代码量比较大,有兴趣的读者可以自己去研究下。 涉及到 基类WebappClassLoaderBase
中有一些字段值得关注
1. triggers
, Servlet规范里要求相关类不允许被Servlet容器加载。 这里就是相关实现处了。 而我们经常会在控制台看到的那句话:validateJarFile(xxx) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: xxx
, 正是位于其validateJarFile
方法中 - 第3373行
2. packageTriggers
在进行 委托给父classloader之前,不允许自身先加载,类的集合。
3. 等等
InstanceManager
类DefaultInstanceManager
。几个关键字段classLoader
,containerClassLoader
的赋值情况
// ------------------- DefaultInstanceManager类的构造函数
// catalinaContext实际类型为: StandardContext ,
// 所以这个classLoader应该就是WebappClassLoader
classLoader = catalinaContext.getLoader().getClassLoader();
privileged = catalinaContext.getPrivileged();
// containerClassLoader就是加载StandardContext的ClassLoader, 结合之前的digester.setUseContextClassLoader(true);
// 所以这个containerClassLoader应该就是common.loader(即URLClassLoader,其praent Classloader是sun.misc.Launcher$AppClassLoader)
this.containerClassLoader = containerClassLoader;
就是这个InstanceManager
类负责加载所有的Servlet
,Filter
,Listener
等。
StandardContext.listenerStart()
。这里可以发现使用了StandardContext
的InstanceManager
字段来负责实例化了Listener
。StandardContext.filterStart()
。这里最终可以追踪到ApplicationFilterConfig
类的构造函数, 其也是使用了StandardContext
的InstanceManager
字段来负责实例化了Filter
。loadOnStartup(findChildren())
。这里可以追踪到StandardWrapper
的loadServlet
方法, 其正是使用了StandardContext
的InstanceManager
字段来负责实例化了Servlet
。本类中负责加载Servlet,Filter,Listener的逻辑为其内部的 loadClass
方法,
// ----------------------- DefaultInstanceManager.loadClass
/*
大概逻辑是
1. 使用containerClassLoader进行加载 (这个containerClassLoader应该就是common.loader(即URLClassLoader,其praent Classloader是sun.misc.Launcher$AppClassLoader))
1. "org.apache.catalina" package内的Class.
2. ContainerServlet的实现类
2. 剩下的都使用classLoader来进行加载 (这个classLoader应该就是WebappClassLoader )
3. 再结合之前的测试, 可以得出结论: 我们自定义的类都是由WebappClassLoader来加载的.
*/
protected Class> loadClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
// 特定的 package 内
if (className.startsWith("org.apache.catalina")) {
return containerClassLoader.loadClass(className);
}
try {
Class> clazz = containerClassLoader.loadClass(className);
// 关于这个ContainerServlet, 本篇文章就不涉及了
if (ContainerServlet.class.isAssignableFrom(clazz)) {
return clazz;
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
return classLoader.loadClass(className);
}
Bootstrap
类中的类级字段catalinaLoader
,sharedLoader
, commonLoader
为同一个URLClassLoader
,而且该URLClassLoader
的Parent ClassLoader正是sun.misc.Launcher$AppClassLoader
。Server
,Context
等config/server.xml
中配置的组件的ClassLoader为catalinaLoader
字段指示的URLClassLoader。 也就是在Catalina的createStartDigester
方法中设置的digester.setUseContextClassLoader(true);
注意这里是遵从父优先的。而其parent Classloader为sun.misc.Launcher$AppClassLoader
(即:加载Catalina的类)parentClassLoader
字段,其赋值是通过digester组件来完成的,其值为加载Catalina
的ClassLoader,即sun.misc.Launcher$AppClassLoader
。Context使用InstanceManager
来完成了实例化工作。 Servlet, Filter, Listener就是借助于它来完成实例化操作的,也是使用它来对Servlet, Filter, Listener的相关Class进行加载的。
ContainerServlet
的实现类。WebappClassLoader
的parent ClassLoader由StandardContext.getParentClassLoader
来决定。实际的加载类的工作最终是交给了WebappClassLoader了,大部分工作逻辑被基类WebappClassLoaderBase来完成, 而Loader接口基本只是做了外围的配置工作。
delegate
的应用场景之一正是 WebappClassLoaderBase.loadClass
中。其直接影响了加载类的优先级。使用不同项目上的Tomcat时,PathUtils.class.getClassLoader().getResource("").getPath()
获取到的路径不一致。
经过一番探索, 发现是因为tomcat下的 conf/context.xml
文件中多了一行
// 这 delegate="true" 设置后面Web应用的类查找时是父优先还是子优先
// 其默认为false,即子优先
// 就是因为设置了true, 所以导致了取得的路径不一致
delegate="true" />