Tomcat类加载
与很多服务器应用一样,Tomcat也实现了类加载器。
在Java虚拟机类加载的基础上,Tomcat进行了稍许改动,以适应自身业务的需求。
当Tomcat启动后,它会创建一组类加载器,这些类加载器也会形成双亲委派模型中的父子关系,父类加载器在子类加载器之上:
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...
结合双亲委派模型来看,Tomcat的类加载结构,如图所示:
Tomcat在原有双亲委派模型的基础上,增加了Common类加载器、Catalina类加载器、Shared类加载,以及WebApp类加载器。
如果将两者结合起来看,那么Bootstrap对应的是Bootstrap ClassLoader和Extension ClassLoader,System对应的是Application ClassLoader,Common对应的是Common ClassLoader、Catalina ClassLoader和Shared ClassLoader,WebApp1/WebApp2..对应的是WebApp ClassLoader。
Bootstrap ClassLoader:C++编写,无具体实现类,无法通过方法获得,负责加载
Extension ClassLoader:Java程序编写,sun.misc.Launcher.ExtClassLoader实现,负责加载
Application ClassLoader:Java语言编写,sun.misc.Launcher.AppClassLoader实现,负责加载Tomcat目录下lib文件夹下的bootstrap.jar、commons-daemon.jar、tomcat-juli.jar。
Common ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的common.loader属性所指定的目录。
Catalina ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的server.loader属性所指定的目录。
Shared ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的shared.loader属性所指定的目录。
WebApp ClassLoader:Java语言编写,org.apache.catalina.loader.WebappClassLoader实现(在tomcat源码中),负责加载Tomcat目录webapps文件下中应用里的/WEB-INF/lib和/WEB-INF/class.
Tomcat目录结构中,有三组目录(/common/,/server/和shared/)可以存放公用Java类库,以及第四组Web应用程序目录/WEB-INF/:
放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
放置在/WebApp/WEB-INF目录中:类库仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
值得注意的是,在Tomcat的6.0版本之后,只有在tomcat/conf/catalina.properties文件中给server.loader属性和share.loader属性赋值后,才会去建立Catalina ClassLoader和Shared ClassLoader两个类加载器的实例,否则在用到这两个类加载器的地方都会用Common ClassLoader的实例代替(org.apache.catalina.loader.StandardClassLoader),而/common、/server和/shared目录在Tomcat6.0之后就不存在了,取而代之的是/lib目录。
Tomcat类加载源码
说完了,Tomcat类加载的结构。本小节,具体说下Tomcat的类加载机制,想要说清楚,那必须还得从Tomcat源码来入手。
找到org.apache.catalina.startup.Bootstrap类,发现该类中有main方法,没错这就是Tomcat的起点。
public static void main(String args[]) {
if (daemon == null) {
// 创建Bootstrap对象:
Bootstrap bootstrap = new Bootstrap();
try {
// bootstrap初始化
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
// command为start,代表着启动:
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null==daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
.....省略
}
}
在main方法中,我们首先创建了一个Bootstrap对象,并调用其init()方法。
在初始化结束后,判断虚拟机传入的指令,通常为“start”,开始加载、启动流程,调用了daemon.load(args)、daemon.start()方法来实现。
public void init() throws Exception {
setCatalinaHome();
setCatalinaBase();
// Tomcat类加载器初始化
initClassLoaders();
// 设置线程上下文类加载器:
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 创建Catalina对象:
Class> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
// 将Catalina对象中的 parentClassLoader属性设置为sharedLoader类加载器
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
在Bootstrap初始化方法中,有两行值得注意,(1)initClassLoaders:初始化Tomcat类加载器、(2)Thread.currentThread().setContextClassLoader(catalinaLoader):将catalinaLoader设置为线程上下文类加载器(打破了双亲委派模型);
紧接着,又通过反射的方式,创建了Catalina对象,并对Catalina对象中的parentClassLoader属性重新赋值,赋值为sharedLoader类加载器,也就是common ClassLoader。注意,此处赋值的目的是为了后续创建WebApp ClassLoader使用。
private void initClassLoaders() {
try {
// Common类加载器
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
// Catalina类加载器
catalinaLoader = createClassLoader("server", commonLoader);
// Shared类加载器
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
在初始化Tomcat类加载器方法中,我们总共创建了3大类加载器,分别为commonLoader、catalinaLoader、sharedLoader。默认情况下,在Tomcat6.0之后,commonLoader、catalinaLoader、sharedLoader对象相同,并且均为org.apache.catalina.loader.StandardClassLoader实例。
Tomcat类加载器初始化时,很明确是指出catalina ClassLoader和shared ClassLoader类加载器的父类加载器为common ClassLoader类加载器,而common ClassLoader的父类加载器传值为null,继续调用createClassLoader()方法:
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {
// 从conf/catalina.properties中获取具体的key:
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
// 将common.loader中的值,替换成服务器上的绝对路径,并以;相隔
value = replace(value);
List repositories = new ArrayList();
// 遍历value中的值:
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
...省略
// 将value中的值,组装成Repository对象,并存入到List集合中
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
在创建ClassLoader方法中,我们解析catalina.properties文件中的common.loader属性,默认值为${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar**,将默认值中的${}进行替换,最终得到了服务器上的绝对路径,例如:(catalina.base,catalina.home是环境变量中的值,可通过System.getProperty("xxx")获得)
F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar,
F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar
在得到这些绝对路径后,进行Repository对象组装,并添加到ArrayList集合中来。最后,调用ClassLoaderFactory.createClassLoader(repositories, parent)生成具体的ClassLoader实现类。
在创建common ClassLoader中:commonLoader = createClassLoader("common", null),传递父类加载器为parent = null,当实际创建类加载器对象时,会以系统类加载器(App ClassLoader)作为common ClassLoader的父类加载器。
说完了Common ClassLoader、Catalina ClassLoader、Shared ClassLoader的实例化,我们接下来在来看看WebApp ClassLoader在Tomcat中是如何初始化的!!!
之前,在创建Catalina对象时候,我们说到了对Catalina对象中的parentClassLoader属性重新赋值,是为了创建WebApp ClassLoader时使用。具体如何,我们来看下源码!!
由于Tomcat代码太多,本次不展示代码的流转过程,直接来看源码!
org.apache.catalina.loader.WebappClassLoader创建源码,存在于org.apache.catalina.loader.WebappLoader类的createClassLoader()方法中:
private WebappClassLoaderBase createClassLoader() throws Exception {
Class> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = container.getParentClassLoader();
}
Class>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
loaderClass是WebappLoader的实例变量,其值为org.apache.catalina.loader.WebappClassLoader,通过反射调用了WebappClassLoader的构造函数,然后传递了sharedLoader(也就是common ClassLoader)作为其父亲加载器。
来看下org.apache.catalina.loader.WebappClassLoader的构造器:
public WebappClassLoader(ClassLoader parent) {
super(parent);
}
由于WebappClassLoader继承org.apache.catalina.loader.WebappClassLoaderBase类,所以来看看WebappClassLoaderBase类的构造:
public WebappClassLoaderBase(ClassLoader parent) {
super(new URL[0], parent);
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.j2seClassLoader = j;
securityManager = System.getSecurityManager();
if (securityManager != null) {
refreshPolicy();
}
}
将父类加载传入,赋值给parent属性,为sharedLoader(也就是common ClassLoader)。此外,继续对j2seClassLoader属性赋值,通过遍历获取到了sun.misc.Launcher.ExtClassLoader类加载。
至此,整个Tomcat类加载器初始化流程结束。
工作中遇到的类加载问题
上面我们说了Tomcat的类加载机制。
其中,关于Tomcat的类加载器加载路径,笔者提到了跟/conf/catalina.properties文件有关。
在该文件中,可以自定义common.loader属性,修改此属性后,其他目录下的.jar包、.class文件可以通过Tomcat的common ClassLoader加载进来。
在笔者工作的环境中,Tomcat的此属性就被进行了修改,将应用中maven所有依赖的jar包都放到了common.loader所配置的目录下,这样做的结果就是:common ClassLoader将加载应用中依赖的jar包,而应用本身的WebappClassLoader失去了原本存在的意义。不过,这么做倒没什么关系,只是在代码开发时,会出现莫名的异常。
截图中,就是笔者所在公司的项目结构,使用了maven的模块化功能。
将hessian打成war包,进行Tomcat部署,hessian中如果没有servlet的话,基本上没有业务代码。
facade是对外的接口层,core是底层实现。
hessian依赖facade、core模块。
编写测试代码:
在hessian模块下创建servlet类,并调用core模块中的任意一个类。
public class TestClassLoaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.service(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.doGet(req, resp);
TestUtil.test();
}
}
在core模块中,类加载hessian模块下的创建servlet类,使用Class.forName("xxxx")即可。
public static void test(){
try{
Class clazz = Class.forName("com.jiaboyan.hessian.TestClassLoaderServlet");
logger.info("要加载的类为:",clazz);
}catch (Throwable th){
logger.error("异常",th);
}
}
在本地对hessian进行打包,结果如下:
项目中的依赖,以及core模块、facade模块都会被打到hessian.war/WEB-INF/lib下。
本地tomcat启动,可以在idea中启动,也可以放到Tomcat目录中去启动也行。
输出结果如下:
要加载的类为:class com.jiaboyan.hessian.TestClassLoaderServlet
一切都是如此的正常。
此时,你的新功能开发完了,自测完了,你需要部署到测试服务器中。利用jenkins进行部署,公司的jenkins进行了修改,在部署构建过程中,会将war包中/WEB-INF/lib下的所有jar包,拷到Tomcat/commonlib目录下,并删除/WEB-INF/lib下的所有jar包。
而我们公司的Tomcat又对/conf/catalina.properties中的common.loader进行了修改:
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,${catalina.home}/commonlib/*.jar,${catalina.home}/commoncfg
通过common.loader的配置可知,common ClassLoader会加载commonlib目录下的.jar包。
如果此时你发现了问题,那么说明你已经理解了Tomcat的类加载机制。
由此可见,对于core模块、facade模块下的代码,均有common ClassLoader进行加载,而hessian模块中的代码是由WebApp ClassLoader进行加载。并且,common ClassLoader是WebApp ClassLoader的父类加载器,父类加载无法加载子类加载器的资源,当测试服务器中代码执行时候,common ClassLoader无法加载WebApp ClassLoader所拥有的类,结果就是类加载异常。