Skip to content

Tomcat介绍

Tomcat,全称是 Apache Tomcat,是 Apache 软件基金会下的一个核心项目。它是一个开源、免费的 Java Web 应用服务器。

从技术上讲,Tomcat 主要实现了两个核心的 Java 规范:

  1. Java Servlet:允许开发者编写Java代码来处理客户端请求并生成动态响应。

  2. JavaServer Pages (JSP):允许将Java代码嵌入到HTML页面中,使得页面内容的生成更加方便。

简单来说,当你用 Java 编写了一个网站或一个 Web 服务后,你需要一个软件来运行它,让用户可以通过浏览器访问。Tomcat 就是这样一个运行环境。它接收来自用户的 HTTP 请求(比如访问一个网址),然后将这些请求交给你的 Java Web 应用程序进行处理,最后将应用程序生成的响应(比如一个网页)返回给用户。

Tomcat 的使用场景

Tomcat 的主要目的就是为基于 Java 的 Web 应用程序提供一个可靠、高效的运行环境。

它的核心使用场景包括:

  1. 运行动态网站:对于需要与用户交互、从数据库读取数据、根据不同条件显示不同内容的网站(例如电商网站、社交网络、博客系统),Tomcat 是一个非常流行的选择。

  2. 提供 API 服务:现代应用架构中,前后端分离非常普遍。后端通常会提供一套 RESTful API 接口供前端(网页、手机 App)调用。这些后端的 API 服务就可以部署在 Tomcat 中。

  3. 作为 Servlet 容器:许多 Java Web 框架(如 Spring MVC)本身并不处理网络通信,它们需要一个像 Tomcat 这样的“容器”来管理 Servlet 的生命周期并处理 HTTP 请求。

Tomcat 和 Spring 的区别

  • Tomcat 是一个服务器/容器(Container)

    • 它是一个具体的、可运行的软件。

    • 它的职责是提供一个符合 Java EE Web 规范的环境,用来承载和管理你的 Web 应用程序。

    • 它关心的是底层的网络通信、HTTP 协议的解析、Servlet 的生命周期管理等基础设施层面的问题。

  • Spring 是一个应用程序框架(Framework)

    • 它不是一个能直接运行的软件,而是一套代码库(JAR 包的集合)和一种开发应用的理念。

    • 它的目的是简化企业级 Java 应用的开发。它提供了诸如依赖注入(DI)、面向切面编程(AOP)、事务管理、数据访问等一系列功能,让开发者可以更专注于业务逻辑的实现,而不用处理大量重复的、底层的代码。

Tomcat 和 Spring 并不冲突,而是合作关系。它们是“服务器”和“在服务器上运行的程序”之间的关系。

  • 一个用 Spring 框架开发好的 Web 应用程序,最终需要被打包(例如打成一个 WAR 包)。

  • 然后,这个包会被部署到 Tomcat 服务器上运行。

  • Tomcat 负责接收请求,然后把请求转交给 Spring 框架控制的程序代码来处理。

现在流行的 Spring Boot 框架为了简化部署,会直接将 Tomcat “内嵌”到应用程序中。当你启动一个 Spring Boot 应用时,它会自动在内部启动一个 Tomcat 服务器,这样就不需要再单独安装和配置 Tomcat 了。

Tomcat 为什么主动破坏双亲委派机制

  • 双亲委派机制(Parents Delegation Model):当一个类加载器收到加载类的请求时,它不会自己先去尝试加载,而是把这个请求委托给它的父加载器去做。每一层加载器都是如此,一直委托到最顶层的启动类加载器(Bootstrap ClassLoader)。只有当父加载器无法完成加载请求时(在它的搜索路径下找不到所需的类),子加载器才会自己尝试去加载。

  • 目的:1. 避免类的重复加载。 2. 保证 Java 核心库的类型安全(例如,确保用户自己写的 java.lang.String 不会被加载,防止核心 API 被篡改)。

