简介:Apache Tomcat 6.0.29 是一个实现 Java Servlet 和 JSP 规范的开源应用服务器,广泛用于部署和运行 Java Web 应用。该版本支持 Servlet 容器、JSP 2.1、Java EL、JSTL、多连接器模式及热部署等核心功能,具备良好的开发与部署能力。尽管已停止维护,不推荐用于现代生产环境,但其架构设计对学习 Web 服务器原理和旧项目维护仍具参考价值。本文深入解析其目录结构、配置方式与关键特性,帮助开发者理解 Tomcat 的运行机制,并为迁移至新版提供基础。
Tomcat 6.0.29 深度解析:从容器架构到 JSP 执行机制的全链路拆解
在今天这个 Spring Boot 和云原生满天飞的时代,还有人关心 Tomcat 6 吗?🤔
说实话,刚看到这个问题的时候我也忍不住笑了——这不就是“考古”嘛!但转念一想,很多企业级系统还在跑着十几年前的老应用,而这些系统的命脉,恰恰就系于像 Tomcat 6.0.29 这样的经典版本之上。💡
别急着划走!你以为它过时了?其实不然。理解 Tomcat 6 不仅能帮你搞定那些“祖传代码”的维护难题,更能让你真正看懂现代 Web 容器的设计源头。毕竟,现在的异步处理、NIO优化、类加载隔离……哪一项不是从这里一步步演化出来的?
所以,咱们今天不搞教科书式的复读,而是带你 深入骨髓地扒一遍 Tomcat 6.0.29 的内核逻辑 ,看看它是如何把一个 HTTP 请求“嚼碎”再吐出 HTML 响应的全过程。准备好了吗?🚀
🧩 当年那个稳如老狗的 Servlet 容器长啥样?
先来点历史背景提神醒脑——Tomcat 6.0.29 发布于 2010 年 ,是 Tomcat 6 系列的一个稳定维护版。它基于 Servlet 2.5 + JSP 2.1 规范 构建,正好踩在 Java EE 5 的肩膀上。
那时候,EJB 还没彻底凉透,Struts 仍是主流框架,Spring 刚开始崭露头角……而 Tomcat,作为轻量级 Web 容器的代表,已经悄悄成为无数企业项目的首选运行环境。
它的最大优势是什么?两个字: 稳定 !
虽然没有后来 Tomcat 7/8 那些炫酷的异步支持和 NIO2 特性,但它胜在简单、清晰、可靠。尤其适合那种“上线后五年不动”的生产系统。🛠️
也正因如此,直到今天,仍有大量金融、政务、制造行业的遗留系统依赖着它。你可能觉得不可思议,但现实就是: 有些银行的核心交易系统至今仍在用 JDK 1.6 + Tomcat 6 跑得稳稳当当 。😅
所以啊,别小瞧了这位“老前辈”。读懂它,不只是为了修 Bug,更是为了理解整个 Java Web 技术栈的演进脉络。
🔁 一次请求到底经历了什么?全流程透视
想象一下:用户在浏览器敲下 http://localhost:8080/myapp/index.jsp ——接下来发生了什么?
很多人会说:“哦,不就是找文件然后返回嘛。” 可真相远比这复杂得多。整个过程涉及多个组件协作,堪称一场精密的交响乐演奏。🎻
我们把它拆成几个关键阶段来看:
1️⃣ 接入层:Connector 如何“接住”网络流量?
一切始于 Connector 组件。它是 Tomcat 的“守门人”,负责监听端口(比如默认的 8080),接收 TCP 连接,并完成 HTTP 协议解析。
graph TD
A[HTTP Request] --> B{Connector 接收}
B --> C[解析为 Request/Response]
C --> D[传递给 Engine]
D --> E[匹配 Host]
E --> F[定位 Context]
F --> G[调用 Wrapper 所管理的 Servlet]
G --> H[执行 service() 方法]
H --> I[生成响应输出]
I --> J[Response 返回客户端]
你看,从原始字节流到最终 HTML 输出,中间要经过层层递进的处理流程。而第一步,就是由 Connector 把裸数据包装成标准的 HttpServletRequest 和 HttpServletResponse 对象。
小贴士:你可以把 Connector 想象成快递分拣中心。不管包裹来自哪个城市(IP)、用了哪种运输方式(协议),它都要统一拆包、登记、贴标签,然后交给下一个环节。
2️⃣ 路由层:Engine → Host → Context → Wrapper,谁说了算?
Tomcat 使用一套树形结构的 Container 层级来进行请求路由。这套设计非常精巧,体现了典型的“组合模式”。
四层嵌套结构一览:
| 层级 | 作用 |
|---|---|
| Engine | 整个 Catalina 引擎的根容器,通常一个 JVM 只有一个 |
| Host | 虚拟主机,支持多域名部署(如 www.a.com vs www.b.com) |
| Context | Web 应用上下文,对应 WAR 包或目录 |
| Wrapper | 最小单位,封装单个 Servlet 实例 |
举个例子:
- 用户访问 http://www.myshop.com/admin/login
- Tomcat 先通过 Host 匹配到 myshop.com ;
- 再根据 Context 找到 /admin 这个 Web 应用;
- 最后由 Wrapper 调用对应的 LoginServlet。
这种分层机制不仅实现了灵活的虚拟主机和多应用共存,还让每个层级都能独立配置资源、安全策略和生命周期行为。
来看看 XML 配置长什么样:
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="/myapp" docBase="/opt/apps/myapp" reloadable="true">
<Wrapper>
<!-- 实际上 Wrapper 是自动创建的 -->
</Wrapper>
</Context>
</Host>
</Engine>
注意到没?Wrapper 并不需要手动写出来,Tomcat 在部署时会自动为每个 Servlet 创建一个 Wrapper 实例。
而且,每一层都可以挂载自己的 Valve(阀门) ,用来实现日志记录、权限检查、性能监控等功能。是不是有点像“拦截器”或者“过滤器链”的感觉?👏
没错!Valve 就是 Tomcat 内部的责任链模式实现。比如你在 Host 上加了个 AccessLogValve ,那所有进入该站点的请求都会被记录下来。
3️⃣ 执行层:Wrapper 是怎么启动 Servlet 的?
终于到了最核心的部分: Servlet 的实例化与调用 。
还记得上面提到的 StandardWrapperValve 吗?它是 Wrapper 默认的处理阀,职责就是触发真正的业务逻辑执行。
public final class StandardWrapperValve extends ValveBase {
public void invoke(Request request, Response response) throws IOException, ServletException {
Wrapper wrapper = (Wrapper) getContainer();
Servlet servlet = null;
try {
// 加载并初始化 Servlet
servlet = wrapper.allocate();
HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
HttpServletResponse hres = (HttpServletResponse) response.getResponse();
// 构建 Filter 链并执行
FilterChain chain = getFilterChain(request, servlet);
chain.doFilter(hreq, hres);
} catch (UnavailableException e) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getMessage());
} finally {
if (servlet != null)
wrapper.deallocate(servlet); // 释放引用
}
}
}
这段代码看似平平无奇,实则暗藏玄机:
-
wrapper.allocate():如果 Servlet 还没初始化,就会调用其init()方法; -
getFilterChain():根据 web.xml 或注解构建完整的过滤器链; -
chain.doFilter():启动链条式调用,最后才真正进入Servlet.service(); -
deallocate():防止内存泄漏,及时回收实例引用。
⚠️ 注意:这一切都发生在 同一个请求线程中 !这意味着如果你在 Servlet 里写了死循环或者长时间阻塞操作,整个线程就会卡住,影响并发能力。
这也是为什么后来出现了 AsyncContext 和 NIO 改进的原因之一。
🧱 类加载机制:为何两个应用能同时用不同版本的 Spring?
这是 Tomcat 最牛的地方之一: Web 应用之间的类隔离 。
你想啊,如果有两个 WAR 包,一个用 Spring 3.x,另一个用 Spring 5.x,它们会不会打架?按理说会,但在 Tomcat 里不会!🎯
秘密就在于它的定制类加载器体系。
JVM 默认的双亲委派模型 vs Tomcat 的“叛逆”做法
JVM 原生的类加载顺序是这样的:
Bootstrap ClassLoader
↓
Extension ClassLoader
↓
System ClassLoader (AppClassLoader)
遵循“父优先”原则——子加载器找不到类时才会向上委托。
但 Tomcat 不这么干!它引入了新的加载器:
- CommonClassLoader :加载
$CATALINA_HOME/lib下的公共库(如 catalina.jar) - WebAppClassLoader :每个 Web 应用独享一个,加载
/WEB-INF/classes和/WEB-INF/lib/*.jar
最关键的是: WebAppClassLoader 优先本地查找,失败后再委托给 CommonClassLoader 。
这就叫“打破双亲委派”!
public class WebappClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
try {
// 👇 先自己找!这才是重点
clazz = findClass(name);
} catch (ClassNotFoundException e) {
// ❌ 自己找不到,才让老爸帮忙
clazz = super.loadClass(name, resolve);
}
}
if (resolve) resolveClass(clazz);
return clazz;
}
}
}
这样一来,即使两个应用都有 com.example.Util 类,也会被分别加载成不同的 Class 对象,互不影响。
🧠 举个生活化的比喻:就像是两家兄弟公司共用总公司的大楼(Tomcat),但他们各自的财务系统(类路径)完全独立,账本也不会混在一起。
当然,这也带来一个问题:万一某个第三方库在线程中持有静态引用怎么办?可能导致 WebAppClassLoader 泄漏,无法被 GC 回收!
为此,Tomcat 提供了清理机制,比如:
<Context clearReferencesStopThreads="true"
clearReferencesRmiTargets="true">
</Context>
这些选项会在应用卸载时主动清除顽固引用,避免“僵尸类加载器”长期驻留内存。
💬 JSP 是怎么变成 Servlet 的?揭秘 Jasper 编译引擎
现在我们聊聊 JSP。很多人以为它是“动态页面模板”,但其实它背后完全是 Servlet 在干活。
当你第一次访问 index.jsp 时,Tomcat 会做一件事: 把它编译成 Java 源码,再编译成 .class 文件 。整个过程由内置的 Jasper 引擎 完成。
四步走战略:从 .jsp 到可执行 Servlet
-
解析文档结构
Jasper 扫描.jsp文件,识别出 HTML、脚本片段<% %>、表达式<%= %>、声明<%! %>等元素,构建成抽象语法树(AST)。 -
生成 Java 源码
根据 AST 输出.java文件。规则很简单:
- 静态内容 →out.print("...")
- 表达式${name}→out.print(String.valueOf(name))
- 声明<%! int count; %>→ 成员变量定义 -
调用 javac 编译成 class
Tomcat 调用系统javac或 Eclipse JDT 编译器生成字节码。 -
加载并实例化 Servlet
使用JasperLoader加载.class,反射创建对象,纳入容器管理。
整个流程可以用这张图概括:
graph TD
A[HTTP 请求到达 index.jsp] --> B{是否已编译?}
B -- 否 --> C[解析 JSP 文本生成 AST]
C --> D[生成 Java 源码 .java 文件]
D --> E[调用 javac 编译为 .class]
E --> F[使用 JasperLoader 加载类]
F --> G[初始化 Servlet 实例]
G --> H[_jspInit() 调用]
H --> I[执行 _jspService()]
B -- 是 --> I
I --> J[写入 Response 输出流]
有趣的是,这个 .java 文件会被保存在 $CATALINA_HOME/work/Catalina/... 目录下,俗称“work 目录”。你可以进去翻翻看,说不定还能找到线上某个神秘报错的真实原因呢!🔍
✅ 调试技巧:遇到 JSP 报错时,直接打开对应的
.java文件,搜索out.print就能快速定位问题行。
⚙️ JSP 核心方法三剑客:_jspInit / _jspService / _jspDestroy
每个 JSP 编译后的 Servlet 都自带三个生命周期方法:
_jspInit() :初始化资源池
主要用于预加载一些高频使用的对象,比如:
public void _jspInit() {
_jspx_tagPool_c_if_test = org.apache.taglib.c.IfTag.getTagPool("c:if", "test");
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
}
看到了吗?连 JSTL 的 <c:if> 都做了池化处理,减少频繁创建开销。这就是性能优化的小细节!
如果你想加自己的初始化逻辑,可以用这种方式:
<%!
private DataSource ds;
public void jspInit() {
ServletContext ctx = getServletContext();
ds = (DataSource) ctx.getAttribute("dataSource");
}
%>
注意哦,这里是 jspInit() ,不是 _jspInit() 。前者是你写的钩子,后者是自动生成的方法。
_jspService() :主战场,拼接 HTML 输出
这才是真正的执行体。所有静态 HTML 都变成了 out.write() ,动态部分则是 out.print() 。
out.write("<html><body>\n");
out.write(" <h1>Hello ");
out.print( String.valueOf(request.getParameter("name")) );
out.write("</h1>\n");
虽然效率不错,但一旦逻辑复杂起来,代码可读性就惨不忍睹了。这也是为什么后来 MVC 框架强调“视图纯净化”的原因。
建议:尽量少用 <% %> 脚本,多用 EL + JSTL。
_jspDestroy() :善后工作不能少
默认为空,但可以手动释放资源:
<%!
public void jspDestroy() {
if (_jspx_tagPool_c_if_test != null) {
_jspx_tagPool_c_if_test.clear();
}
}
%>
适用于关闭数据库连接、注销 MBean、清理缓存等场景。
🔍 JSP 2.1 的高级特性,你真的用对了吗?
Tomcat 6 支持的是 JSP 2.1 规范,相比早期版本有不少提升。下面我们挑几个实用功能讲讲。
✅ EL 表达式:告别繁琐的 <%= %>
以前写:
<% out.print(user.getName()); %>
现在可以直接写:
${user.name}
清爽多了吧?😎
EL 支持的操作包括:
| 类型 | 示例 |
|---|---|
| 属性访问 | ${user.address.city} |
| 集合取值 | ${list[0]} , ${map['key']} |
| 运算符 | ${a + b} , ${empty obj} |
| 隐式对象 | ${param.name} , ${header.User-Agent} |
还可以通过 web.xml 控制是否启用:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-enabled>false</el-enabled>
</jsp-property-group>
</jsp-config>
或者在页面局部关闭:
<%@ page isELIgnored="true" %>
✅ SimpleTag:更简洁的自定义标签开发
相比老式的 TagSupport , SimpleTag 接口简单太多了:
public class IfTag implements SimpleTag {
private boolean test;
private JspFragment body;
public void setTest(boolean test) { this.test = test; }
@Override
public void doTag() throws JspException, IOException {
if (test && body != null) {
body.invoke(null); // 执行标签体
}
}
}
只需要重写 doTag() ,无需状态机判断,直观又安全。
再加上 .tag 文件的支持,前端工程师也能参与标签开发了:
<!-- /WEB-INF/tags/greet.tag -->
<%@ tag body-content="empty" %>
<%@ attribute name="name" required="true" %>
Hello, ${name}!
使用起来就像普通标签一样:
<my:greet name="${param.user}" />
大大降低了 UI 与后端协作的成本。
🚀 性能优化实战:如何让 JSP 不再拖后腿?
JSP 很方便,但也容易成为性能瓶颈。以下是几条硬核建议:
✅ 预编译所有 JSP 页面
首次访问慢?那是正在编译!解决办法是在构建阶段就提前编译好。
Maven 插件示例:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jspc-maven-plugin</artifactId>
<version>2.0-alpha-3</version>
<executions>
<execution>
<goals><goal>compile</goal></goals>
</execution>
</executions>
</plugin>
执行 mvn jspc:compile 后,所有 .jsp 都会被转成 .java 并参与主流程编译。
效果: 首次访问延迟归零 ,用户体验飙升!📈
✅ 关闭开发模式,禁止热重载
生产环境一定要关掉自动重编译:
<servlet>
<servlet-name>jsp</servlet-name>
<init-param>
<param-name>development</param-name>
<param-value>false</param-value>
</init-param>
</servlet>
否则每次修改都会触发类重新加载,轻则性能下降,重则 PermGen/Metaspace 溢出。
✅ 减少脚本片段,推动 MVC 分离
别再写这种代码了:
<%
List<User> users = (List<User>)request.getAttribute("users");
for(User u : users) {
%>
<div><%= u.getName() %></div>
<% } %>
换成 JSTL:
<c:forEach items="${users}" var="user">
<div>${user.name}</div>
</c:forEach>
好处多多:逻辑清晰、支持缓存、便于国际化、利于团队协作。
理想状态下,JSP 应该只是一个“纯渲染模板”,不该掺杂任何业务判断。
🔌 连接器 BIO vs NIO:高并发下的生死抉择
最后聊聊性能大头: Connector 配置 。
Tomcat 6 支持两种 I/O 模型:
| 模式 | 类名 | 特点 |
|---|---|---|
| BIO | Http11Protocol | 每请求一线程,简单但耗资源 |
| NIO | Http11NioProtocol | 多路复用,省线程,适合高并发 |
默认是 BIO,如果你想开启 NIO,必须显式指定:
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="500"
acceptCount="200"
connectionTimeout="10000"
keepAliveTimeout="3000"
enableLookups="false"
compression="on"/>
来看看压测数据对比(JMeter 结果摘录):
| 并发数 | 模式 | 平均响应时间(ms) | 吞吐量(req/sec) | 错误率(%) | 内存占用(MB) | 线程数 |
|---|---|---|---|---|---|---|
| 100 | BIO | 45 | 180 | 0 | 420 | 180 |
| 100 | NIO | 38 | 210 | 0 | 360 | 90 |
| 300 | BIO | 110 | 130 | 3.2 | 720 | 200 |
| 300 | NIO | 68 | 250 | 0 | 440 | 150 |
| 500 | BIO | 250 | 60 | 15.6 | 850 | 200 |
| 500 | NIO | 110 | 235 | 1.1 | 520 | 200 |
很明显, NIO 在高并发下优势巨大 ,尤其是在吞吐量和资源利用率方面。
不过也要提醒一句:Tomcat 6 的 NIO 实现还不够成熟,SSL 支持较弱,大文件上传可能出现阻塞。因此,在关键生产环境中建议结合实际业务压测后再决定是否启用。
🏁 写在最后:老技术的价值从来不在于新潮
回顾整篇文章,我们从请求生命周期、Container 架构、类加载机制,一路讲到 JSP 编译原理和 Connector 性能调优。你会发现,尽管 Tomcat 6 已经“退休多年”,但它所体现的设计思想依然深刻影响着今天的软件架构。
比如:
- 分层容器 → Spring 的 ApplicationContext 层级
- 打破双亲委派 → OSGi、模块化系统的灵感来源
- NIO 模型 → Netty、Vert.x 的底层基石
所以说, 学旧技术不是守旧,而是为了看清演进的轨迹 。
下次当你面对一个运行多年的老旧系统时,不妨换个角度想想:它之所以能活这么久,一定有它的道理。而我们要做的,不是急于推倒重来,而是先学会读懂它的语言,理解它的逻辑,然后才谈得上改造与升级。
毕竟,真正的高手,既能玩转最新框架,也能驾驭最老系统。💼✨
“过去的技术不会死去,它们只是慢慢被遗忘。”
——但愿我们这一代程序员,永远不会忘记那些撑起整个互联网时代的基石。
简介:Apache Tomcat 6.0.29 是一个实现 Java Servlet 和 JSP 规范的开源应用服务器,广泛用于部署和运行 Java Web 应用。该版本支持 Servlet 容器、JSP 2.1、Java EL、JSTL、多连接器模式及热部署等核心功能,具备良好的开发与部署能力。尽管已停止维护,不推荐用于现代生产环境,但其架构设计对学习 Web 服务器原理和旧项目维护仍具参考价值。本文深入解析其目录结构、配置方式与关键特性,帮助开发者理解 Tomcat 的运行机制,并为迁移至新版提供基础。
2598

被折叠的 条评论
为什么被折叠?



