Tomcat的类加载顺序的实现
其实之所以为写这篇文章的原因,主要是前段时间,因为使用第三方的一些库,需要将这些库放到JDK的ext目录,由Java的ExtClassLoader进行加载,第三方库也是用了日志框架,而且使用的是slf4j
,所以要使用这个第三方库,必须将slf4j-api.jar
这个slf4j的接口包放到ext目录下。将这个日志放到slf4j-api.jar
放到ext目录下,接着应用层的日志打印不出来,应用层的日志包也是使用slf4j
,应用是在Tomcat中跑的。
上面的问题,在Debug日志包和查找类加载相关的资料,最终发现与类加载相关;实际上一直使用的slf4j-api.jar
是ext目录下,而它的实现包在应用层,即项目中引入的,ExtClassLoader
加载的slf4j-api.jar
日志包,根据类加载的特性不允许向下查找到它的实现包slf4j-simple.jar
,所以实际上就会找不到实现,导致日志打印不出来问题。如下示意图,这是类加载的特性和顺序决定的,具体可以参考另外一篇关于Java的类加载器的文章:https://blog.csdn.net/strive_or_die/article/details/98664519。
应用服务器一般都会自行创建类加载器来实现更加灵活的控制,Tomcat服务器也不例外。
- 隔离性:Web应用类库互相隔离,避免依赖库或者应用包相互影响。因为一个Tomcat服务器可以跑多个应用,不同的应用对于同一个包所依赖的版本可能不一样,例如有的
slf4j-api
包的版本是1.6.x,而有的应用是1.8.x,那么这样必然会导致部分应用的日志无法打印。 - 灵活性:Web应用之间的类加载器互相独立,那么就可以针对一个Web应用进行重新部署,此时Web应用的类加载器会重新创建并且不会影响其他Web应用。
- 性能:由于每个Web应用都有一个类加载器,因此Web应用在加载类时,不会搜索其他Web应用包含的Jar包,这样性能是比只有一个累加器的情况要好的。
先看一下Tomcat的类加载器的图示:
从上图中可以看到,除了每个Web应用加载器外,Tomcat也提供了3个基础的类加载器,这3个类加载器指向的路径和包列表都可以再catalina.properties
中配置。
- Common Class Loader:以System类加载器为父类加载器,位于Tomcat应用夫妻顶层的公用类加载器。它的路径为
commmon.loader
,默认指向的是$CATALINA_HOME/lib
下的包。 - Catalina Class Loader:以Common类加载器为父类加载器,用于加载Tomcat应用服务器的累加器,其路径为
server.loader
,默认为空。此时Tomcat使用Common类加载器加载器应用服务器。 - Shared Class Loader:以Common类加载器为父类加载器,是所有父类应用加载器的父类加载器,其路径为为shared.loader,默认为空,此时此时Tomcat使用Common类加载器作为Web应用的父类加载器。
- Web Application Class Loader:以Shared为父类加载器,加载
/WEB-INF/classes
目录下的未压缩的Class和资源文件以及/WEB-INF/lib
目录下的Jar包。特别注意:该类加载器只对当前Web应用可见,对其他Web应用都不可见。
首先,Common类加载器负责加载Tomcat的应用服务器了内部和Web应用均可见得类,例如Servlet相关包和一些通用的工具包。
其次,Catalina类加载器负责加载只有Tomcat应用服务器内部可见的类,这些类对Web应用不可见。如Tomcat的具体实现类,因为我们的Web应用最好和服务器松耦合。
再次,Shared累加器负责加载Web应用共享的类,这些类Tomcat服务器不会依赖。
最后,Tomcat服务器$CATALINA_HOME/bin
目录下的包作为启动入口由System类加载器加载。通过将者几个启动包剥离,Tomcat简化了应用服务器的启动,同时增加了灵活性。
Tomcat的默认加载机制是委派模式,委派过程如下。
- 1.从缓存中加载
- 2.如果缓存中没有,则从父类加载器中加载。
- 3.如果父类加载器没有,则从当前类加载器加载。
- 4.如果没有,则抛出异常。
Tomcat提供的Eeb应用类加载器和默认的委派模型有些地方是不同的。当进行类加载是,除了JVM基础类库外,他首先会尝试通过当前累加器加载,然后才进行委派。另外一点需要注意的是:Servlet规范相关API禁止通过Web应用类加载器加载,因此,不要在Web应用中包含这些包,例如javax.servlet-api.jar
。
Web应用加载器默认加载顺序如下:
- 1.从缓存中加载
- 2.如果没有,则从扩展类加载器和Bootstrap类加载器中加载。
- 3.如果没有,则从当前类加载器加载(按照
WEB-INF/classes、WEB-INF/lib
的顺序)。 - 4.如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模型,所以加载顺序为System、Common、Shared。
Tocmat提供了delegate属性用于控制是否启用Java委派模型,默认为false(不启用)。当配置为true时,Tomcat将使用默认委派模型,即按如下顺序加载。
- 1.从缓存中加载
- 2.如果没有,则从扩展类加载器和Bootstrap类加载器中加载。
- 3.如果没有,则从父类加载器加载,加载顺序为System、Common、Shared。
- 4.如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)。
通过前面的Tomcat的类加载器知识,我们可以知道为啥那个在ext目录下的slf4j-api.jar
会使得应用的jar打印不出来,因为即使Tomcat实现了自己的类加载器,但是对于Web应用类加载器来说,扩展类加载器和Bootstrap类加载器是优先于它进行加载的,所以应用中的slf4j-api.jar
实际并没有被加载,ext目录下的被加载了,但是正常的类加载是没法实现父类加载器向子类加载器方向去寻找实现的,所以slf4j-api.jar
找不到实现了,打印不出日志。
那该怎么解决呢,有了上面的知识,我们可以知道如果Tomcat的delegate属性为false时,这也是默认值来的,Web应用加载器优先于System、Common、Shared类加载器,那么我们可以将第三方库和它所以来的slf4j-api.jar
包移动到Common类加载器加载的目录$CATALINA_HOME/lib
下,那么就可以保证使用的slf4j-api.jar
是应用中引入的了,这样我们的应用就可以打印出日志了。特别注意:这时候的第三方库的日志是打印不出来的,因为它依旧没法找到它的日志实现包,除非将它的实现包也放到$CATALINA/lib
下。
Tomcat 架构解析 刘光瑞 著.
https://tomcat.apache.org/tomcat-9.0-doc/class-loader-howto.html