06-阿里面试题:Tomcat容器类加载器设计 [线程上下文类加载器+破坏性双亲委派机制]

简介: Tomcat这种web容器中的类加载器应该如何设计实现?

首先我们来看下Tomcat类加载器的设计结构:

那么应用程序类加载器下的都是Tomcat自定义的类加载器,Tomcat为什么要自定义这么多类加载器又分别有什么用呢?

我们通过以下一张图来进行说明:

首先Tomcat会通过Common类加
载器来加载本地lib包下的核心文件,比如servlet-api.jar、jsp-api.jar、el-api.jar等,这些类可以供Tomcat以及所有的WebApp进行访问和使用。

可以通过查看 conf/catalina.properties配置文件查看

common.loader="${catalina.base}/lib","${catalina.base}/lib/.jar","${catalina.home}/lib","${catalina.home}/lib/.jar"

其次Catalina类加载器加载Tomcat应用程序所独有的一些类文件,这些文件对所有WebApp不可见,比如实现自己的会话存储方案。其路径由server.loader指定,默认为空,可以手动更改指定。

可以通过查看 conf/catalina.properties配置文件查看

server.loader=

再次,Shared类加载器负责加载Web应用共享类,这些类tomcat服务器不会依赖。

可以通过查看 conf/catalina.properties配置文件查看

shared.loader=

而我们的WebApp类加载器主要是加载我们每个应用程序自己编写的代码,主要路径为: /WEB-INF/classes/目录下的Class和资源文件 以及 /WEB-INF/lib目录下的jar包,该类加载器加载的资源仅对当前应用程序可见,其他应用程序无法访问。并且WebApp类加载器可以使用到上级Shared类加载器加载到的类。

最后JSP类加载器是为每一个JSP文件单独设计的一个类加载器,这也能解释为什么JSP文件被修改后不用重启服务器就能实现新的代码功能,这也是现在的热部署方案原因。当某一个JSP文件被修改后,对应的类加载器会被销毁重新创建新的一个JSP类加载器进行加载。

问题思考:

当我们的服务器中有多个应用程序的时候,并且都使用到了Spring来进行组织和管理,那么我们就可以把Spring放到Shared类加载器路径下进行加载,让所有应用程序进行共享,我们自己写的代码由于是WebApp加载器加载的所以访问上级Shared加载器加载的类是没问题的。但是Spring中的类要对应用程序中的类进行管理,如何访问呢?根据我们上文所说的双亲委派机制,显然是无法做到让上级类加载器去请求下级类加载器进行类加载的动作的。因此这里我们需要引出上下文类加载器机制。(如下图)

破坏双亲委派机制

上文提到过双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

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

都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

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

其实Tomcat中的JSP类加载器的设计就是一种热部署的实现,也是一种打破了双亲委派模型的一种设计。这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。

总结:

按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。

下一篇文章我们将深入Tomcat启动过程的源码分析来验证我们的类加载器机制

目录
相关文章
|
1月前
|
Java 应用服务中间件
面对海量网络请求,Tomcat线程池如何进行扩展?
【10月更文挑战第4天】本文详细探讨了Tomcat线程池相较于标准Java实用工具包(JUC)线程池的关键改进。首先,Tomcat线程池在启动时即预先创建全部核心线程,以应对启动初期的高并发请求。其次,通过重写阻塞队列的入队逻辑,Tomcat能够在任务数超过当前线程数但未达最大线程数时,及时创建非核心线程,而非等到队列满才行动。此外,Tomcat还引入了在拒绝策略触发后重新尝试入队的机制,以提高吞吐量。这些优化使得Tomcat线程池更适应IO密集型任务,有效提升了性能。
面对海量网络请求,Tomcat线程池如何进行扩展?
|
1月前
|
网络协议 Java 应用服务中间件
深入浅出Tomcat网络通信的高并发处理机制
【10月更文挑战第3天】本文详细解析了Tomcat在处理高并发网络请求时的机制,重点关注了其三种不同的IO模型:NioEndPoint、Nio2EndPoint 和 AprEndPoint。NioEndPoint 采用多路复用模型,通过 Acceptor 接收连接、Poller 监听事件及 Executor 处理请求;Nio2EndPoint 则使用 AIO 异步模型,通过回调函数处理连接和数据就绪事件;AprEndPoint 通过 JNI 调用本地库实现高性能,但已在 Tomcat 10 中弃用
深入浅出Tomcat网络通信的高并发处理机制
|
1月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
103 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
1月前
|
Java 应用服务中间件 Apache
浅谈Tomcat和其他WEB容器的区别
Tomcat是一款轻量级的免费开源Web应用服务器,常用于中小型系统及并发访问量适中的场景,尤其适合开发和调试JSP程序。它不仅能处理HTML页面,还充当Servlet和JSP容器。相比之下,物理服务器是指具备处理器、硬盘等硬件设施的服务器,如云服务器,其设计目标是在处理能力、稳定性和安全性等方面提供高标准服务。简言之,Tomcat专注于运行Java应用,而物理服务器则提供基础计算资源。
|
2月前
|
设计模式 人工智能 安全
【Tomcat源码分析】生命周期机制 Lifecycle
Tomcat内部通过各种组件协同工作,构建了一个复杂的Web服务器架构。其中,`Lifecycle`机制作为核心,管理组件从创建到销毁的整个生命周期。本文详细解析了Lifecycle的工作原理及其方法,如初始化、启动、停止和销毁等关键步骤,并展示了LifecycleBase类如何通过状态机和模板模式实现这一过程。通过深入理解Lifecycle,我们可以更好地掌握组件生命周期管理,提升系统设计能力。欢迎关注【码上遇见你】获取更多信息,或搜索【AI贝塔】体验免费的Chat GPT。希望本章内容对你有所帮助。
|
4月前
|
存储 安全 Java
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
109 13
|
4月前
|
弹性计算 运维 应用服务中间件
容器的优势,在Docker中运行Tomcat
摘要:了解Docker与虚拟机的区别:虚拟机使用Hypervisor创建完整操作系统,而容器通过namespace和cgroup实现轻量级隔离,共享主机内核。Docker启动快、资源利用率高,适合快速部署和跨平台移植。但安全性相对较低。示例介绍了如何通过Docker搜索、拉取官方Tomcat镜像并运行容器,最后验证Tomcat服务的正常运行。
|
3月前
|
Kubernetes 网络协议 Linux
容器跨主机通信:Flannel网络实现机制分析(二)
容器跨主机通信:Flannel网络实现机制分析(二)
61 0
|
3月前
|
存储 Linux 数据中心
容器跨主机通信:Flannel网络实现机制分析(一)
容器跨主机通信:Flannel网络实现机制分析(一)
133 0
|
4月前
|
监控 Java 调度
Java面试题:描述Java线程池的概念、用途及常见的线程池类型。介绍一下Java中的线程池有哪些优缺点
Java面试题:描述Java线程池的概念、用途及常见的线程池类型。介绍一下Java中的线程池有哪些优缺点
72 1