当前位置: 代码迷 >> 综合 >> Tomcat8.5类加载机制
  详细解决方案

Tomcat8.5类加载机制

热度:98   发布时间:2024-02-22 18:21:02.0

JVM的类加载机制

JVM默认内置3种类加载器,分别是:

1.BootstrapClassLoader

     引导类加载器,由c++实现,用于加载java的核心类库,如rt.jar,提供了调用java自定义类main方法的入口,java环境下无法获取其引用。

默认加载的类路径由系统变量sun.boot.class.path指定,默认是JRE_HOME/lib,可以通过两种方式指定引导的类路径:

1. 使用-Xbootclasspath指定要加载的核心类库如:

-Xbootclasspath:"JRE_HOME\lib\resources.jar;JRE_HOME\lib\rt.jar;JRE_HOME\lib\sunrsasign.jar;JRE_HOME\lib\jsse.jar;JRE_HOME\lib\jce.jar;JRE_HOME\lib\charsets.jar;JRE_HOME\lib\jfr.jar;"

2.使用-D参数指定系统变量sun.boot.class.path的值来告诉核心类库的路径,如:

-Dsun.boot.class.path="JRE_HOME\lib\resources.jar;JRE_HOME\lib\rt.jar;JRE_HOME\lib\sunrsasign.jar;JRE_HOME\lib\jsse.jar;JRE_HOME\lib\jce.jar;JRE_HOME\lib\charsets.jar;JRE_HOME\lib\jfr.jar;"

注:以上两种方式指定jar时必须提供完整的jar路径,不支持直接配置目录,且多个路径必须使用“系统路径分隔符”分隔。

 

2.ExtClassLoader

    扩展类加载器,用于加载核心类库之外的扩展类库,加载路径由系统属性java.ext.dirs指定, 默认在JRE_HOME/lib/ext下。

例如对于jce的实现,sun提供了sunjce_provider.jar,如果sun的部分加密算法不符合你的需求,可以使用bcp的实现,只需将bcprov-ext-jdk15on-160.jar拷贝到JRE_HOME/lib/ext下即可。可以通过如下方式指定扩展目录:

-Dsun.boot.class.path="JRE_HOME\lib\ext;D:/ext"

注:与sun.boot.class.path不同,这里支持配置目录,系统会自动搜索目录下所有jar包和class文件,多个目录使用“系统路径分隔符”分隔。

 

2.AppClassLoader

    应用(或系统)类加载器,用于加载用户自定义的类,由Java -classpath选项指定,未指定,继续找系统属性-Djava.class.path,还没有,则检查环境变量classpath,如果还没有,默认是运行类所在的当前目录。

 

类加载机制

 

    BootstrapClassloader|ExtClassloader|AppClassloader/     \Custom1   Custom2 ...

 jdk类加载默认使用双亲委派模式,即当一个类加载器收到了类加载请求,它会把这个请求委派给父(parent)加载器加载,所有的加载请求最终都被传送到顶层的启动类加载器中。只有在父加载器无法加载该类时再从上到下从自己的类路径中查找。

注意事项:

1. 我们自定义的classloader一般直接或间接继承自ClassLoader类,并实现findClass从我们希望的路径中寻找class文件,并最终通过ClassLoader的defineClass(final方法无法覆盖)去加载类,如果需要打破双亲委派的规则需要重写loadClass,但无论如何java核心类库的加载我们应该始终交由javase的类加载器(ExtClassloader,BootstrapClassloader)进行加载。

2.同一个类加载器只能加载类一次,如果强行加载载多次会报连接错误。

3.由子加载器(层级关系parent,不是继承)的类可以访问父加载器加载的类,但由父加载器加载的类无法直接访问由子加载器加载的类。例如通过AppClassloader加载的A类访问了一个只能由Custom1(如上图)加载的类B,由于双亲委派机制,AppClassloader会将B的加载委托给父ExtClassloader直到BoosstrapClassloader,最终因为这3个加载器都无法载B而报错。

同理由Custom1加载的类A也无法直接访问由Custom2加载的类B,因为由Custom1加载的A,在运行时遇到了B时,会尝试使用Custom1加载B,很显然是加载不到的。那么如何才能访问B?用反射可以,如:

