Tomcat
开篇:Java Web 的"发动机"
如果说 Nginx 是互联网的"前台接待员",那 Tomcat 就是后厨的"大厨" -- 它负责接收请求、执行 Java 代码、返回结果。Tomcat 是一个开源的 Java Servlet 容器和 JSP 引擎,几乎所有 Java Web 应用的背后都有它的身影。
在实际架构中,Tomcat 通常和 Nginx 配合使用:Nginx 处理静态资源和负载均衡,把动态请求(Servlet、JSP)转发给 Tomcat 处理。两者通过 AJP 或 HTTP 协议通信。
Tomcat 架构
Tomcat 的架构是层层嵌套的容器结构,从外到内依次是 Server、Service、Connector、Engine、Host、Context。
| 组件 | 职责 | 类比 |
|---|---|---|
| Server | 整个 Tomcat 实例 | 一栋大楼 |
| Service | 将 Connector 和 Engine 关联起来 | 一层楼 |
| Connector | 监听端口,接收请求 | 大楼的门 |
| Engine | 请求处理引擎 | 楼层总管 |
| Host | 虚拟主机,支持多域名 | 一个公司 |
| Context | 一个 Web 应用 | 一个部门 |
| Wrapper | 封装具体的 Servlet | 一个员工 |
启动流程
Tomcat 的启动入口是 Bootstrap 类,主要分三步:
- init:加载启动类加载器,创建 Catalina 对象
- load:加载
server.xml和web.xml,依次初始化 Server -> Service -> Connector -> Engine -> Host -> Context - start:按顺序启动所有组件,扫描 webapps 目录自动部署应用
启动完成后,有请求到达时,根据 URL 匹配对应的 Context(应用),转发到对应的 Servlet 处理。
类加载器 -- 打破双亲委派
Tomcat 的类加载机制是面试高频考点。它没有严格遵守双亲委派,而是采用了层次化的类加载器结构,目的是实现多个 Web 应用之间的类隔离。
为什么要打破双亲委派
一个 Tomcat 可以同时跑多个应用,不同应用可能依赖同一个类库的不同版本(比如应用 A 用 Spring 5,应用 B 用 Spring 6)。如果严格遵守双亲委派,相同全路径名的类只能加载一次,就会导致版本冲突。
所以 Tomcat 给每个应用分配独立的 WebappClassLoader,各自加载各自的 Jar 包,互不干扰。
类加载器层级
| 类加载器 | 加载范围 | 说明 |
|---|---|---|
| Bootstrap | JRE 核心类(java.lang 等) | JVM 内置 |
| System | Tomcat 启动类(bootstrap.jar 等) | $CATALINA_HOME/bin |
| Common | Tomcat 公共类 | $CATALINA_HOME/lib |
| WebApp | 应用自己的类和 Jar | WEB-INF/classes 和 WEB-INF/lib |
加载顺序(关键)
Tomcat 中有一个 delegate 配置(默认 false),它决定了类加载的顺序:
delegate=false(默认)-- 打破双亲委派:
Bootstrap → WebApp → System → Common先让 Bootstrap 加载(避免覆盖 JRE 核心类如 String),然后 WebApp 自己尝试加载,加载不到再委托给上层。
delegate=true -- 严格双亲委派:
Bootstrap → System → Common → WebApp严格按照"先问爸爸"的双亲委派顺序。
// WebappClassLoaderBase.loadClass 核心逻辑(精简版)
public Class<?> loadClass(String name) {
// 1. 检查缓存
Class<?> clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// 2. 先让 Bootstrap 加载(保护 JRE 核心类)
clazz = javaseLoader.loadClass(name);
if (clazz != null) return clazz;
// 3. 根据 delegate 决定顺序
if (delegate) {
// delegate=true: 先委托父加载器
clazz = parent.loadClass(name);
if (clazz != null) return clazz;
// 再自己加载
clazz = findClass(name);
} else {
// delegate=false: 先自己加载
clazz = findClass(name);
if (clazz == null) {
// 自己加载不到再委托父加载器
clazz = parent.loadClass(name);
}
}
return clazz;
}可选的 SharedClassLoader 可以让多个应用共享公共 Jar(如统一版本的 Spring),减少重复加载。需要在
conf/catalina.properties中配置shared.loader。
请求处理流程
Tomcat 处理一个 HTTP 请求的完整流程:
- 接收请求:Connector 监听端口,接收 HTTP 请求
- 请求解析:解析请求方法、URL、请求头、参数等
- Servlet 查找:根据 URL 找到对应的 Servlet
- Servlet 处理:执行
service()方法处理业务逻辑 - 返回响应:把处理结果发送给客户端
Servlet 生命周期
每个 Servlet 经历三个阶段:
| 阶段 | 方法 | 时机 | 说明 |
|---|---|---|---|
| 初始化 | init() | 首次请求或容器启动时 | 只执行一次,读配置、连数据库 |
| 处理请求 | service() | 每次请求 | 根据请求方法调用 doGet/doPost |
| 销毁 | destroy() | 容器关闭或应用卸载时 | 只执行一次,释放资源 |
IO 模型(BIO / NIO / APR)
Tomcat 的 Connector 支持多种 IO 模型,不同版本默认值不同:
| IO 模型 | 特点 | protocol 配置 |
|---|---|---|
| BIO | 每连接一线程,阻塞式,并发高时线程爆炸 | HTTP/1.1(旧版默认) |
| NIO | 多路复用,一个线程处理多个连接 | Http11NioProtocol(新版默认) |
| NIO2 | 基于 AIO,异步非阻塞 | Http11Nio2Protocol |
| APR | 调用本地操作系统 IO,性能最强 | Http11AprProtocol(需安装 APR 库) |
<!-- server.xml 中配置 IO 模型 -->
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443" />启动日志可以看到当前使用的模型:
Starting ProtocolHandler ["http-nio-8080"]一般建议使用 NIO 或 NIO2。对性能有极致要求的场景可以考虑 APR,但需要安装和配置额外的库。
为什么默认最大线程数是 200
Tomcat 默认最大线程数 200,核心线程数 10。很多人质疑"不是说线程池建议 2N 或 N+1 吗",但注意那说的是核心线程数,不是最大线程数。
200 是 Tomcat 团队反复测试验证的结果,原因包括:
- 处理短连接:Web 请求处理时间短,每条请求不会占用线程太久
- I/O 密集型:等待数据库、网络调用时线程处于空闲,200 个线程可以充分利用 CPU
- 高并发需求:Web 服务器需要同时处理大量请求
- 内部优化:可回收线程、无锁化算法等策略降低了线程开销
实际工作中不要教条地按公式配,而是根据业务场景充分压测后合理配置。
Filter vs Interceptor
在 Spring MVC 应用中,过滤器(Filter)和拦截器(Interceptor)都能对请求做预处理,经常被放在一起比较。核心区别在于生效的位置不同。
| 对比项 | Filter | Interceptor |
|---|---|---|
| 规范 | Servlet 规范 | Spring 框架 |
| 生效位置 | 请求进入 Servlet 容器之前 | 进入 Servlet 容器之后,Controller 之前 |
| 实现方式 | 实现 javax.servlet.Filter | 实现 HandlerInterceptor |
| 配置方式 | web.xml 或 @WebFilter | @Configuration + WebMvcConfigurer |
| 能力范围 | 只能获取 request/response | 可以获取 Handler、ModelAndView |
| 典型用途 | 编码转换、安全验证、日志 | AOP、权限控制、事务管理 |
Filter 示例:
public class LoginFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
// 前置处理:登录验证
chain.doFilter(req, resp);
// 后置处理
}
}Interceptor 示例:
@Component
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// Controller 执行前
return true; // true=放行, false=拦截
}
public void postHandle(...) {
// Controller 执行后,视图渲染前
}
public void afterCompletion(...) {
// 请求完成后,用于清理资源
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/**");
}
}一句话总结:Filter 管大门(Servlet 容器入口),Interceptor 管内部(Controller 前后)。
面试高频问答
Q1: Tomcat 的类加载机制是怎样的?为什么要打破双亲委派?
关键词:WebappClassLoader、delegate、类隔离
Tomcat 默认情况下先让 Bootstrap 加载(保护 JRE 核心类),然后 WebappClassLoader 自己加载,加载不到再委托上层(顺序:Bootstrap -> WebApp -> System -> Common)。打破双亲委派的原因是:一个 Tomcat 跑多个应用,不同应用可能依赖同名类库的不同版本,严格双亲委派会导致同一个类只能加载一次,造成版本冲突。通过 delegate=true 可以切换回严格双亲委派。
Q2: Tomcat 处理请求的完整流程?
关键词:Connector -> Engine -> Host -> Context -> Servlet
Connector 监听端口接收请求并解析,然后转发给 Engine。Engine 根据域名匹配 Host,Host 根据路径匹配 Context(应用),Context 找到对应的 Servlet 执行 service() 方法处理业务逻辑,最后将响应返回客户端。Servlet 生命周期包括 init(初始化,执行一次)、service(每次请求)、destroy(销毁,执行一次)。
Q3: Filter 和 Interceptor 的区别?
关键词:Servlet 规范 vs Spring 框架、生效位置不同
Filter 基于 Servlet 规范,在请求进入 Servlet 容器之前拦截,适合做编码转换、安全验证等通用处理。Interceptor 基于 Spring 框架,在进入 Servlet 容器之后、Controller 之前拦截,可以获取 Handler 和 ModelAndView,适合做权限控制、AOP 等更精细的处理。简单说:Filter 管大门,Interceptor 管内部。
Q4: Tomcat 支持哪些 IO 模型?为什么默认线程数是 200?
关键词:BIO/NIO/NIO2/APR、I/O 密集型
Tomcat 支持 BIO(阻塞式,每连接一线程)、NIO(多路复用,新版默认)、NIO2(异步)和 APR(本地系统 IO,性能最强)。默认最大线程数 200 是因为 Web 请求属于 I/O 密集型(大量时间在等待数据库和网络),200 个线程可以在等待 IO 时充分利用 CPU。实际生产应根据业务场景压测后调整。
小结
Tomcat 是 Java Web 的核心基础设施。架构上理解 Server -> Service -> Connector + Engine -> Host -> Context 的层级关系。类加载上理解为什么要打破双亲委派(多应用类隔离)以及 delegate 参数的作用。请求处理上理解 Servlet 生命周期和 IO 模型的选择。最后区分 Filter 和 Interceptor 的作用位置。把这四个知识点串起来,Tomcat 相关的面试题基本就能覆盖了。