简介:Apache Tomcat 8.0.47 是一份面向 Java Web 开发者与初学者的轻量级 Servlet/JSP 容器发行包,可用于本地搭建、部署和调试基于 Java 的 Web 应用。压缩包共 628 个文件,约 9.75MB,包含 html、class、java、jsp 等源码与页面文件,jar、xml、properties 等依赖和配置文件,以及 sh、bat 启动脚本和 gif、svg 等静态资源,较完整地呈现了 Tomcat 的目录结构与运行所需组件。其核心覆盖 Catalina 容器、Jasper JSP 引擎、Coyote 连接器等机制,并带有 conf 配置、webapps 部署、manager/admin 管理工具和日志排错相关文件,便于读者理解请求处理、应用部署与 HTTPS 安全配置。现有 122 人学习下载,适合需要稳定运行环境、研究容器内部组成或进行课程实验的读者参考。
1. 一个老 war 卡住的下午
apache-tomcat-8.0.47 的安装,说难不难,但确实能把人卡掉一下午。前阵子帮人收拾一台机器:项目方给的是一个跑了多年的老 war,运维图省事,直接在服务器上装了最新版 Tomcat,解压、启动看着都正常,访问却一直 404,日志里翻出来一片java.lang.NoClassDefFoundError: javax/servlet/...。换回 8.0.47 后 war 能展开了,可JAVA_HOME又指到了高版本 JDK,启动直接抛Unsupported class file major version。这类老版本 Tomcat 的坑,八成不在 Tomcat 本体,而在它跟 JDK、端口、系统环境之间那层看不见的耦合。
这篇就按我平时上机器的顺序记:先把版本和 JDK 钉死,再解压、配环境变量,然后动server.xml,最后落到验证和排错。适合手里攥着一个老 war、必须在一台 Linux 机器上把 8.0.47 跑稳的人,也适合想弄清 8.0、8.5、9 这几个分支到底差在哪的熟手。
2. 装之前先把版本钉死:apache-tomcat-8.0.47 与 JDK 的匹配
2.1 为什么老项目只能落在 8.0 这条线上
Tomcat 每个大分支对应的规范不一样,这件事决定了你手里的 war 能不能直接扔进去跑。7.0 实现 Servlet 3.0,8.0 实现 Servlet 3.1、JSP 2.3、EL 3.0 和 WebSocket 1.1,8.5 虽然也是 Servlet 3.1,但内部重构过一批容器实现,9.0 跳到 Servlet 4.0,10 之后干脆把javax.*全换成jakarta.*。老 war 里写死了javax.servlet.http.HttpServlet,扔进 10 里连类都找不到,这不是配置能救的,只能换版本。
8.0.47 是 8.0.x 分支最后一个发布版本,之后这个分支就不再出新包了。选它的理由很实际:war 是按 Servlet 3.1 编的、依赖javax命名空间、可能在 JDK 7 上编译过,而 8.0.47 是这条线上最后一个把这些都兜住的稳定包。反过来说,如果你手上是全新的项目,没有历史包袱,那没必要停在 8.0,直接从 9.0 起步更省心。判断标准就一条:看 war 包WEB-INF/lib里有没有 Servlet API 的依赖,以及web.xml的version写的是 3.0 还是 3.1。
提示:8.0.47 停止更新已久,如果这套服务要暴露在公网,先想清楚有没有前置网关做拦截,别把端口直接摊在外面。
2.2 JDK 版本对照:JDK 8 是 8.0.47 最稳的搭子
| Tomcat 分支 | Servlet 规范 | 最低 JDK | 8.0.47 场景下的取舍 |
|---|---|---|---|
| 7.0.x | Servlet 3.0 | JDK 6 | 太老,新包基本不再兼容 |
| 8.0.x(8.0.47) | Servlet 3.1 | JDK 7 | 首选 JDK 8,兼容性最好 |
| 8.5.x | Servlet 3.1 | JDK 7 | 内部实现有变,老代码慎换 |
| 9.0.x | Servlet 4.0 | JDK 8 | 新项目起点 |
| 10.1.x | Servlet 6.0 | JDK 11 | 老 war 别碰 |
JDK 这一侧我一般固定用 JDK 8 的某个后期更新版本,不要用 8 的最早期构建。原因有两个:一是从 JDK 8u151 之后 TLS 1.2 相关套件默认打开,老系统对接外部接口时不至于握手失败;二是 JDK 8 里Metaspace已经取代了永久代,参数写法跟 JDK 7 不一样,网上搜到的-XX:PermSize那套在 JDK 8 上会被直接忽略,容易让人误以为参数没生效。
JDK 11 及以后不建议往 8.0.47 上凑。倒不是完全跑不起来,而是 JDK 11 移除了一批javax下的模块(JAXB、Annotation 之类),老 war 里只要间接依赖到,启动阶段就会ClassNotFoundException,而且报错位置往往在第三方 jar 内部,排查起来相当费劲。
# 确认当前机器上的 JDK 版本与位数,8.0.47 需要 64 位 JDK 配 64 位系统 java -version # 输出里出现 64-Bit Server VM 才说明是 64 位;没有这一行就是 32 位,换包java -version的输出会打到 stderr,习惯上重定向一下再看更清楚。位数这一项经常被忽略:机器是 64 位、JDK 装成 32 位,Tomcat 一样能启动,但堆内存上不去,-Xmx设到 2G 以上会直接起不来,报的错还跟内存无关,绕一大圈才反应过来。
2.3 取包与校验:别省这一步
包从官方归档目录取,Linux 服务器要tar.gz,Windows 本地调试用zip,别把带界面的安装包传到服务器上,那玩意儿在无图形界面的机器上跑不起来。
mkdir -p /opt/soft && cd /opt/soft # 把 apache-tomcat-8.0.47.tar.gz 放到当前目录后执行 ls -lh apache-tomcat-8.0.47.tar.gz # 与归档页公布的 sha512 做比对,两个值必须完全一致 sha512sum apache-tomcat-8.0.47.tar.gz校验这步看着多余,但内网传大文件断点续传、U 盘拷贝中途拔线,都会留下一个大小正常、内容截断的包。这种包解压时可能只报一句gzip: stdin: unexpected end of file,也可能解压成功但lib里少个 jar,启动时报NoClassDefFoundError,方向全被带偏。我现在的习惯是先sha512sum再解压,三十秒的事。
2.4 落地前的系统检查清单
装之前花两分钟确认几件事,能省掉后面一大半排错时间:
- 端口占用:
ss -lntp | grep -E ':8080|:8005|:8009',有输出就说明端口被占,先查清楚是谁。 - 内存余量:
free -m,堆设 1G 的话机器至少留 2G 空闲,不然启动就跑交换。 - 文件句柄:
ulimit -n,默认 1024 对并发稍高的服务不够,后面 systemd 里会单独提。 - 磁盘:
df -h /usr/local,日志和临时文件都落在这块盘上。 - SELinux 状态:
getenforce,返回Enforcing就要额外处理端口标签,这一点在第 5 章细说。
3. 解压与 CATALINA_HOME:三个路径写错一个就白干
3.1 建运行用户、解压、做软链
Tomcat 不建议用 root 跑,一是权限太大,二是被入侵后影响面完全不同。先建一个不能登录的系统用户,再把包解到/usr/local下。
# 建一个不能登录的系统用户专门跑 Tomcat useradd -r -s /sbin/nologin tomcat # 解压到 /usr/local,-C 指定目标目录 tar -zxvf apache-tomcat-8.0.47.tar.gz -C /usr/local/ # 做软链,路径里带版本号,以后升级只改软链指向 ln -s /usr/local/apache-tomcat-8.0.47 /usr/local/tomcat # 先把真实目录的属主改掉,再确认软链能正常访问 chown -R tomcat:tomcat /usr/local/apache-tomcat-8.0.47 chmod -R u+rwX /usr/local/apache-tomcat-8.0.47 ls -l /usr/local/tomcat/bin/catalina.sh这里的逻辑是:真实目录保留版本号,所有配置文件、脚本、systemd 单元统一写软链路径/usr/local/tomcat。将来要换 8.0.x 的另一个包,只要解压新目录、把软链指过去、重启服务,配置文件不用改一行。chown必须对真实目录执行,对软链执行只会改链接本身的属主,bin下的脚本仍然是解压时的原始权限,startup.sh会提示权限不足。
3.2 JAVA_HOME、CATALINA_HOME、CATALINA_BASE 三个变量的区别
这三个变量是新手最容易混的一组。JAVA_HOME指向 JDK 根目录;CATALINA_HOME是 Tomcat 程序的安装位置,bin、lib都在它下面;CATALINA_BASE是运行时实例目录,conf、logs、webapps、work从这里读。单实例部署时HOME和BASE指向同一个路径,看起来重复,但一旦要在一台机器上跑两个实例,就必须拆开,各自有独立的conf和webapps。
# 写入 /etc/profile.d/tomcat.sh,登录 shell 自动生效 export JAVA_HOME=/usr/local/jdk1.8.0_202 export JRE_HOME=$JAVA_HOME/jre export CATALINA_HOME=/usr/local/tomcat export CATALINA_BASE=/usr/local/tomcat export PATH=$JAVA_HOME/bin:$PATHJRE_HOME在 JDK 8 下可以指向$JAVA_HOME/jre,也可以不写,catalina.sh找不到JRE_HOME时会退回用JAVA_HOME。PATH里把$JAVA_HOME/bin放最前面,是为了避免机器上还装了别的 JDK,java -version看到的和 Tomcat 实际用的不是同一个。写完之后source /etc/profile.d/tomcat.sh,再echo $JAVA_HOME、$JAVA_HOME/bin/java -version各确认一遍。
注意:
/etc/profile.d只对登录 shell 生效。用 systemd 托管服务时,进程不会读这个文件,必须在单元文件里再写一遍Environment=,这是第 5 章第一条坑的根源。
3.3 目录职责:哪些能动,哪些别乱动
| 目录 | 存什么 | 上线后的操作习惯 |
|---|---|---|
bin | 启停脚本、bootstrap.jar、tomcat-juli.jar | 只往里加setenv.sh |
conf | server.xml、web.xml、context.xml、logging.properties、tomcat-users.xml | 主要改造区,改前先备份 |
lib | 容器级公共 jar | 数据库驱动可以放这里 |
logs | catalina.out、localhost_access_log | 配轮转,别让它无限涨 |
temp | 运行期临时文件 | 运行中不要手动清 |
webapps | 应用部署目录 | war 扔进来即可 |
work | JSP 编译产物 | 页面改了不生效时清这里 |
work这个目录值得单独说一句。JSP 是先编译成 Servlet 再跑的,编译结果缓存在work/Catalina/localhost/<应用名>下。有时候改完页面刷新还是旧内容,或者报一个莫名其妙的 500 而日志里只有编译异常,把对应子目录删掉让它重新编译,往往就好了。这是老版本 Tomcat 的常规操作,不算玄学。
3.4 setenv.sh:把 JVM 参数固化下来
catalina.sh启动时会检查bin下有没有setenv.sh,有就source进来。参数写在这里比改catalina.sh干净,升级换包时也不会丢。
#!/bin/bash # /usr/local/tomcat/bin/setenv.sh export JAVA_OPTS="-server \ -Xms1024m -Xmx1024m \ -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/usr/local/tomcat/logs" export CATALINA_PID="/usr/local/tomcat/tomcat.pid" export CATALINA_OPTS="-Djava.security.egd=file:/dev/./urandom"逐项说一下取舍。-Xms与-Xmx设成一样大,避免运行期堆反复扩缩带来的停顿;MetaspaceSize是 JDK 8 的写法,JDK 7 下要换成-XX:PermSize和-XX:MaxPermSize,写错了不会报错,只是静默失效。-Dfile.encoding=UTF-8只影响 JVM 的默认字符集,解决不了 HTTP 请求参数的编码问题,那个在Connector上,下一章单独讲。-Duser.timezone在容器里尤其重要,不设的话日志时间经常差 8 小时。HeapDumpOnOutOfMemoryError是留给自己的后悔药,真 OOM 了至少有现场可看。
CATALINA_PID指定 pid 文件位置,shutdown.sh会读它来精确定位进程。没有这个文件时,停止逻辑靠匹配命令行里的关键字,同机多实例的情况下很容易关错人,这个坑后面还会提。
# 加执行权限,没有 x 权限 catalina.sh 不会 source 它 chmod +x /usr/local/tomcat/bin/setenv.sh4. 配置 server.xml:8080、8005、8009 各有各的脾气
4.1 一份 server.xml 的层级关系
server.xml的结构是Server → Service → Connector / Engine → Host → Context。Server是最外层,管整个容器和关闭端口;Service把一个或多个Connector绑到一个Engine上;Connector负责协议和端口;Engine是请求处理流水线;Host对应虚拟主机;Context对应一个具体应用。日常改动集中在Connector和Host这一层,Engine基本不动。
下面是精简并调过参的版本,可以直接对照着自己那份改:
<Server port="8005" shutdown="SHUTDOWN" address="127.0.0.1"> <Service name="Catalina"> <!-- HTTP 连接器:协议钉死为 NIO,编码显式声明 --> <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" maxThreads="500" minSpareThreads="50" acceptCount="200" URIEncoding="UTF-8" redirectPort="8443" compression="on" compressionMinSize="2048" compressableMimeType="text/html,text/xml,text/css,application/javascript,application/json" /> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b %D" /> </Host> </Engine> </Service> </Server>4.2 Connector 参数逐个掰
| 参数 | 8.0.47 默认值 | 常见调整 | 说明 |
|---|---|---|---|
port | 8080 | 按需改 | 对外端口,改完同步防火墙 |
protocol | HTTP/1.1(自动选 NIO) | 显式写 NIO 实现 | 8.0 起默认已是 NIO |
maxThreads | 200 | 500 左右 | 并发处理线程上限 |
acceptCount | 100 | 200 | 队列满后的等待队列长度 |
connectionTimeout | 20000 | 20000~30000 | 连接超时,单位毫秒 |
URIEncoding | ISO-8859-1 | UTF-8 | 8.0 分支必须显式改 |
compression | off | on | 文本类响应压缩 |
URIEncoding这一项要单独强调。8.0 分支的 HTTP 连接器默认用 ISO-8859-1 解码 URI,8.5 之后才改成 UTF-8。所以同样的代码、同样的web.xml,在 8.0.47 上 GET 请求带中文参数就会出现乱码或者问号,POST 表单却完全正常。原因是 POST 的参数在请求体里,走的是request.setCharacterEncoding();GET 的参数在 URI 里,由Connector按URIEncoding解。两边规则不同,只改一处当然不够。这个坑我在第一次碰 8.0.x 的时候踩得很实在,查了两小时配置,最后发现是版本默认值的问题。
maxThreads别盲目往上加。线程数受内存和下游依赖限制,调到 1000 而数据库连接池只有 20,多出来的线程全堵在等连接上,表现是响应时间整体拉长。500 是多数业务系统的起点,压测之后再看要不要动。
4.3 8005 关闭端口与 8009 AJP 的处置
<Server port="8005">是关闭端口,向它发送SHUTDOWN字符串就能停掉实例。这个端口有两个讲究:一是加address="127.0.0.1",只允许本机连,别让外网随便发个字符串就把服务关了;二是同机多实例时每个实例必须用不同端口,否则 A 实例执行shutdown.sh,实际关掉的是 B 实例,因为默认8005撞在一起了。
如果不用远程关闭,直接把port写成-1禁用也是常规做法,停服务就靠catalina.sh stop加 pid 文件,更可控。
8009的 AJP 连接器是给前置 httpd 用的。如果前面没有 Apache httpd 做反向代理,这个Connector建议整段注释掉。原因有两个:一是多开一个监听端口就多一份暴露面;二是 2020 年前后公开的 AJP 文件读取问题影响范围正好覆盖 8.0.47,而这个分支不会再出修复包。真要用 AJP,至少把address绑到127.0.0.1,并且requiredSecret设上。
<!-- 不需要 AJP 时把这一段整体注释掉 --> <!-- <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="127.0.0.1" requiredSecret="一段长随机串" /> -->4.4 tomcat-users.xml 与 manager 应用的访问限制
manager和host-manager这两个自带应用默认只允许本机访问,规则写在conf/Catalina/localhost/manager.xml的RemoteAddrValve里。要开远程部署,改的是这个白名单,而不是在tomcat-users.xml里加个用户就完事。
<!-- conf/Catalina/localhost/manager.xml --> <Context antiResourceLocking="false" privileged="true"> <!-- 只放行内网网段和本机,正则别写 .* --> <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.0\.0\.1|10\.10\.\d+\.\d+" /> </Context>allow是正则,.*等于对全网开放,配上弱密码基本就是给人递钥匙。tomcat-users.xml这边按用途分角色,自动化部署只给manager-script,别顺手把manager-gui、admin-gui一起塞进去。
<!-- conf/tomcat-users.xml,放在 <tomcat-users> 标签内 --> <role rolename="manager-script"/> <user username="deployer" password="换成一串足够长的随机串" roles="manager-script"/>5. 避坑与排查:8.0.47 起步阶段最容易翻车的五处
5.1 现象:startup.sh一闪而过,日志里写Neither the JAVA_HOME nor the JRE_HOME environment variable is defined
原因:JDK 环境变量只写在当前 shell 或/etc/profile.d里,手工执行startup.sh时正常,交给 systemd、cron 或者别的进程拉起时不生效。非登录 shell 不会加载profile.d,这是最常见的解释。
解决:三方同时确认。/etc/profile.d/tomcat.sh里写一份给交互式登录用;setenv.sh里兜底export JAVA_HOME=...,因为catalina.sh一定会 source 它;systemd 单元里再写Environment="JAVA_HOME=/usr/local/jdk1.8.0_202"。三处写同一个路径,不要一处写软链一处写真实目录。排查时用systemctl show tomcat -p Environment看进程实际拿到的环境,比猜快得多。
5.2 现象:本机curl 127.0.0.1:8080返回 200,从别的机器访问超时
原因:三种可能叠在一起。Connector上写了address="127.0.0.1"只监听回环;系统防火墙或云上安全组没放行 8080;SELinux 处于Enforcing且没有 8080 端口的标签。
解决:先ss -lntp | grep 8080看监听地址,出现127.0.0.1:8080就是第一种,改成0.0.0.0或内网网卡 IP 后重启。监听地址对了仍然不通,再查防火墙:firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload。SELinux 这边用semanage port -l | grep http_port_t看已放行端口,没有的话semanage port -a -t http_port_t -p tcp 8080补上。三层依次排查,别一上来就setenforce 0,那是把问题藏起来不是解决。
5.3 现象:日志里中文正常,页面上 GET 参数的中文全是问号,POST 提交的多字节内容却是好的
原因:8.0.47 的 HTTPConnector默认URIEncoding="ISO-8859-1",GET 参数按这个字符集解码,中文必然丢失;POST 请求体走的是另一套编码逻辑,所以表现不一致。
解决:Connector上加URIEncoding="UTF-8",重启生效。同时确认setenv.sh里的-Dfile.encoding=UTF-8也加上了,它管的是 JVM 默认字符集,跟 URI 解码是两件事,缺一个都会在某些场景露馅。改完用curl -G --data-urlencode "kw=测试" http://127.0.0.1:8080/app/query验证,比在浏览器里点更准。
5.4 现象:同机两个实例,停掉 A 之后 B 也跟着挂了,或者 A 停了 B 还在跑
原因:两个实例的server.xml都用了默认8005关闭端口,shutdown.sh发出SHUTDOWN后,实际响应的是另一个实例。另一种情况是没配CATALINA_PID,停止脚本按关键字匹配进程,把同机其他 Java 进程一起算了进去。
解决:每个实例的Server port分配不同值,比如 8005、8006、8007,并在setenv.sh里为每个实例指定独立的CATALINA_PID路径。CATALINA_BASE也必须是各自独立的目录,conf、logs、webapps全部分开。停服务统一走bin/catalina.sh stop 30 -force,30 是等待秒数,超时后用-force强制结束,比直接kill -9少一堆残留的临时文件。
5.5 现象:跑一段时间后OutOfMemoryError: Metaspace,或者整体响应越来越慢直到无响应
原因:一是MaxMetaspaceSize没设上限,类加载器泄漏时会把机器内存吃干;二是生产环境开着autoDeploy="true",反复热部署导致旧的WebappClassLoader无法回收,这是 8.0.x 上非常典型的泄漏路径;三是堆本身设得太小,业务量一上来就频繁 Full GC。
解决:setenv.sh里设-XX:MaxMetaspaceSize=256m和-XX:+HeapDumpOnOutOfMemoryError,真出问题时至少有 dump 可分析。生产环境把Host的autoDeploy关掉,改成发布时显式重启。观察 GC 用jstat -gcutil <pid> 1000,看老年代占比是不是一路往上不回落,这比看top里的 CPU 更能说明问题。
6. 装完怎么验证跑通:三层验收加一个开机自启
装完别急着交付,按三层走一遍:进程层、端口层、HTTP 层。缺哪层都会留下隐患。
# 第一层:进程在不在,能同时看到启动参数和 pid ps -ef | grep '[o]rg.apache.catalina.startup.Bootstrap' # 第二层:端口是否按预期监听,8080 必须在,8009 应该是没有的 ss -lntp | grep -E ':8080|:8005|:8009' # 第三层:HTTP 是否真的能返回,只看状态码和页面关键字 curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/ curl -s http://127.0.0.1:8080/ | grep -i 'tomcat'第三层返回200且页面里能看到版本信息,容器本身就算通了。接着验证应用层:把一个测试 war 放进webapps,tail -f logs/catalina.out看到Deployment of web application archive ... has finished in xxx ms,再用curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/demo/确认返回码不是 404 或 500。catalina.out里那句Server startup in xxx ms也要扫一眼,启动时间突然从 8 秒涨到 60 秒,通常是某个应用在初始化时卡住了外部连接。
最后配开机自启,用 systemd 托管,Type=forking配合CATALINA_PID:
# /etc/systemd/system/tomcat.service [Unit] Description=Apache Tomcat 8.0.47 After=network.target [Service] Type=forking Environment="JAVA_HOME=/usr/local/jdk1.8.0_202" Environment="CATALINA_HOME=/usr/local/tomcat" Environment="CATALINA_BASE=/usr/local/tomcat" PIDFile=/usr/local/tomcat/tomcat.pid ExecStart=/usr/local/tomcat/bin/startup.sh ExecStop=/usr/local/tomcat/bin/catalina.sh stop 30 -force User=tomcat Group=tomcat Restart=on-failure RestartSec=10 LimitNOFILE=65536 [Install] WantedBy=multi-user.targetType=forking是因为startup.sh拉起 Java 进程后就退出了,systemd 需要靠PIDFile找到真正的进程;LimitNOFILE把句柄上限提到 65536,避免并发上来之后报Too many open files;Restart=on-failure让异常退出能自愈,但注意它不会区分正常停机和崩溃,systemctl stop之后不会自动拉起。
systemctl daemon-reload && systemctl enable --now tomcat,再systemctl status tomcat看一遍状态,reboot之后重复上面三层验证。
从那以后我每次装 Tomcat,不管多急,都会先ps -ef | grep java确认没有残留进程,再按setenv.sh、server.xml、三层验证的顺序走一遍。多花五分钟,比半夜被叫起来看 503 划算得多。希望帮到你。
本文还有配套的精品资源,点击获取