//由Custom1加载的类A,其方法f访问由Custom2加载的B
public void f() {//这里不能直接暴露B的类型,否则会报类找不到,只能使用反射api调用B的方法Object bInstance = Custom2.loadClass("B").newInstance();System.out.print(bInstance.toString());
}

 通过反射无法知道被调用类的具体类型,也无法直接调用类的方法,非常不便,那么该如何在类A中显式调用B类的方法?

需要声明一个接口C,C接口必须有Custom1与Custom2共同的父加载器加载(如上图的AppClassloader),并且让B类去实现接口C,伪代码如下:

//C必须由Custom1与Custom2共同的父加载器如AppClassloader加载
public interface C {public void f();}//类A由Custome1加载
public class A {public static void main(String[] args) throws Exception {//没有显式暴露B的类型,但可以暴露CC bInstance = (C)(Custom2.loadClass("B").newInstance());bInstance.f();//成功通过接口方法调用了由Custom2加载的B的f方法}}public class B implements C {@Overridepublic void f() {}}

4.同一个class文件交给不同的ClassLoader加载产生的Class对象是不相等的(==或equals都为false),它们彼此独立,包括类上的静态属性,且类型无法兼容,因此对于两个不同加载器创建的User对象无法直接互通,如User u1由Custom1加载,User u2由Custom2加载,对于u1=u2这样的赋值是不允许也是无法强转的,就好像一个String对象无法赋值给Date对象一样。

试想下:如果我想定义一个java.lang.String,并自定义一个类加载器Custom3,打破双亲委派,先从本地路径加载搜索,搜索不到再委托给父加载器,这样是否能对系统进行攻击?

其实是行不通的,因为Classloader的defineClass在加载用户自定义类之前发现包名以java开头会抛出SecurityException异常。即使可以使用自定义的String,但当你的类用到了java核心的类,而核心类是由ext或bootstrapClassloader加载,核心类中使用的String类型只能由bootstrapClassloader类加载器加载(上层的类加载器,无法访问下层类加载器加载的类),于是整个系统会出现两个相同签名单不同类型的String对象(一个是bootstrap加载的String,一个是Custom3加载的String),这样也可能导致错误发生。

 

Tomcat类加载机制

 

tomcat默认提供5中类型的类加载器,如图:
 

                       BootstrapClassloader|ExtClassloader|AppClassloader|commonLoader|_______________________________|                             ||                             |catalinaLoader                  sharedLoader/     \/       \webappLoader1   webappLoader2 .../   \/     \jasperloader1   jasperloader2 ...

1. commonLoader (父为AppClassloader)

     tomcat服务器最顶层的公共类加载器,加载路径在文件catalina.properties中配置,由配置项“common.loader“指定,

由服务器本身和web应用共享,例如servlet接口规范包如servlet-api.jar、jsp-api.jar,tomcat作为servlet容器的标准实现必然需要依赖这些包,而我们的应用需要实现我们自己的servlet也需要依赖这些包,这种情况将servlet-api.jar、jsp-api.jar放到common.loader指定的路径下并有commonLoader加载是做好的选择。

2. catalinaLoader(父为commonLoader )

    Tomcat容器私有的类加载器,用于加载框架类本身,加载路径中的class对于Webapp不可见,加载路径在文件catalina.properties中配置,由配置项“server.loader“指定。加载诸如:catalina.jar(核心类库,包含启动入口类)、tomcat-coyote.jar(对socket字节流的支持)等。

3. sharedLoader(父为commonLoader )

    被各个web应用共享的类加载器,加载路径中的class对于所有web应用可见,由于与catalinaLoader平级,因此tomcat框架核心类对应用不可见,加载路径在文件catalina.properties中配置,由配置项“shared.loader“指定。

例如一些jdbc数据库的连接驱动被所有web应用共享,可以由它加载。

4. webappLoader(父为sharedLoader)

    web应用私有的类加载器,一般用于加载应用自身/WEB-INF/classes下的class文件和/WEB-INF/lib目录下的jar

5. jasperLoader(父为webappLoader)

    用于加载由jsp编译生成的servlet,默认路径为${catalina.base}/work/Catalina/localhost/

Catalina为service.xml下service的名称,localhost为Host节点的名称。

注:

需要注意的是自tomcat6以后,在catalina.properties中默认只配置了“common.loader“,用于commonLoader的实例化,而其它两个选项

“server.loader“与“shared.loader“默认是空,因此这两个类加载器不会单独创建实例,而是使用共同实例commonLoader,即

commonLoader 、catalinaLoader、sharedLoader三者是同一个实例对象,都指向commonLoader ,因此tomcat6之后默认的层次图如下:

