简介:Apache Tomcat 9.0.56 的 ZIP 包是面向 Java Web 应用开发与部署场景的官方发行版,适合需要在 Windows、Linux 或 macOS 上快速搭建 Servlet/JSP 服务器的开发者与运维人员。包内含完整目录结构,包括 bin 启动脚本、conf 核心配置、lib 运行库、webapps 默认部署目录等,可满足部署应用、调试连接器、配置 SSL 与性能调优等常见需求。资源共 639 个文件,类型涵盖 html、class、java、jsp、jar、xml、properties、sh、bat 等,既有源码与编译产物,也有配置文件和跨平台启动脚本,整体压缩包约 11.57MB,体积精简便于下载与反复解压使用。目前已有 143 人学习下载。通过解析该压缩包,读者可以直观理解 Tomcat 的目录职责、配置文件作用以及应用部署流程,可作为入门学习、环境搭建和日常维护的参考资料。
1. 为什么偏偏是 apache-tomcat-9.0.56.zip:一个版本号背后的生产环境真相
拿到apache-tomcat-9.0.56.zip这个压缩包,很多人的第一反应是“随便找个 Tomcat 下载不就完了”。但如果你在生产环境里维护过 Java Web 应用,就会明白:Tomcat 的版本号不是随便挑的,尤其是 9.0.x 这条线,每个小版本都对应着不同的 Servlet 规范实现、不同的默认行为,甚至不同的漏洞修复状态。9.0.56 这个版本,刚好卡在一个非常微妙的时间点上——它修复了之前几个版本里被反复讨论的若干问题,同时又没有引入后续版本里那些更激进的行为变更。对于很多跑着老项目、不敢随便升级大版本的技术团队来说,这个版本往往是“再撑一年”的务实选择。
这篇文章要讲的东西很直接:这个 zip 包下载下来之后,怎么从零开始部署、配置、调优,以及最重要的——你会踩到哪些坑。Tomcat 的难点从来不在启动本身,而在于 JVM 参数、连接器配置、应用部署方式、日志排查这几件事。我会把每个环节的命令、参数和排错思路都拆开讲清楚,保证新手照着做能跑起来,老手也能看到一些平时容易忽略的边界情况。
2. 从 zip 到可运行实例:目录结构、环境变量与首次启动的七个细节
2.1 解压后的目录结构:哪些文件夹能动,哪些别碰
拿到 zip 后先解压。Windows 上直接右键解压,Linux 上用unzip apache-tomcat-9.0.56.zip。解压完你会看到一个以apache-tomcat-9.0.56命名的目录,里面大概有这些子目录:bin、conf、lib、logs、temp、webapps、work。很多人第一次接触 Tomcat 时会困惑,为什么没有src目录,也没有README里说的“源码包”——因为这个 zip 是二进制发行版,不是源码包,你要部署的是编译好的 Servlet 容器,不是自己去编译 Tomcat。
这里有一个很重要的认知:bin目录下是启动和关闭脚本,conf目录下是全部配置文件,lib目录下是 Tomcat 自身运行依赖的 jar 包,而webapps目录才是你放置 Web 应用的地方。logs和temp是运行时生成的,work目录是 JSP 编译后的 class 文件缓存。我的习惯是,conf和webapps做备份,logs挂到独立磁盘分区,因为日志增长会拖垮系统盘。
2.2 必须先配好的两个环境变量
在启动之前,必须确认JAVA_HOME已经指向一个可用的 JDK。Tomcat 9.0.x 要求 Java 8 及以上版本,但如果你用的是 Java 11 或更高版本,需要注意模块化系统带来的反射访问限制。我一般会在/etc/profile.d/tomcat.sh里写:
export JAVA_HOME=/usr/local/jdk-11.0.21 export CATALINA_HOME=/opt/apache-tomcat-9.0.56 export CATALINA_BASE=$CATALINA_HOME export PATH=$JAVA_HOME/bin:$CATALINA_HOME/bin:$PATH这段配置的关键在于CATALINA_HOME和CATALINA_BASE。绝大多数单实例部署下两者指向同一个目录,但如果你在同一台机器上跑多个 Tomcat 实例,就必须让CATALINA_BASE指向各自的配置目录,否则多个实例会互相覆盖logs和work。CATALINA_HOME是程序文件所在位置,CATALINA_BASE是实例运行时使用的配置和临时文件目录,这个区别很多人搞混,后面排错时会坑到自己。
2.3 首次启动:startup.sh 与 catalina.sh run 的区别
启动方式有两类:startup.sh后台启动,catalina.sh run前台启动。第一次部署我强烈建议用前台启动,因为你能直接看到控制台日志,任何报错都会第一时间刷出来。命令如下:
cd $CATALINA_HOME/bin ./catalina.sh run如果一切正常,你会看到类似 “Server startup in [xxxx] milliseconds” 的日志。此时在浏览器访问http://服务器IP:8080,能看到默认首页。如果看不到,先别急着查防火墙,先确认启动日志里有没有报错。常见的问题是端口被占用,或者JAVA_HOME没配好导致脚本直接退出。
2.4 修改端口:server.xml 里的三个连接器
Tomcat 默认监听 8080 端口,但有三个端口需要关注,全在conf/server.xml里。第一个是 HTTP 连接器:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />第二个是 AJP 连接器,默认注释掉了。第三个是8005端口,那是 shutdown 监听端口,用来接收关闭指令。如果你改了 HTTP 端口,通常也要改 shutdown 端口,避免同一个机器上多个实例冲突。这个文件改完必须重启才生效,而且要注意 XML 标签别写错,Tomcat 启动时会做严格解析,一个标签没闭合整个实例都起不来。
2.5 验证启动成功的三个方法
启动成功的标志不是进程存在,而是端口真正在监听。我一般按顺序做三件事:
ps -ef | grep tomcat netstat -tlnp | grep 8080 curl -s http://127.0.0.1:8080 | head -20ps命令看进程是否还活着,netstat看端口是否真的在监听,curl看 HTTP 响应是否正常。有些时候进程在但端口没起来,说明应用启动到一半失败了;有些时候端口起来了但curl超时,说明应用线程阻塞或连接器出了问题。这三种情况分别对应不同的排查路径,不要混为一谈。
2.6 关闭方式:shutdown.sh 的隐藏风险
关闭 Tomcat 的脚本是shutdown.sh,它通过向8005端口发送一个特定字符串来触发关闭。这里有一个很隐蔽的坑:如果 JVM 卡死或线程死锁,shutdown.sh很多时候不生效,因为 Tomcat 的关闭钩子需要 JVM 正常运行才能执行。此时只能kill -9,但这可能带来数据丢失或应用状态不一致的问题。我的习惯是先用shutdown.sh,等 30 秒,如果进程还活着再kill -9,并且写进运维手册里。
2.7 开机自启与以非 root 用户运行
生产中绝对不要用 root 跑 Tomcat,这是安全红线。常见做法是创建一个专门的系统用户,然后通过 systemd 管理:
useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/apache-tomcat-9.0.56systemd 服务单元文件里的关键参数是User=tomcat和ExecStart=/opt/apache-tomcat-9.0.56/bin/catalina.sh run。用run而不用startup,是因为 systemd 需要 Tomcat 在前台运行才能正确管理进程生命周期。这里我踩过一次坑:用startup.sh启动,systemctl status 显示 inactive,因为 Tomcat 启动后把进程 fork 出去了,systemd 认为服务已经退出。
3. 部署一个 Web 应用:war 包与目录热部署的完整流程
3.1 war 包放哪:webapps 下的自动部署机制
Tomcat 默认开启自动部署,只要把 war 包拷贝到webapps目录下,它会自动解压并启动应用。这个过程看着简单,但实际上有一个非常容易忽视的点:war 包的文件名决定了应用访问路径。比如myapp.war会被部署到/myapp这个上下文路径下,访问地址就是http://IP:8080/myapp。如果你希望应用直接跑在根路径上,需要把 war 包命名为ROOT.war。
这个机制对已有线上环境很不友好。因为自动部署是异步的,war 包拷贝进去到应用真正可用可能间隔几秒钟,而且如果 war 包已经存在同名目录,Tomcat 会先展开新包再覆盖,期间如果请求打进来,会出现间歇性 404。我一般会先停止 Tomcat,清空旧的解压目录和 work 缓存,再放新包,启动。虽然牺牲了一点自动化,但换来的是确定性。
3.2 解压目录与可重部署性:为什么删 work 缓存能治诡异问题
JSP 文件在第一次被访问时会编译成 class 文件,缓存在work/Catalina/localhost/下。如果你更新了 JSP 文件但浏览器看到的还是旧页面,十有八九是 work 缓存没清理。更诡异的情况是,应用里的静态资源改了但页面没变,这时候除了清浏览器缓存,也要清 Tomcat 的 work 目录。这个目录删掉完全不影响运行,Tomcat 会在下次访问时重新编译。
3.3 上下文路径配置:server.xml 的 Context 标签与 META-INF/context.xml
虽然自动部署很方便,但有些场景需要手动指定上下文路径或其他参数,比如关联到外部目录下的应用文件。常见做法是在conf/Catalina/localhost/下放一个 XML 文件,文件名就是上下文路径:
<Context docBase="/data/apps/myapp" reloadable="false" />这段配置的意思是,把/data/apps/myapp这个目录映射到http://IP:8080/myapp这个访问路径。docBase指向真实路径,reloadable="false"表示不开启自动重载。这里我要多说一句:reloadable这个参数是个双刃剑,开发环境设成true很爽,改个 class 文件就自动重载;生产环境设成true就是灾难,因为每次更新文件都会触发一次类加载器重建,如果并发高,可能引起内存溢出或短暂不可用。
3.4 部署时如何看日志:catalina.out 与 localhost.log 的分工
Tomcat 有多个日志文件,新手经常搞混。catalina.out是控制台输出的汇集,包括启动日志、JVM 的 System.out 输出;localhost.log是 Tomcat 内部组件(连接器、容器)的日志;localhost_access_log.txt是访问日志,记录每次 HTTP 请求。还有一个容易被忽略的manager.log,只有当你开启 Manager 应用时才会产生。
排错的第一原则:先看catalina.out,但不要只看最后几行,要搜索Exception、ERROR、Caused by这些关键词。很多部署失败的真正原因在堆栈的Caused by部分,而不是在最开始的Exception行。某次我部署一个应用时,catalina.out 只显示LifecycleException,看起来毫无头绪,搜到Caused by才发现是数据库驱动 jar 没放进lib目录。
3.5 依赖 jar 放哪:lib 目录与 WEB-INF/lib 的边界
应用依赖的 jar 包可以放在两个地方:Tomcat 的lib目录,或者应用自身的WEB-INF/lib目录。放lib目录的位置是全局共享,多个应用可以复用;放WEB-INF/lib只对当前应用生效。这个选择有讲究:如果两个应用需要不同版本的同一个库,就不能放全局lib,否则后加载的类会覆盖先加载的,导致诡异的 NoSuchMethodError。我的原则是:除非确定所有应用都能共用同一个版本,否则一律打进 WAR 包里的WEB-INF/lib,可以避免太多类加载器层面的玄学问题。
3.6 把部署过程写成脚本:一个可靠的发布流程
手动操作容易出错,我通常会用脚本把部署流程固化下来。一个典型的上线脚本逻辑是:备份旧包 → 停服务 → 清缓存 → 放新包 → 启服务 → 健康检查。注意脚本里每个步骤之间都要有明确的结果判断,不能盲目往下走。
4. JVM 参数与连接器调优:让 Tomcat 在压力下不拖后腿
4.1 内存参数:catalina.sh 里的 JAVA_OPTS 设置
Tomcat 默认的 JVM 内存很小,通常只有 256MB 或 512MB。一旦你的应用稍微有点规模,就会遇到java.lang.OutOfMemoryError: Java heap space。这个问题几乎每个做 Java Web 的人都会碰上,但千万不要一上来就无脑调大内存。正确的做法是在bin/catalina.sh或bin/setenv.sh里设置:
JAVA_OPTS="-Xms2048m -Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC"-Xms和-Xmx分别表示堆内存的初始值和最大值,生产环境我通常会把两者设成一样,避免 JVM 在运行中动态调整堆大小带来的性能抖动。-XX:MaxMetaspaceSize是元空间上限,很多老项目跑着跑着就报Metaspace溢出,就是没设这个值。G1 是 JDK 11 及以后默认的垃圾回收器,适合大多数 Web 应用场景。
4.2 连接器参数:maxThreads、acceptCount 与 connectionTimeout 的三角关系
HTTP 连接器有几个关键参数,直接影响并发能力。maxThreads是处理请求的最大线程数,acceptCount是等待队列的长度,connectionTimeout是建立连接后的超时时间。这三者的关系很多人理解有误:不是线程数越大越好,也不是队列越长越好。一个典型的配置:
<Connector port="8080" protocol="HTTP/1.1" maxThreads="400" acceptCount="200" connectionTimeout="20000" minSpareThreads="50" />这里maxThreads=400表示最多同时处理 400 个请求,acceptCount=200表示当 400 个线程全忙时,还能排队的请求数量是 200,超过这个数量的请求会被直接拒绝。minSpareThreads=50表示即使空闲,也保持至少 50 个线程待命,避免短时间内大量请求涌入时频繁创建线程。
这个参数要怎么设,没有标准答案,取决于你的应用平均响应时间和目标 QPS。但有一个血泪经验:maxThreads不要超过 CPU 核心数的 10 到 20 倍,因为线程切换本身的代价会吃掉性能。如果 CPU 只有 4 核,设maxThreads=1000只会让系统在大量请求时卡死在线程调度上,而不是真正在处理业务。
4.3 慢请求排查:Tomcat 卡住时先看线程转储
Tomcat 卡死、请求全部超时的时候,最直接的排查手段是抓线程转储。用jstack命令:
jstack -l <tomcat_pid> > /tmp/thread_dump_$(date +%Y%m%d_%H%M%S).txt这个命令会输出 JVM 里所有线程的当前状态和调用栈。抓下来的文件里,重点看处于RUNNABLE状态的线程是否大量集中在某个类的方法上,或者是否有大量线程在java.net.SocketInputStream.socketRead0这种地方,那通常说明线程都阻塞在等待外部资源(数据库连接池、外部 HTTP 调用),而不是 Tomcat 本身的问题。
4.4 线程池参数什么时候改:executor 与 Connector 的分离配置
Tomcat 支持用一个独立的线程池给多个连接器使用,配置在server.xml里:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="400" minSpareThreads="50" maxIdleTime="60000" />然后在 Connector 里用executor="tomcatThreadPool"替代直接设置maxThreads。这个做法的价值在于,如果你的 Tomcat 同时开启了 HTTP 和 AJP 连接器,它们可以共享同一个线程池,避免线程资源被某个连接器独占。
4.5 一个典型的调优检查清单
我的习惯是部署完成后,用压测工具(常见的就是 Apache Bench 或 wrk)打一下,观察ab -n 10000 -c 200 http://127.0.0.1:8080/的结果。重点关注Time per request和Failed requests这两个指标。如果失败率不为 0,通常是连接器参数不合理或者应用本身有瓶颈。压测一定要在测试环境做,不要直接对生产环境发起任何形式的压测,这是红线。
5. 避坑指南:Tomcat 9.0.56 最容易翻车的五个场景
5.1 静态资源 404 但应用能正常访问
现象:访问http://IP:8080/myapp能打开页面,但页面里引用的 JS、CSS 文件全部 404。
原因:这个坑通常出在应用本身的静态资源路径上,或者请求被某个过滤器拦截了。90% 的情况是应用的 web.xml 里配置了过滤器映射,把静态资源请求也拦下来做了权限校验,导致返回 404 而不是正常返回文件。
解决:在应用的web.xml里为静态资源扩展名配置白名单,或者修改过滤器逻辑,对/static/、/resources/这类路径直接放行。还有一种情况是 Spring MVC 的<mvc:default-servlet-handler />没配,导致请求压根没转发到默认 Servlet。
5.2 启动时提示端口被占用,但 netstat 查不到
现象:./startup.sh执行后,catalina.out里报java.net.BindException: Address already in use,端口是 8080。执行netstat -tlnp | grep 8080却看不到任何占用进程。
原因:这是一个很隐蔽的坑。Tomcat 用的是一个 IPv6 的::地址绑定端口,而netstat默认只显示 IPv4 的监听。或者,端口被其他用户(比如 root)的进程占用了,当前用户没有权限查看。
解决:用netstat -tlnp | grep :8080加不加引号都是一样,关键是加上-p参数要 root 权限才能看到完整进程。更可靠的方案是用ss -tlnp | grep 8080,这个命令在多数新系统上更准确。如果还是看不到,试试lsof -i :8080。
5.3 修改了 server.xml 却看不到任何变化
现象:改完server.xml里的端口,重启 Tomcat 后,访问的端口还是旧的。
原因:最常见的原因是CATALINA_BASE指向了别的目录,你改的文件不是实际加载的文件。很多发行版会单独创建/var/lib/tomcat/conf之类的目录作为CATALINA_BASE,与解压出来的conf目录不是同一个地方。
解决:先执行./catalina.sh configtest看看加载的是哪个配置文件路径,或者直接在脚本里加一句echo $CATALINA_BASE打印出来确认。另外注意,server.xml的 XML 格式必须严格合法,改完先用xmllint或在线校验工具检查一遍。
5.4 应用部署后 ClassNotFoundException,但 jar 明明在 lib 目录里
现象:应用启动后,某个类报ClassNotFoundException,你确认了对应 jar 已经放进WEB-INF/lib或者 Tomcat 的lib目录,但仍然报错。
原因:这个坑大概率是 jar 包冲突,或者说 jar 包虽然存在,但被其他 jar 里的同名类抢先加载了,而那个类又缺少某个依赖。另一种常见情况是,jar 包的文件名以奇怪的字符结尾,比如带空格或中文,Tomcat 的类加载器解析不了。
解决:用jar tf查看冲突 jar 里的类路径,确认是不是存在同名类。如果确实有,需要把其中一个 jar 移除或改名。另外可以用-verbose:classJVM 参数启动 Tomcat,日志里会打印每个类的实际来源 jar,这是定位类加载问题最直接的手段。
5.5 启动了多个实例,日志互相串
现象:同一台机器跑了两个 Tomcat 实例,但logs/catalina.out里出现了两个应用的日志。
原因:两个实例的CATALINA_BASE指向了同一个目录,导致日志文件、work 目录、temp 目录全被共享了。这在复制目录部署新实例时特别容易发生,比如直接cp -r了一份解压目录,但忘了改CATALINA_BASE或server.xml里的日志路径。
解决:严格分离每个实例的CATALINA_BASE。确保每个实例的conf/logging.properties里catalinaBase指向独立目录,同时修改server.xml里所有端口,避免端口冲突。这一步做了,日志就不会串。
6. 从解压包到生产基线:一套可以固化的安装与验收流程
新手把一个 Tomcat 跑起来,往往以为事情就结束了。但真实的交付标准不是“能启动”,而是“可运维、可监控、可重建”。我习惯把整个安装过程固化成一套脚本和检查清单,以下是我每次部署apache-tomcat-9.0.56.zip都会执行的完整流程,可以作为你的生产基线。
6.1 安装与初始化脚本的核心片段
# 解压并移动到指定目录 unzip apache-tomcat-9.0.56.zip -d /opt mv /opt/apache-tomcat-9.0.56 /opt/tomcat # 创建系统用户 useradd -r -s /sbin/nologin -d /opt/tomcat tomcat chown -R tomcat:tomcat /opt/tomcat # 配置环境变量 cat > /etc/profile.d/tomcat.sh <<'EOF' export JAVA_HOME=/usr/local/jdk-11.0.21 export CATALINA_HOME=/opt/tomcat export CATALINA_BASE=/opt/tomcat EOF source /etc/profile.d/tomcat.sh这段脚本做完之后,Tomcat 已经具备基本的运行条件。但注意,这个阶段还没有做任何调优,如果直接对外提供服务,性能和安全都不够。下一步是修改server.xml和 JVM 参数,然后验证。
6.2 验证清单:五项测试全部通过才算交付
部署完成后,我会按顺序跑一遍验证,任何一项失败都视为交付不通过。第一个测试是启动测试,执行./catalina.sh run,日志里出现Server startup in即通过。第二个测试是端口测试,用ss -tlnp | grep 8080确认端口监听。第三个测试是 HTTP 响应测试,curl -I http://127.0.0.1:8080/返回 200 或 302。第四个测试是关闭测试,./shutdown.sh后进程能在 30 秒内退出。第五个测试是重启测试,连续执行三次启停,确认没有端口残留或临时文件累积问题。
这五项测试看起来简单,但很多环境问题就藏在这些细微的地方。某次交付一个内部系统时,第一次启动正常,重启后却一直报错,原因是内存参数里-Xmx设得太大,超过了服务器可用内存,第一次启动侥幸成功,第二次因为系统其他进程占了内存就起不来了。所以第六个隐藏测试项是:重启后观察free -m的内存变化,确认 Tomcat 没有把系统内存吃到极限。
6.3 日志轮转:一种值得养成的运维习惯
catalina.out这个文件默认不会自动做大小切分,时间久了能涨到几个 GB,占用磁盘空间不说,查日志也会卡到不行。Linux 上可以用logrotate来处理:
/opt/tomcat/logs/catalina.out { daily rotate 30 compress missingok copytruncate }这里的关键配置是copytruncate:先复制日志文件再清空原文件,好处是不需要重启 Tomcat。如果不用这个选项,logrotate会删掉原文件然后创建一个新文件,但是 Tomcat 的日志句柄还指向被删除的旧文件,导致新的日志继续写入一个不存在的 inode,磁盘空间不会释放。这个坑我建议每个人提前埋好避雷针。
6.4 最后收尾:一个值得保留的自我检查习惯
每次部署完,我会把当时用到的 Java 版本、Tomcat 版本、JVM 参数、数据库驱动版本全部记录在一个文档里,下次排错时先看这份记录,能省掉大量重复排查的时间。特别是针对9.0.56这个版本,如果后续要升级,这份记录能帮你判断新版本会不会引入行为变更。运维这件事,靠的不是记忆力,是沉淀下来的习惯。希望这些内容能帮到你,哪怕只是帮你少踩一个坑,也算值得。
本文还有配套的精品资源,点击获取