Tomcat|Tomcat源码分析-类加载器
Tomcat自定义类加载器在其体系中起着举足轻重的作用,了解类加载器这块内容是很有意义的。
比如目前我所在公司erp产品定制了自己的类加载器,实现了通过扩展的方式进行二次开发等。
Tomcat针对不同场景也定制了自己的类加载器,下面是我对自定义类加载器在tomcat中是如何应用的一些思考。
1、java是如何实现"双亲委派模型"的?这个模型的特点是什么?理解这个模型的意义是什么?
2、Tomcat有哪些类加载器?分别在何时创建的?其用途?以及如何实现的?
3、java的类加载器是如何与启动Tomcat类加载器做连接的?需要注意哪些点?
4、为什么要设置上下文类加载器?其核心起了什么作用?Tomcat是如何使用的?
5、如何查看运行期对象是被哪个类加载器加载的,以及对应的路径?
【Tomcat|Tomcat源码分析-类加载器】
首先来回答第一个问题
1、java是如何实现"双亲委派模型"的?这个模型的特点是什么?理解这个模型的意义是什么?
双亲委派模型如下
文章图片
java设计了一个抽象类加载器ClassLoader,在这个抽象类中维护了一个指向parent的ClassLoader,是通过这种组合关系实现“双亲”的,代码逻辑如下:
文章图片
在HotSpot虚拟机中,提供了一个启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机的一部分。另外就是所有其他类加载器,这些类加载器是java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。 在Java中另外两个常被提到类加载器是扩展类加载器(Extension ClassLoader)与应用程序类加载器(Application ClassLoader),这两个类加载器是在初始化sun.misc.Launcher时进行实例化上面两个类加载器并设置关系的,代码如下:
文章图片
Launcher完整源码地址:
http://www.docjar.com/html/api/sun/misc/Launcher.java.html
从源码注释可知,这是用于启动程序入口(即main方法)的类
从这份源码中,我们可以清楚知道扩展类加载器以及应用程序类加载器是如何创建的以及获取类路径的,从创建的角度证明扩展类加载器是应用程序类加载器的“父类”;结构上这两个类加载器都是直接继承了URLClassLoader,与我们自定义类加载器在继承体系上并无差异,这两个类加载器在加载类时,实则都是通过"父类"java.lang.ClassLoader的loadClass进行的,上面是对双亲委派模型实现的理解。
这个模型的特点是什么呢?
(1)、单向的自下而上,这个特点决定了基点越低,查找范围越广;也是分支与分支之间具有隔离性的基础。
(2)、主干与分支特点。 (3)、不同分支之间具有隔离性,而好多场景需要这个特点支持,如不同应用可以用同一个jar的不同版本这种场景。 这种设计是有很多优势的,既能保证核心代码的安全,又可以方便扩展,灵活,支持的场景多,具体就不在此赘述了,深入理解Java虚拟机和网上一些文章都有描述。 模型是现实场景的抽象,理解了这个模型,首先对这个模型对应各种场景有了更深入的理解,其次对代码的实现有更清晰的指导,而理解这块代码后会加深对这模型的理解,对实现自定义类加载器是有大大的好处的。
在来回答第二个问题
2、Tomcat有哪些类加载器?分别在何时创建的?如何实现的?以及其用途?
首先奉上Tomcat类加载器结构图,如下
Tomcat类加载器总的来讲可以分为三种。
第一种可以理解为"Tomcat系统类加载器",指加载实现Tomcat程序jar对应的类加载器,分别称之为CommonClassLoader,CatalinaClassLoader,SharedClassLoader,这三个对象对应的类实则同一个,只是根据common.loader,server.loader,shared.loader这三个类路径信息实例化了三个类加载器,默认情况下server.loader,shared.loader的类路径是未指定的,实则只是实例化了一个类加载器,这种情况下CatalinaClassLoader与SharedClassLoader都只是CommonClassLoader实例化对象的一个引用。这个过程是在Tomcat的引导类org.apache.catalina.startup.Bootstrap初始化时创建的,代码现如下:
在Tomcat7.0.57版本中是定义的org.apache.catalina.loader.StandardClassLoader实例化Tomcat系统类加载器,而有些其它的版本,则是直接使用java.net.URLClassLoader实例化Tomcat系统类加载器,具体实例化是在org.apache.catalina.startup.ClassLoaderFactory类中的createClassLoader方法中进行的。
对于系统类加载器catalinaLoader加载的是实现Tomcat软件本身相关的jar,在没有定制tomcat情况下,该类加载器实则是委托其父加载器commonLoader加载全部jar的;实现tomcat软件jar是放在tomcat安装目录/lib目录下的,commonLoader类加载的类路径正是此处,而tomcat使用catalinaLoader去加载而非直接commonLoader去加载原因也是很明显的,在设计时把catalinaLoader定位成加载tomcat软件jar相关的类加载器,因此如我们基于tomcat源码进行一些扩展定制后,只要配置server.loader的类路径信息,并把这些扩展定制的类打成jar包放到此目录中即可。
系统类加载器sharedLoader的父类加载器也是commonLoader,因此catalinaLoader与sharedLoader实则是在不同分支上,两者具有隔离性;而sharedLoader的定位是加载不同应用公共的jar,因此sharedLoader是tomcat各应用类加载器(WebappClassLoader)的父加载器,当不同应用用到相同的jar,并且想只需加载一份时,只需要把这个jar放到shared.loader指定的类路径即可,这样不仅节约了磁盘的资源,更重要的是节省了JVM中的堆栈等资源,并且这样设计实则把tomcat本身的资源与应用公共资源做了一层隔离,不用把应用公共的jar放到Tomcat安装目录\lib下,这样设计职责更加分明。
Bootstrap相当于tomcat启动引导类,Tomcat真正进行初始化与启动服务器的操作的类是org.apache.catalina.startup.Catalina,当然这样设计的原因也是非常充分的,在后面在进行详细赘述,用catalinaLoader显示loadClass Catalina后,会把sharedLoader设置到Catalina的一个ClassLoader类型的成员变量中进行维护,并最终在创建应用类加载器时把其设置成parent父类加载器。
第二种是Tomcat应用类加载器,是用来加载部署在tomcat中的应用的。Tomcat在设计容器对象时,就设计了一个应用加载器,而应用加载器则又会关联一个应用类加载器,应用加载器是Tomcat里面的一个组件,存在生命周期的管理,而应用类加载器则更像是这个组件里面的一个核心部件,在应用(StandardContext)启动的过程中(startInternal)会首先判断有无应用加载器,没有则创建一个,之后在启动,在启动的过程中创建应用类加载器,核心代码如下:
查看StandardContext启动代码可知,中间进行了几次bindThread和unBindThread,bindThread主要是把WebappClassLoader设置成上下文类加载器,因为应用加载器启动创建完应用类加载器后,会触发配置事件,进而解析应用下的web.xml,从而可能会实例化应用类加载器类路径下(如web项目/WEB-INF/lib)中的类,web.xml的解析与注射工作是由Digester来完成的,而Digester在实例化对象时正是取的上下文的类加载器,调用栈如下:
加载完应用后,通过unBindThread方法把老的类加载器catalinaLoader还原回上下文类加载器中,否则容易乱。
Tomcat应用类加载器重写了loadClass,而并不是直接调用父类的loadClass,但实现思路是很相似的,首先是判断该类有无加载过,加载过则直接返回,没有则让j2se的类加载器先加载,以防web应用中定义了j2se重名的类覆盖j2se中的类这种情况的发生,也是加载到了即返回,如没有在根据是否有委托设置来决定让当前类加载器来加载还是父类加载器来先加载,这个参数决定了当前类加载器与父类加载器加载的先后顺序,只要其中一个加载到了就返回,如果都没加载到就抛ClassNotFoundException,默认情况下是不委托的,当一个类既存在share.loader类路径中又存在于应用类路径中,那么是否委托则显得尤其重要了。
第三种则是jsp对应的类加载器(JasperLoader),tomcat可实现对jsp动态加载,在tomcat启动后,存在一个Mapper的数据结构存放访问资源对应的Wrapper等信息。当访问jsp资源时,连接器适配器CoyoteAdapter会根据请求信息以及一些附加信息封装到org.apache.catalina.connector.Request和org.apache.catalina.connector.Response中,之后Mapper根据请求资源通过二分查找算法找到Mapper内部类各数据结构对应的唯一实例,在把这些实例中的信息一起封装到新的数据结构MappingData中,并把MappingData对象设置到Request中,一个MappingData对象存放了一个url在tomcat处理所需经过的容器信息,设置完MappingData对象后,会进一步把MappingData部分信息封装到Request相关变量中进行维护,如Context,Wrapper等信息,如资源是jsp,对应的Wrapper则为StandardWrapper[jsp],请求最终通过连接器传到容器管道中逐层处理。
当index.jsp资源第一次被访问时,会判断\work\Catalina\localhost\_\org\apache\jsp\index_jsp.class是否存在jsp对应的class文件,如没有,则会进行编译操作,在编译前会将jsp编译器上下文(JspComplicationContext)关联的加载器置空,进入JDTComplier生成class文件时(generateClass)会从jsp编译上下文取类加载器,此时就会创建JSP类加载器,代码如下:
根据jsp文件生成java代码调用栈如下:
是否需要编译或者重新编译jsp文件,主要逻辑在编译器的抽象类Compiler的isOutDated方法中,当jsp被访问后,会根据命名规范把jsp生成的class在放在work目录下; 当重启服务器后,对应的class文件还是存在的,此时再次访问这个资源的时,如此时对应jsp文件未作修改的话,则会重新加载这个jsp对应的servlet,并进行初始化,而无需要重新编译了,此时Jsp编译器上下文对应的类加载器如未设置的话,则会重新创建一次,事实上这种场景也是需要在创建jsp类加载器的,调用栈如下:
上面解答了JSP类加载器是何时,何种场景下创建的。
下面来解答是如何创建的?
弄清楚这个jsp类加载器的创建过程实则弄清楚类路径是如何指定的,以及父类加载是如何指定的,创建代码如下:
该方法是jsp编译上下文(JspCompilationContext)中的一个方法,当jsp请求在对应StandardWrapperValue中做处理时,阀(StandardWrapperValue)通过所在的容器(StandardWrapper)获取对应的servlet,如servlet未进行初始化则先进行初始化(init),初始化在整个生命周期中只进行一次,之后会根据请求,servlet信息创建一个过滤器链,并执行这个过滤链,如这个过滤器链中有设置过滤器,先执行过滤器,最后执行servlet。
在JspServlet进行真正处理jsp时(指service方法),会根据JspServlt维护的JspRuntimeContext,ServletConfig,jspuri等对象包装成一个JspServletWrapper新对象,在这个JspServletWrapper实例化过程中,就会实例化一个jsp编译上下文(JspCompilationContext)对象,因此jsp编译上下文是在tomcat启动后处理请求的过程中实例化的,之后在判断是否需要编译,在这过程中会判断输出目录是否为空,为空的话创建并且设置成类路径,调用栈如下:
上述是类路径的指定过程。
如jsp编译上下文指定了类加载器(setClassLoader),则用该类加载器作为jsp类加载器的父加载器,否则就取Jsp运行时(JspRuntimeContext)上下文关联的parent类加载器,而JspRuntimeContext中维护的parent类加载器是在应用启动的过程中初始化JspServlet中指定的,并且取的是上下文类加载器作为parent的值,很明显parent实则为tomcat应用类加载器(WebappClassLoader),JspRuntimeContext维护的parent类加载器指定的调用栈如下:
因而在处理jsp请求时就会通过这个阀(StandardWrapperValue)获取对应容器(StandardWrapper[Jsp]),阀与容器的关系在实例化容器时就建立的,之后便获取JspServlet进行处理jsp资源,如jsp资源需要编译则就会在此过程中获取JspRuntimeContext中维护好的parent的ClassLoader来实例Jsp类加载器。
每个jsp资源对应着各自的JspServletWrapper,每个JspServletWrapper对应着各自的jsp编译上下文(JspComplicationContext),而JspRuntimeContext是所有的JspServletWrapper的容器,代码逻辑如下:
上述是Jsp类加载器实例化过程中指定父类加载器涉及的相关过程。
下面来回答第三个问题?
3、java的类加载器是如何与启动Tomcat类加载器做连接的?需要注意哪些点?
当以调试源码方式启动tomcat时,Tomcat自身的类都是通过java的AppClassLoader加载的,因为org.apache.catalina.startup.Bootstrap与其他包里面的类都是在同一个类路径下(tomcat源码工程\bin 目录);而通过批处理启动安装的tomcat时,org.apache.catalina.startup.Bootstrap是被AppClassLoader加载的,org.apache.catalina.startup.Catalina则是被catalinaLoader加载的;可以写个jsp页面作为工具来获取运行期指定类是被哪个类加载器所加载的,以及对应的物理路径是哪。其实不难发现,Bootstrap是被打成jar放在tomcat安装目录/bin目录下,而tomcat自身的其它类则是打成各种jar放在tomcat安装目录/lib目录下,批处理在执行时,会把bin目录也设置成了AppClassLoader的类路径,因此这是要分别设计一个Bootstrap与Catalina的好处,要注意的是这两个类在部署时是需要隔离的,并且需把Bootstrap所在的路径设置成AppClassLoader的类路径。
下面则是第四个问题的解答。
4、为什么要设置上下文类加载器?其核心起了什么作用?Tomcat是如何使用的?
在显示loadClass时可以方便的从当前线程中拿到另外一个可用类加载器,如在java中提供了一些spi(Service Provider Interface)的接口,而实现是由其真正厂商实现的,因此spi的实现类可能没有部署在AppClassLoader的类路径下而可能是在其他容器的类路径下或者在容器应用的类路径下,因而可能需要指定的自定义类加载器去加载,而上下文类加载器是可设置更改的,因此十分适合这种场景的使用,上下文类加载器创建逻辑如下:
在实例化一个线程类(Thread)时就会默认指定一个上下文类加载器,并且提供了setContextClassLoader方法进行更改。
Tomcat通过Digester实例化对象时典型的应用了上下文类加载器,调用栈如下:
在启动上下文时等场景也有用到上下文类加载器。
tomcat在设计类结构时,对程序入口(如Bootstrap,Catalina),容器对象,一些"运行时上下文"(指JspRuntimeContext)对象,Digester都提供了一个类加载器成员变量维护类加载器信息;当Tomcat新增线程处理请求时,新增线程此时上下文类加载器为StandardClassLoader,当请求进入StandardHostValue处理时,则会取StandardContext中维护的类加载器为该线程上下文类加载器,之后进行下一层的处理,处理完后在把老的类加载器重新设置回上下文类加载器中,在使用上下文类加载器时这些点都是需要注意的,部分代码如下:
最后来回答第五个问题。
5、如何查看运行期对象是被哪个类加载器加载的?以及对应的路径?
写个jsp页面,核心代码如下:
放到tomcat其中一个普通应用中即可,之后通过如下类似url获取运行期指定类的类加载器以及路径。
http://localhost:8080/hello/getclassurl.jsp?className=org.apache.catalina.core.StandardContext
此文Tomcat版本为Tomcat7.0.57,不同版本有些地方可能有些差异。
推荐阅读
- 如何寻找情感问答App的分析切入点
- D13|D13 张贇 Banner分析
- Linux下面如何查看tomcat已经使用多少线程
- 自媒体形势分析
- 2020-12(完成事项)
- Android事件传递源码分析
- Python数据分析(一)(Matplotlib使用)
- 探索免费开源服务器tomcat的魅力
- Quartz|Quartz 源码解析(四) —— QuartzScheduler和Listener事件监听
- 泽宇读书会——如何阅读一本书笔记