                       BootstrapClassloader|ExtClassloader|AppClassloader|commonLoader/     \/       \webappLoader1   webappLoader2 .../   \/     \jasperloader1   jasperloader2 ...

java应用程序的入口是由“main“方法开始的,而main方法所在的类只能由AppClassloader类加载器加载,AppClassloader

通常从CLASSPATH环境变量或java.class.path系统属性或-classpath选项指定的路径中加载所需的类。所有这些类对于Tomcat内部类和Web应用程序都是可见的。但是,标准的Tomcat启动脚本($CATALINA_HOME/bin/catalina.sh或 %CATALINA_HOME%\bin\catalina.bat)完全忽略CLASSPATH环境变量本身的内容,而是从以下存储库构建AppClassloader:

  • $ CATALINA_HOME / bin / bootstrap.jar — 包含用于初始化Tomcat服务器的main()方法位于Bootstrap类,以及tomcat内部依赖的类加载器实现类(ClassLoaderFactory工厂类)。

  • $ CATALINA_BASE / bin / tomcat-juli.jar或 $ CATALINA_HOME / bin / tomcat-juli.jar —记录日志的实现类。其中包括对java.util.loggingAPI的增强类 ,称为Tomcat JULI。

    如果tomcat-juli.jar是出现在 $ CATALINA_BASE / bin中,它被用来代替一个在 $ CATALINA_HOME / bin中。在某些日志记录配置中很有用

  • $ CATALINA_HOME / bin / commons-daemon.jar — Apache Commons Daemon项目中的类,好像是将java程序以一个服务运行在系统上,有兴趣的自己研究下。

前面提到,tomcat框架类的加载不是交由catalinaLoader来完成的么?没错,tomcat的启动类有两个,一个是Bootstrap类包含了main方法,只能由AppClassloader加载,另一个是Catalina类,对于tomcat的的启动停止(实则调用Bootstrap的start、stop)其实都是将调用委托给Catalina类实现的,也就是所Catalina类才是tomcat的真正入口类。Bootstrap做了哪些事?:

1.负责创建tomcat的3个核心类加载器catalinaLoader,commonLoader,sharedLoader由AppClassloader加载。

2.通过catalinaLoader创建Catalina类,并将对Bootstrap发起的调用都转发给Catalina,由于Catalina由catalinaLoader加载,因此

Catalina依赖的所有框架类,默认的类加载器也变成了catalinaLoader。

绕了个弯子只是为了安全,因为catalinaLoader并不在web应用程序加载器的层次中,也就无法直接访问tomcat核心类

 

应用类加载器webappLoader

    与jvm的委派模式不同,webappLoader在加载应用类时并没有直接委托给父加载器加载,tomcat打破委派机制的原因是什么?

通过前面的讲解我们知道,一个类加载器只能加载类一次,如果我委托给父加载器加载,比如AppClassloader,而AppClassloader在虚拟机启动的时候有且仅有一个实例,对于多个应用使用相同的类,如App1和App2都用到了log4j的类,是无法同时加载的,必然导致应用启动失败,另外tomcat支持热加载,修改一个class文件,在不启动应用的情况下重新加载修改后的类,显然AppClassloader无法加载两次,除非AppClassloader可以先卸载之前就的Class再加载新的,不过很遗憾,jvm没有提供支持,对于一个类的卸载,仅当没有任何引用指向一个ClassLoader时,此Classloader和其加载的类才会被卸载。而AppClassloader是不可能被卸载的。要解决此问题只能自己实现类加载器,从自己的本地仓库加载类,在有必要的情况下销毁自定义的Classloader并重新创建,来实现热加载功能。

加载类过程如下:

1.从自身的缓存中查找,webappLoader维护一个map缓存已加载的类

2.如果自身缓存中没找到,则从虚拟机的缓存中查找

3.如果没有再委托给ExtClassloader、BootstrapClassloader查找,这样javase的核心类就可以仍由核心加载器加载

4.如果还没找到再从本地挂载路径/WEB-INF/classes中查找(不知道什么是挂载路径?可以简单理解为先WEB-INF/classes,与WEB-INF/lib中查找)

5.如果还没找到,再委托给sharedLoader加载(sharedLoader委托给commonLoader,再委托给AppClassloader。。。)

6.还没找到,抛出ClassNotFoundException异常

注:tomcat针对Loader元素提供了delegate属性,用于控制是否启用java的委派模式,默认是false,如果设成true,那么第4、5的搜索顺序进行颠倒,即按:1,2,3,5,4,6进行搜索,实际上在如果在步骤3之后tomcat发现要加载的类的包名是以如下包开头,那么delegate将隐式为true,也会默认按1,2,3,5,4,6搜索:

javax.el.、javax.servlet.、javax.websocket.、javax.security.auth.message.、

org.apache.jdbc.、org.apache.catalina.、org.apache.jasper.、org.apache.juli.、

org.apache.tomcat.、org.apache.naming.、org.apache.coyote.

 

JasperLoader