Tomcat 破坏这个机制,主要是为了解决以下几个在 Web 服务器场景下非常重要的问题:

  1. 应用隔离(Isolation):

    一个 Tomcat 服务器可以同时运行多个 Web 应用程序。如果这些应用都严格遵循双亲委派,那么它们都会共享父加载器(如 CommonClassLoader)加载的类。

    • 问题场景:假设应用 A 需要 log4j-1.2.jar,而应用 B 需要 log4j-2.0.jar。这两个版本是不兼容的。如果都遵循双亲委派,那么哪个 log4j 包先被加载,就会被所有应用共享,导致后一个加载的应用因类方法不兼容而崩溃。

    • Tomcat 的解决方案:为每个 Web 应用创建一个独立的类加载器(WebAppClassLoader)。当这个加载器加载类时,它会优先在自己的应用目录(WEB-INF/classesWEB-INF/lib)下查找,而不是先委托给父加载器。只有在自己的目录找不到时,才会委托给父加载器。这样,应用 A 加载自己的 log4j-1.2,应用 B 加载自己的 log4j-2.0,它们互不干扰,实现了隔离。

  2. 热部署(Hot Deployment)和热加载(Hot Reloading):

    Tomcat 允许你在不重启整个服务器的情况下,更新某个 Web 应用。

    • 实现原理:要实现热部署,就需要卸载掉旧应用的所有类,然后加载新应用的类。Java 的类加载机制规定,类只能由加载它的那个类加载器来卸载。如果应用的所有类都由共享的父加载器加载,那么就无法单独卸载某个应用对应的类,因为其他应用可能还在使用这个父加载器。

    • Tomcat 的解决方案:通过为每个应用创建独立的 WebAppClassLoader,当需要重新部署应用时,Tomcat 只需要丢弃掉这个应用对应的 WebAppClassLoader 实例即可。这样,由它加载的所有类都会被 JVM 的垃圾回收机制回收掉。然后,Tomcat 会为新部署的应用创建一个全新的 WebAppClassLoader 实例。

总结来说,Tomcat 破坏双亲委派机制,是为了实现 Web 应用之间的类库隔离灵活的热部署功能,这对于一个需要稳定运行且管理多个应用的服务器来说是至关重要的。它并非完全颠覆,只是在加载应用私有类时调整了委托顺序(先自己找,再向上委托),而对于 Java 核心类库(如 java.lang.Object),它依然会委托给父加载器,以保证 JVM 的正常运行。

其他打破双亲委派机制的例子

除了 Tomcat,还有其他一些技术和场景也打破了双亲委派机制,通常都是为了实现模块化、热插拔或者SPI(Service Provider Interface)等功能。

  1. JDBC 驱动加载:

    JDBC 的规范由 Java 核心库(rt.jar)定义,例如 java.sql.DriverManager 类,它由启动类加载器加载。但是,具体的数据库驱动(如 MySQL、Oracle 的驱动 JAR 包)是由各个厂商实现的,通常放在应用的 classpath 下,由应用类加载器加载。根据双亲委派,DriverManager 是无法看到并加载由其子加载器管理的驱动类的。为了解决这个问题,JDBC 使用了 ServiceLoader (SPI) 机制。DriverManager 会使用当前线程的上下文类加载器(Thread Context ClassLoader)去扫描和加载 classpath 中的所有 JDBC 驱动实现,这实际上就是一种父加载器“反向”请求子加载器去工作的行为,打破了自下而上的委托模型。

  2. JNDI(Java Naming and Directory Interface):

    JNDI 的核心代码也由启动类加载器加载,但其服务提供者(SPI)的实现则可能由第三方厂商提供,并通过 jndi.properties 文件来配置。JNDI 的服务管理器也是利用线程上下文类加载器去加载这些服务提供者,逻辑与 JDBC 类似。

  3. OSGi (Open Services Gateway initiative):

    OSGi 是一个动态模块化系统规范。在 OSGi 环境中,每个模块(Bundle)都有自己的类加载器。类加载不再是简单的树状父子结构,而是一个复杂的网状结构。一个 Bundle 可以声明它依赖哪些其他的 Bundle 的某些包,OSGi 框架会根据这些依赖关系来决定类加载的委托路径。这种机制远比双亲委派复杂和灵活,是实现真正模块化和热插拔的关键。