读tomcat的源码的时候,我建议和官方的User Guide一起阅读,明白tomcat做某件事情的目的之后,看源码会容易一点。另外,debug当然是一个非常重要的工具。源码上了规模之后,如果单纯静态地看,基本是看不出什么来的,但是跟着数据流走一走,很多问题就清楚了 debug环境的搭建方法,请看另外一篇博客:http://zhh2009.iteye.com/blog/1557891。这篇文章写得很清楚了,但是我不太明白为什么需要转换成maven工程,以及为什么需要一个dist版本 作为本系列的第一篇文章,本文不涉及源码,首先介绍一下tomcat的classloader机制 参考的文档包括: http://tomcat.apache.org/tomcat-7.0-doc/class-loader-howto.html servlet-spec-2.4-fr 一、过时的模型 在网上搜索“tomcat classloader”,很容易搜索到下图,但是这是一个过时的模型 这个模型是在tomcat5.x使用的,可以看一下tomcat5.x的目录结构 再对比一下tomcat7.x的目录结构 可以看到,5.x里的server、shared、common目录,在7.x中已经废弃了。所以上图中的ClassLoader模型也是过时的 在tomcat7.x里,ClassLoader的模型应该是下图这样: 至于这个模型中,各个ClassLoader的具体作用,下文会说明 二、JVM默认的classloader机制 jvm默认定义了三种classloader,分别是bootstrap classloader、extension classloader、system classloader bootstrap是jvm的一部分,用C写的,每一个java程序都会启动它,去加载%JAVA_HOME%/jre/lib/rt.jar extension也差不多,它会去加载%JAVA_HOME%/jre/lib/ext/下的类 system则是会去加载系统变量CLASSPATH下的所有类 这3个部分,在上面的tomcat classloader模型图中都有体现。不过可以看到extension没有画出来,可以理解为是跟bootstrap合并了,都是去%JAVA_HOME%/jre/lib下面加载类 另外,java的classloader一般是采用委托机制,即classloader都有一个parent classloader,当它收到一个加载类的请求时,会首先请求parent classloader加载,如果parent classloader加载不到,才会自己去尝试加载(如果自己也加载不到,则抛出ClassNotFoundException)。采用这种机制的目的,主要是从安全角度考虑。比如用户自己定义了一个java.lang.Object,把jdk中的覆盖了,那显然是有问题的 当然,这个机制不是绝对的,比如在OSGi中,就故意违反了这个模式。后面可以看到,tomcat里的webapp classloader也违反了这个规定 三、tomcat为什么要自定义classloader 主要有2个目的,首先是要实现servlet规范中对类加载的要求,其次是实现不同web app的类隔离 servlet规范中对类加载要求如下: This specification defines a hierarchical structure used for deployment and packaging purposes that can exist in an open file system, in an archive file, or in some other form. It is recommended, but not required, that servlet containers support this structure as a runtime representation. Web applications can be packaged and signed into a Web ARchive format (WAR) file using the standard Java archive tools. For example, an application for issue tracking might be distributed in an archive file called issuetrack.war. When packaged into such a form, a META-INF directory will be present which contains information useful to Java archive tools. This directory must not be directly served as content by the container in response to a Web client’s request, though its contents are visible to servlet code via the getResource and getResourceAsStream calls on the ServletContext. Also, any requests to access the resources in META-INF directory must be returned with a SC_NOT_FOUND(404) response. 四、各classloader详细说明 4.1 Bootstrap — This class loader contains the basic runtime classes provided by the Java Virtual Machine, plus any classes from JAR files present in the System Extensions directory ($JAVA_HOME/jre/lib/ext). Note: some JVMs may implement this as more than one class loader, or it may not be visible (as a class loader) at all. 4.2 System — 这个classloader通常是由CLASSPATH这个环境变量初始化的,通过这个classloader加载的所有类,都对tomcat自身的类,以及所有web应用的类可见。但是,标准的tomcat启动脚本($CATALINA_HOME/bin/catalina.bat),完全无视默认的CLASSPATH环境变量,而是加载了以下3个.jar $CATALINA_HOME/bin/bootstrap.jar — Contains the main() method that is used to initialize the Tomcat server, and the class loader implementation classes it depends on. $CATALINA_HOME/bin/tomcat-juli.jar — Logging implementation classes. These include enhancement classes to java.util.logging API, known as Tomcat JULI, and a package-renamed copy of Apache Commons Logging library used internally by Tomcat. $CATALINA_HOME/bin/commons-daemon.jar — The classes from Apache Commons Daemon project.(这个类不是直接在$CATALINA_HOME/bin/catalina.bat里加进来的,不过在bootstrap.jar的manifest文件中包含进来了) 4.3 Common — 这个classloader加载的类,对tomcat的类和web app的类都是可见的。通常来说,应用程序的类不应该放在这里。该加载器的加载路径是在$CATALINA_BASE/conf/catalina.properties文件里,通过common.loader属性来定义的,默认是:
Text代码
By default, this includes the following: annotations-api.jar — JavaEE annotations classes. catalina.jar — Implementation of the Catalina servlet container portion of Tomcat. catalina-ant.jar — Tomcat Catalina Ant tasks. catalina-ha.jar — High availability package. catalina-tribes.jar — Group communication package. ecj-*.jar — Eclipse JDT Java compiler. el-api.jar — EL 2.2 API. jasper.jar — Tomcat Jasper JSP Compiler and Runtime. jasper-el.jar — Tomcat Jasper EL implementation. jsp-api.jar — JSP 2.2 API. servlet-api.jar — Servlet 3.0 API. tomcat-api.jar — Several interfaces defined by Tomcat. tomcat-coyote.jar — Tomcat connectors and utility classes. tomcat-dbcp.jar — Database connection pool implementation based on package-renamed copy of Apache Commons Pool and Apache Commons DBCP. tomcat-i18n-**.jar — Optional JARs containing resource bundles for other languages. As default bundles are also included in each individual JAR, they can be safely removed if no internationalization of messages is needed. tomcat-jdbc.jar — An alternative database connection pool implementation, known as Tomcat JDBC pool. See documentation for more details. tomcat-util.jar — Common classes used by various components of Apache Tomcat. 4.4 WebappX — 该classloader加载所有WEB-INF/classes里的类,以及WEB-INF/lib里的jar 该classloader就有意违反了上述的委托模型,它首先看WEB-INF/classes和WEB-INF/lib里是否有请求的类,而不是委托parent classloader去加载。但是,JRE里定义的类不能被覆盖(比如java.lang.String),以及Servlet API会明确地被忽略。 前面说的bootstrap、system、common,都遵循普通的委托模型 4.5 总的来说,从web app的角度来看,类或者资源加载是按照以下的顺序来查找的: Bootstrap classes of your JVM(rt.jar) System class loader classes(bootstrap.jar、tomcat-juli.jar、commons-deamon.jar) /WEB-INF/classes of your web application /WEB-INF/lib/*.jar of your web application Common class loader classes (在$CATALINA_HOME/lib里的jar包)
一、工具准备
tomcat的ClassLoader模型如上图,主要是为了满足servlet规范中类隔离的要求(见JSR154的Section9.4、9.6、9.7) 1.1 Bootstrap 这个类加载器和普通的JAVA应用一样,都是由JVM启动的,加载%JAVA_HOME%/jre/lib下的JAR包,如rt.jar等 通常情况下,Bootstrap和Extension是分开考虑的,但是在tomcat的ClassLoader体系里,没有将二者区分开。当谈到Bootstrap时,就包括了Bootstrap和Extension 1.2 System System类加载器,也叫App类加载器,一般就是启动应用程序的那个加载器,是根据classpath创建的 但是在tomcat里,完全忽略了默认的classpath,而是根据指定的参数,直接创建System类加载器,默认情况下,会加载%CATALINA_HOME%/bin目录下的bootstrap.jar、tomcat-juli.jar、commons-daemon.jar 这些jar包充当了启动入口的角色,但是tomcat真正的核心实现类,不是在这个ClassLoader里加载的,所以后面会提到,源码里调用核心实现类(Catalina等)的方法,必须指定ClassLoader,通过反射完成 1.3 Common 这个类加载器是tomcat特有的,对于所有web app可见。这个类加载器默认会加载%CATALINA_HOME%/lib下的所有jar包,这些都是tomcat的核心 1.4 WebappX 对于部署在容器中的每一个webapp,都有一个独立的ClassLoader,在这里实现了不同应用的类隔离 这里的ClassLoader与标准的ClassLoader委托模型不同,当需要加载一个类的时候,首先是委托Bootstrap和System;然后尝试自行加载;最后才会委托Common 加载顺序如下: Bootstrap -> System -> WEB-INF/classes -> WEB-INF/lib -> Common 2、疑问 Bootstrap.class和Catalina.class是打在不同的JAR包里的。前者在bootstrap.jar里,后者在catalina.jar里 并且这2个jar包,是由不同的ClassLoader加载的。前者由System ClassLoader加载,后者由Common ClassLoader加载 这造成代码比较麻烦,Bootstrap.class要引用Catalina.class的时候,不是直接引用,而是通过反射实现 我还没搞清楚tomcat这样设计的原因,跟类隔离貌似没有直接关系 3、tomcat启动 3.1 执行脚本 tomcat启动是从运行startup.bat脚本开始的,在此脚本中首先会设置一系列环境变量,然后配置参数,最后实际上执行的catalina.bat脚本。所以用catalina.bat start命令,也可以启动tomcat 3.2 加载Bootstrap类 在catalina.bat中,会将classpath设置为%CATALINA_HOME%/bin/bootstrap.jar和%CATALINA_HOME%/bin/tomcat-juli.jar,然后根据此classpath创建System类加载器,加载bootstrap.jar中的Bootstrap.class,执行main()方法 3.3 调用Catalina类 在Bootstrap.class里,读取配置文件(%CATALINA_HOME%/conf/catalina.properties),然后创建Common ClassLoader,加载%CATALINA_HOME%/lib里的所有jar包 之后根据实际的命令行参数,调用org.apache.catalina.startup.Catalina中相应的方法,比如start()等 4、参考文档 http://tomcat.apache.org/tomcat-7.0-doc/class-loader-howto.html
前几天想了一下,最近主要学习linux和httpd,所以tomcat源码阅读先放一放,可能到9月份左右再继续。不过先把已经写好的几篇陆续贴上来
Java代码
自定义ClassLoader的时候,一般来说,需要做的并不是覆盖loadClass()方法,这样的话就“破坏”了双亲委派模型;需要做的只是实现findClass()方法即可 不过,从上面的代码也可以看出,双亲委派模型只是一种“建议”,并没有强制保障的措施。如果自定义的ClassLoader无视此规定,直接自行加载,不将请求委托给parent,当然也是没问题的 在实际情况中,双亲委派模型被“破坏”也是很常见的。比如在tomcat里,webappx classloader就不会委托给上层的common classloader,而是先委托给system,然后自己加载,最后才委托给common;再比如说在OSGi里,更是有意完全打破了这个规则 当然,对于普通的JAVA应用开发来说,需要自定义classloader的场景本来就不多,需要去违反双亲委派模型的场景,更是少之又少 3 自定义ClassLoader 3.1 自定义ClassLoader的一般做法 从上面的代码可以看到,自定义ClassLoader很简单,只要继承抽象类ClassLoader,再实现findClass()方法就可以了 3.2 自定义ClassLoader的场景 事实上,需要实现新的ClassLoader的场景是很少的 注意:需要增加一个自定义ClassLoader的场景很多;但是,需要自己实现一个新的ClassLoader子类的场景不多。这是两回事,不可混淆 比如,即使在tomcat里,也没有自行实现新的ClassLoader子类,只是创建了URLClassLoader的实例,作为custom classloader 3.3 ClassLoader的子类 在JDK中已经提供了若干ClassLoader的子类,在需要的时候,可以直接创建实例并使用。其中最常用的是URLClassLoader,用于读取一个URL下的资源,从中加载Class
Java代码
可以看到,tomcat就是在URLClassLoader的基础上,包装了StandardClassLoader,实际上并没有任何功能上的区别 3.4 设置parent 在抽象类ClassLoader中定义了一个parent字段,保存的是父加载器。但是这个字段是private的,并且没有setter方法 这就意味着只能在构造方法中,一次性地设置parent classloader。如果没有设置的话,则会默认将system classloader设置为parent,这也是在ClassLoader类中确定的:
Java代码
4 ClassLoader隐性传递 “隐性传递”这个词是我乱造的,在网上和注释中没有找到合适的描述的词 试想这样一种场景:在应用中需要加载100个类,其中70个在classpath下,默认由system来加载,这部分不需要额外处理;另外30个类,由自定义classloader加载,比如在tomcat里:
Java代码
如上,org.apache.catalina.startup.Catalina是由自定义类加载器加载的,需要额外编程来处理(如果是system加载的,直接new就可以了) 如果30个类,都要通过这种方式来加载,就太麻烦了。不过classloader有一个特性,就是“隐性传递”,即: 如果一个ClassA是由某个ClassLoader加载的,那么ClassA中依赖的需要加载的类,默认也会由同一个ClassLoader加载 这个机制是由JVM保证的,对于程序员来说是透明的 5 current classloader 5.1 定义 与前面说的extension、system等不同,在运行时并不存在一个实际的“current classloader”,只是一个抽象的概念。指的是一个类“当前的”类加载器。一个对象实例所属的Class,是由哪一个ClassLoader加载的,这个ClassLoader就是这个对象实例的current classloader 获得的方法是:
Java代码
5.2 实例 current classloader概念的意义,主要在于它会影响Class.forName()方法的表现,贴一段代码进行说明:
Java代码
这个类调用了Class.forName()方法,试图加载net.kyfxbl.test.cl.Target类(Target是一个空类,作为加载目标,不重要)。这个类在运行时能否加载Target成功,取决于它的current classloader,能不能加载到Target 首先,将Test和Target打成jar包,放到classpath之外,jar包中内容如下: 然后在工程中删除Target类(classpath中加载不到Target了) 在Main中用system 加载Test,此时Test的current classloader是system,加载Target类失败
Java代码
然后,这次用自定义的classloader来加载
Java代码
在想象中,这次Test的current classloader应该变成URLClassLoader,并且加载Target成功。但是还是失败了 这是因为前面说过的“双亲委派模型”,URLClassLoader的parent是system classloader,由于工程里的Test类没有删除,所以classpath里还是能找到Test类,所以Test类的current classloader依然是system classloader,和第一次一样 接下来把工程里的Test类也删除,这次就成功了 5.3 Class.forName() 前面说的是单个参数的forName()方法,默认使用current ClassLoader 除此之外,Class类还定义了3个参数的forName()方法,方法签名如下:
Java代码
这个方法的最后一个参数,可以传递一个ClassLoader,会用这个ClassLoader进行加载。这个方法很重要 比如像JNDI,主体类是在JDK包里,由bootstrap加载。而SPI的实现类,则是由厂商提供,一般在classpath里。那么在JNDI的主体类里,要加载SPI的实现类,直接用Class.forName()方法肯定是不行的,这时候就要用到3个参数的Class.forName()方法了 6 ContextClassLoader 6.1 获取ClassLoader的API 前面说过,已经有2种方式可以获取到ClassLoader的引用 一种是ClassLoader.getSystemClassLoader(),获取的是system classloader 另一种是getClass().getClassLoader(),获取的是current classloader 这2种API都只能获取classloader,没有办法用来传递 6.2 传递ClassLoader 每一个thread,都有一个contextClassLoader,并且有getter和setter方法,用来在线程之间传递ClassLoader 有2条默认的规则: 首先,contextClassLoader默认是继承的,在父线程中创建子线程,那么子线程会继承父线程的contextClassLoader 其次,主线程,也就是执行main()方法的那个线程,默认的contextClassLoader是system classloader 6.3 例子 对上面例子中的Test和Main稍微改一下(Test和Target依然打到jar包里,然后从工程中删除,避免被system classloader加载到)
Java代码
Java代码
这次的tryForName()方法在一个子线程中被调用,并依次打印出current classloader和contextClassLoader,如图: 可以看到,子线程继承了父线程的contextClassLoader 同时可以注意到,contextClassLoader对Class.forName()方法没有影响,contextClassLoader只是起到在线程之间传递ClassLoader的作用 6.4 题外话 从这个例子还可以看出,一个方法在运行时的表现,在编译期是无法确定的 在运行时的表现,有时候取决于方法所在的类是被哪个ClassLoader加载;有时候取决于是运行在单线程环境下,还是多线程环境下 这在编译期是不可知的,所以在编程的时候,要考虑运行时的情况。比如所谓“线程安全”的类,并不是说它“一定”会运行在多线程环境下,而是说它“可以”运行在多线程环境下 7 总结 本文大致总结了ClassLoader的背景知识。掌握了背景,再阅读tomcat的源码,基本就不会遇到ClassLoader方面的困难 本文介绍了ClassLoader的标准体系、双亲委派模型、自定义ClassLoader的方法、以及current classloader和contextClassLoader的概念 其中最重要的是current classloader和contextClassLoader 用于获取ClassLoader的API主要有3种: ClassLoader.getSystemClassLoader(); Class.getClassLoader(); Thread.getContextClassLoader(); 第一个是静态方法,返回的永远是system classloader,也就是说,没什么用 后面2个都是实例方法,一个是返回实例所属的类的ClassLoader;另一个返回当前线程的contextClassLoader,具体的结果都要在运行时才能确定 其中,contextClassLoader可以起到传递ClassLoader的作用,所以特别重要 (责任编辑:IT) |