    用于加载由jsp编译后的servlet,每个jspservlet由一个JasperLoader加载,纯一对一关系,为什么是一个JasperLoader每次只加载一个jspservlet?是为了支持热加载,我们经常在不重启动tomcat的情况下修改jsp也能生效。一旦文件被修改,tomcat会将旧的JasperLoader丢弃(没有引用再指向旧的JasperLoader),以达到销毁的目的,然后再创建一个新的JasperLoader实例用于加载修改后的jspservlet进行服务。JasperLoader的父加载器是webappLoader,默认搜索路径如下:

1.从虚拟机的缓存中查找

2.没找到,查看包名是不是以org.apache.jsp开头,如果是,则从tomcat临时目录work目录下查找(默认tomcat会将jsp编译成servlet并放到work目录下,包名以org.apache.jsp开头)

3.如果包名不以org.apache.jsp开头,则委托给webappLoader进行查找

4.找不到,抛出ClassNotFoundException异常

注:为啥要检查是否以org.apache.jsp开头?因为你编译后的jspservlet可能要到了其它类,由于classloader的继承,这些类的加载默认也会使用相同的JasperLoader进行加载,当然需要直接委托给webappLoader。

java endorsed覆盖机制

Java SE运行时环境可以使用自定义JAR文件中的类来覆盖Java核心类。比如你想自定义一个ArrayList用来覆盖JDK自带的ArrayList,你只需要提供相同签名的不同实现版本,然后打成jar包并在启动程序是使用-Djava.endorsed.dirs来设置你自定义jar包路径即可。而对于tomcat你需要将相关jar包放到$CATALINA_HOME/endorsed目录下即可。

不过endorsed机制能够覆盖的包是有限的,比如jdk默认的一些提供xml解析的包可以通过这种方式覆盖,详细参考官方文档https://docs.oracle.com/javase/8/docs/technotes/guides/standards/,值得注意的是oracle并不推荐使用此特性,因为此特性可能在未来版本中删除。

 

自定类查找路径

    前面讲WebappClassLoader时,提到加载类时会搜索挂载路径“/WEB-INF/classes”,这是一个虚拟的路径代表了应用程序加载类时应该搜索的位置,默认情况下搜索挂载路径"/WEB-INF/classes"代表从$WebRoot/WEB-INF/classes和$WebRoot/WEB-INF/lib中查找。如果我们想继续从其它位置查找类,只需要将我们的路径挂载到/WEB-INF/classes下即可。

配置挂载路径需要在Context/Resources指定PreResources、ClassResources、JarResources、PostResources资源节点进行配置(详见http://imgs.hrm.cn/docs/config/resources.html),

如:

<PostResources base="D:\Projects\external\classes"className="org.apache.catalina.webresources.DirResourceSet"webAppMount="/WEB-INF/classes"/>

这段配置代表,如果应用程序,无法从默认的$WebRoot/WEB-INF/classes和$WebRoot/WEB-INF/lib中找到需要的类可以继续在

D:\Projects\external\classes下查找,除了PostResources还有PreResources、MainResources、ClassResources、JarResources

不过MainResources、ClassResources由tomcat内部初始化,不支持xml配置

这几个资源,内部搜索顺序依次为:

  • PreResources
  • MainResources
  • ClassResources
  • JarResources
  • PostResources

ClassResources保存的资源默认默认是由tomcat启动是初始化的默认挂载路径就是/WEB-INF/classes,实际指向的资源就是来自于$WebRoot/WEB-INF/lib

  相关解决方案