tomcat之加载器

在前面的章节中已经介绍了一个简单的加载器,用它来加载 servlet 类。这一章 会介绍标准网络应用加载器(standard web application loader),简单的说就 是加载器。一个 servlet 容器需要一个定制的容器,而不是简单的使用系统的加 载器。如果像前面章节中那样使用系统的加载器来加载 servlet 和其他需要的类, 这样 servlet 就可以进入 Java 虚拟机 CLASSPATH 环境下面的任何类和类库,这 会带来安全隐患。Servlet 只允许访问 WEB-INF/目录及其子目录下面的类以及部 署在 WEB-INF/lib 目录下的类库。所以一个 servlet 容器需要一个自己的加载器, 该加载器遵守一些特定的规则来加载类。在 Catalina 中,加载器使用 org.apache.catalina.Loader 接口表示。

Tomcat 需要一个自己的加载器的另一个原因是它需要支持在 WEB-INF/classes 或者是 WEB-INF/lib 目录被改变的时候会重新加载。Tomcat 的加载器实现中使 用一个单独的线程来检查 servlet 和支持类文件的时间戳。要支持类的自动加载 功能,一个加载器类必须实现 org.apache.catalina.loader.Reloader 接口。

Java 类加载器

在每次创建一个 Java 类的实例时候,必须先将该类加载到内存中。Java 虚拟机 (JVM)使用类加载器来加载类。Java 加载器在 Java 核心类库和 CLASSPATH 环 境下面的所有类中查找类。如果需要的类找不到,会抛出 java.lang.ClassNotFoundException 异常。

从 J2SE1.2 开始,JVM 使用了三种类加载器:bootstrap 类加载器、extension 类加载器和 systen 类加载器。这三个加载器是父子关系,其中 bootstrap 类加 载器在顶端,而 system 加载器在结构的最底层。
其中 bootstrap 类加载器用于引导 JVM,一旦调用 java.exe 程序,bootstrap 类加载器就开始工作。因此,它必须使用本地代码实现,然后加载 JVM 需要的类

另外,它还负责加载所有的 Java 核心类,例如 java.lang 和 java.io 包。另外 bootstrap 类加载器还会查找核心类库如 rt.jar、i18n.jar 等,这些 类库根据 JVM 和操作系统来查找。

extension 类加载器负责加载标准扩展目录下面的类。这样就可以使得编写程序 变得简单,只需把 JAR 文件拷贝到扩展目录下面即可,类加载器会自动的在下面 查找。不同的供应商提供的扩展类库是不同的,Sun 公司的 JVM 的标准扩展目录 是/jdk/jre/lib/ext。

system 加载器是默认的加载器,它在环境变量 CLASSPATH 目录下面查找相应的 类。
这样,JVM 使用哪个类加载器?答案在于委派模型(delegation model),这是出 于安全原因。每次一类需要加载,system 类加载器首先调用。但是,它不会马 上加载类。相反,它委派该任务给它的父类-extension 类加载器。extension 类加载器也把任务委派给它的父类 bootstrap 类加载器。因此,bootstrap 类加 载器总是首先加载类。如果 bootstrap 类加载器不能找到所需要的类的 extension 类加载器会尝试加载类。如果扩展类加载器也失败,system 类加载器 将执行任务。如果系统类加载器找不到类,一个 java.lang.ClassNotFoundException 异常。为什么需要这样的往返模式?

委派模型对于安全性是非常重要的。如你所知,可以使用安全管理器来限制访问 某个目录。现在,恶意的意图有人能写出一类叫做 java.lang.Object,可用于 访问任何在硬盘上的目录。因为 JVM 的信任 java.lang.Object 类,它不会关注 这方面的活动。因此,如果自定义 java.lang.Object 被允许加载的安全管理器 将很容易瘫痪。幸运的是,这将不会发生,因为委派模型会阻止这种情况的发生。 下面是它的工作原理。

当自定义 java.lang.Object 类在程序中被调用的时候,system 类加载器将该请 求委派给 extension 类加载器,然后委派给 bootstrap 类加载器。这样 bootstrap
类加载器先搜索的核心库,找到标准 java.lang.Object 并实例化它。这样,自
定义 java.lang.Object 类永远不会被加载

如何破坏双亲委任模型?

双亲委任模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java的世界中大部分的类加载器都遵循者模型,但也有例外,到目前为止,双亲委派模型有过3次大规模的“被破坏”的情况。

第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。
第二次:是这个模型自身的缺陷导致的。我们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?

这不是没有可能的。一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢?

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。

有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。但这无可奈何,Java中所有涉及SPI的加载动作基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。

第三次:为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。

Tomcat 的类加载器是怎么设计的?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
  3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

再看看我们的问题:Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?我们看,第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。第三个问题和第一个问题一样。我们再看第四个问题,我们想我们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat 如何实现自己独特的类加载机制?

所以,Tomcat 是怎么实现的呢?牛逼的Tomcat团队已经设计好了。我们看看他们的设计图:

image.png

我们看到,前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

好了,至此,我们已经知道了tomcat为什么要这么设计,以及是如何设计的,那么,tomcat 违背了java 推荐的双亲委派模型了吗?答案是:违背了。 我们前面说过:

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。

很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

我们扩展出一个问题:如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?

看了前面的关于破坏双亲委派模型的内容,我们心里有数了,我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。牛逼吧。

你可能感兴趣的:(tomcat之加载器)