简介:Apache Tomcat 8.0.47 是一个轻量级、高性能的 Java Web 应用服务器,面向 Java 后端开发者与运维人员,用于部署和运行基于 Servlet 与 JSP 的 Web 应用。压缩包共 628 个文件,大小约 9.75MB,涵盖 HTML 静态页面、JSP 动态视图、Java 源码与编译后的 class 文件、依赖 jar 包、server.xml 等 XML 配置,以及 Windows/Linux 下的启动脚本等。资源内含对 Catalina、Jasper、Coyote 等核心组件的完整实现,可直接解压后配合 JDK 使用,便于研究 Tomcat 目录结构、默认部署流程与安全配置(如 HTTPS 与角色访问控制)。目前已有 122 人学习下载,适合入门者熟悉 Java Web 环境搭建,也可为生产环境部署提供参考。 apache-tomcat-8.0.47 这个包名出现在面前,通常只有两种场景:要么你接手了一个2017年前后上线的老项目,部署文档里写死了这个版本;要么你正在下载镜像页里反复确认,拿不准这个版本今天还能不能用于生产。这篇笔记就是冲着 apache-tomcat-8.0.47 来的,把我从解压启动、部署应用、参数调优到故障排查、安全加固、迁移升级的完整链路都过一遍,全部是真实环境里验证过的做法。适合刚接手遗留系统的Java开发,也适合正在评估要不要升级Tomcat的运维同学,照着操作能省下不少试错时间。
1. 8.0.47的时代坐标:这版能干什么,不能干什么
1.1 它的技术底子和当年定位
Apache Tomcat是Java生态里最常用的Servlet容器,8.0.x系列对应Servlet 3.1规范,配套JSP 2.3、EL 3.0和WebSocket 1.0。8.0.47是2017年底发布的维护版本,那时候8.0.x已经进入修bug阶段,新功能基本都放到了8.5.x。所以它不支持HTTP/2,默认连接器还是BIO阻塞模型——这不是缺陷,而是版本定位决定的。放在当年跑一套Spring MVC加MyBatis的传统war包应用,它完全够用。
需要注意,8.0.x要求Java 7及以上,实际生产建议直接上JDK 8。它支持Servlet 3.1的异步处理和非阻塞I/O,但应用代码必须显式使用这些特性,Tomcat本身不会替你做。另外,它更适合传统war包部署,不太适合跑Spring Boot可执行jar的嵌入式场景——那套生态是8.5之后才逐步成熟的。如果团队已经全面Spring Boot化,我更建议直接用内嵌容器或升级到Tomcat 9,别在这版上继续投入。
1.2 必须先正视的事实:它已停止安全更新
这是整篇文章最想说的一句话:8.0.x系列在2018年年中停止维护,8.0.53是最后一个版本,官方此后不会再发布任何安全补丁。2020年爆出的Ghostcat漏洞(CVE-2020-1938)就是最典型的教训——攻击者只要能访问AJP端口8009,就可以读取Web应用源码甚至触发文件包含,8.0.x全系列都在受影响范围内,官方又不会给这个系列出修复版,唯一彻底的出路是升级。
如果你的服务还在公网上跑着8.0.47,我的态度很明确:把「安全加固」当短期止血方案,把「升级」当这个月就要排期的事。后面第6部分会给出一套加固清单,但请记住,加固只是时间缓冲,解决不了版本停更的长期风险。
2. 环境准备里的隐形门槛:JDK、目录结构和启动脚本
2.1 为什么必须装JDK而不是JRE
Tomcat 8.0.x要求Java 7及以上,但生产环境我建议直接上JDK 8,原因不是Tomcat本身,而是JSP编译器依赖javac。很多新手图省事装了JRE,Tomcat启动一切正常,第一次访问JSP页面却报Unable to compile class for JSP,排查半天才发现是没装编译器。所以装完先确认JAVA_HOME指向JDK根目录,而不是JRE目录。同时确认JDK位数与系统架构匹配,32位JDK跑在64位系统上,堆内存上限会被压到4GB以内,这对生产环境是硬伤。
2.2 目录结构与启动脚本
安装目录下主要关注六个目录,部署前先走一遍,能避免很多低级的「目录放错」问题。
| 目录 | 作用 | 需要关注的点 |
|---|---|---|
| bin | 启动/关闭脚本 | 生产配置写进setenv.sh,别直接改catalina.sh |
| conf | 全部配置文件 | server.xml、tomcat-users.xml都在这里 |
| lib | Tomcat全局共享jar | 需要全局共享的JDBC驱动等放这里 |
| logs | 运行日志 | catalina.out是主日志,后面单独讲 |
| webapps | Web应用部署目录 | war包或解压目录放这里 |
| work | JSP编译后的class缓存 | 清理无妨,重启后自动重建 |
启动脚本有几个容易误会的点。bin/startup.sh会把Tomcat放到后台,所有stdout输出进logs/catalina.out,终端看不到任何日志,这是很多人以为「没启动成功」的原因。排查时先ps -ef | grep java确认进程在不在,再看catalina.out。前台调试改用bin/catalina.sh run,日志直接打在终端,非常适合定位启动崩溃问题。关闭用shutdown.sh,但它只连接8005端口发指令,端口被占或配置被改时关闭会失败,这时只能kill进程。
另外强调一句:永远不要用root账号启动Tomcat。Tomcat进程一旦被利用,root权限等于把整台服务器交给攻击者。生产环境创建专用tomcat用户,把目录owner改成它,再以该用户启动,这条后面加固章节还会展开。
3. 部署Web应用的三种姿势,顺便把类加载讲明白
3.1 部署方式与Context path
大多数人只会一种部署方式:把war包丢进webapps。这没错,但完整认知得有三条路:
- 直接拷贝:war或解压目录放进webapps,启动时自动识别,文件名就是Context path,最常用。
- Manager应用:通过
/manager/html上传部署或热部署,需要先在conf/tomcat-users.xml里配置manager-gui角色,且默认只允许本机访问。 - 手动配置Context:在server.xml或context.xml里写
<Context>,适合把应用目录放到webapps之外,但server.xml改错会导致整个Tomcat起不来,不推荐。
Context path就是访问URL的前缀。webapps/ROOT.war对应根路径/,webapps/myapp.war对应/myapp。很多404问题的根源就在这:你以为访问http://ip:8080/,其实应用根本没部署在ROOT下。还有一种常见翻车是把目录命名为myapp_v2,结果冒出个/myapp_v2路径,前端所有相对路径全部失效。发布时war包名要固定,别带版本号,版本管理交给发布脚本,而不是目录名。
3.2 类加载顺序:同jar不同位置,行为完全不同
如果你只想记一条Tomcat特性,记住这个:Web应用的类加载器不遵循Java默认的双亲委派,而是优先从应用自己的WEB-INF/classes和WEB-INF/lib加载类,找不到才去Tomcat全局lib找。也就是说,同一个jar放Tomcat的lib和放应用的WEB-INF/lib,最终生效的可能是不同版本。
排NoClassDefFoundError或ClassNotFoundException时,第一反应应该是查类冲突。用mvn dependency:tree理清依赖,再看tomcat/lib下有没有同名不同版本的jar,把重复的清掉。我遇到过一个现场:项目用某个版本的fastjson,Tomcat全局lib被人塞了另一个版本,应用里序列化行为时好时坏,最后查出来就是全局lib污染。经验总结就是:全局lib里的jar越少越好,能放应用里就放应用里。
3.3 reloadable的开发和产线取舍
Context的reloadable="true"会监控WEB-INF/classes和WEB-INF/lib下的文件变化,变了就自动重载应用,开发时很爽。但重载本质是销毁旧类加载器、创建新类加载器,如果应用持有静态变量、线程池或第三方缓存,很容易造成类加载器泄漏,旧资源释放不掉,最终Metaspace或堆内存被慢慢吃满。生产环境一定把reloadable设为false,发布用「停应用、换war、启动」的固定流程,比在线热部署安全得多。开发环境怎么折腾都行,别把开发习惯带进生产。
4. 生产级调优:Connector、Executor和JVM参数这样配合
4.1 8.0默认是BIO,想上NIO要显式写
这是8.0.x和8.5.x最大的差别之一。在8.0里,<Connector protocol="HTTP/1.1">对应的其实是BIO实现Http11Protocol,一个线程同时只能处理一个连接,Keep-Alive长连接多了线程立刻被占满。8.5开始HTTP/1.1默认映射到NIO,一个线程能同时处理多个连接。所以8.0.47想应对大量连接,必须把protocol写成org.apache.coyote.http11.Http11NioProtocol:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" executor="tomcatThreadPool" connectionTimeout="20000" acceptCount="300" maxKeepAliveRequests="100" redirectPort="8443" />这一步不做,后面调多少线程都是事倍功半。
4.2 线程池参数不是拍脑袋
Connector可以单独设maxThreads,但更推荐配一个全局Executor统一管理。参数含义我用类比说明:线程是服务员,连接是到店客人,acceptCount是门口排队凳子。服务员不够,客人站在门口等;凳子坐满,新客人直接被拒——客户端表现为Connection refused,这比无限排队更可感知,至少用户能立刻重试。
默认值maxThreads=200、acceptCount=100、connectionTimeout=20000毫秒,大多数场景够用,但并发上来了必须算一算。如果单请求平均处理时间约200ms,目标支撑600 QPS,并发线程数至少是600×0.2=120,再留30%到50%余量,maxThreads取160到200比较合理。反过来,如果接口平均耗时2秒,同样的线程数只能支撑100 QPS左右,这时候调线程意义不大,得先优化接口本身。线程数不是越大越好,线程过多反而增加上下文切换开销。
4.3 JVM参数用setenv.sh统一管理
不要在catalina.sh里塞内存参数,Tomcat一升级就被覆盖。在bin目录下新建setenv.sh,JVM参数写在这里,catalina.sh启动时会自动加载。我常用的模板:
export CATALINA_OPTS="-server -Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom"-Xms和-Xmx设成相同值,避免堆自动扩容缩容引发的抖动;JDK 8用MaxMetaspaceSize替代老的PermSize;java.awt.headless=true解决图像处理库在无图形环境下的报错;file.encoding=UTF-8从根上减少中文乱码;最后的java.security.egd是解决启动卡在SecureRandom的,下一章细说。另外如果做文件上传,注意maxPostSize默认2MB,必要时调大或设为-1,否则表单参数会被静默丢弃。
5. 排障实录:五类高频问题的定位链路
5.1 端口被占用
启动报java.net.BindException: Address already in use: bind时,第一反应是有人占了8080。排查链路:Linux用netstat -tlnp | grep 8080,Windows用netstat -ano | findstr :8080,拿到占用PID再确认进程身份。常见元凶是第二个Tomcat实例、Nginx误配置或其他中间件。同时检查8005关闭端口,它被占时shutdown.sh一样会失败,关闭指令发不出去。
5.2 中文乱码
GET请求参数乱码,根因是Tomcat默认按ISO-8859-1解析URL,中文自然变问号。解决是在Connector上加URIEncoding="UTF-8"。POST乱码是另一个套路,通常在Filter里调用request.setCharacterEncoding("UTF-8"),而且必须在读取参数之前执行。响应乱码检查Content-Type是否带charset=UTF-8。记住口诀:GET查URIEncoding,POST查Filter顺序,响应查ContentType。
5.3 JSP编译失败
访问页面报Unable to compile class for JSP,十有八九是装了JRE没装JDK。Tomcat运行期编译JSP需要javac,只有JDK才有。检查JAVA_HOME指向和java -version输出,确认带Compiler信息。另一个隐蔽原因是work目录权限不对,Tomcat写不出编译产物,检查work目录owner和启动用户是否一致。
5.4 启动卡在生成Session ID
启动日志停在Creation of SecureRandom instance for session ID generation using [SHA1PRNG]好几分钟,这是Linux下经典的熵不足问题。Tomcat生成随机数从/dev/random读,虚拟机或云主机熵源不够就阻塞。解法就是加-Djava.security.egd=file:/dev/./urandom。注意这个路径里的/./不能省,它绕过某些JDK对urandom的特殊处理,照着写就行。
5.5 内存溢出
内存溢出分Java heap space和Metaspace两种。先加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs/hprof,让OOM时自动dump堆,再用MAT分析,别瞎猜。我处理的案例里最常见元凶就是频繁热部署导致类加载器泄漏——每次reload都有旧Class没被回收,Metaspace缓慢上涨,直到某次发布后撑爆。这也印证了上一章的结论:生产环境老老实实关reloadable。
6. 安全基线:拿到一台8.0.47之后我做的事
6.1 七条加固动作,按风险排序
如果必须继续用8.0.47,以下七件事按重要性从高到低,建议全部执行:
- 清掉默认应用:webapps下的docs、examples、ROOT默认页直接删除,manager和host-manager不用也删,要留就必须改强密码并限制来源IP。
- 禁用AJP:把server.xml里port=8009的AJP Connector整个注释掉。Ghostcat漏洞走的就是这个口子,不用Apache httpd做转发就不要开它。
- 处理shutdown端口:改成随机高位端口并设置强密码;最彻底是把
port="-1",直接禁用关闭端口,代价是shutdown.sh失效,只能kill。 - 不使用root运行:创建tomcat系统用户,chown整个目录,用systemd的User=tomcat启动。
- 收敛对外暴露:如果前面有Nginx做转发,Tomcat就只监听
127.0.0.1:8080,不绑公网IP。 - 隐藏版本号:Connector上加
server="WebServer",ErrorReportValve里把showServerInfo设为false,别让错误页泄露Apache-Coyote/1.1这类指纹。 - 整理tomcat-users.xml:删除示例用户,按manager-gui、manager-script角色最小授权。
其中第2条和第4条无论哪个版本,拿到新环境我都建议立刻做。
6.2 日志体系与监控
Tomcat日志分两类:框架日志按天滚动在logs/catalina. .log,应用System.out输出到catalina.out。关键问题是catalina.out不会自动轮转,跑几个月磁盘就满了。生产上要么配logrotate,要么写cron每天把catalina.out重命名并触发句柄重建。访问日志默认没开,要在server.xml的Host里加AccessLogValve,否则排查攻击和统计流量都没有依据。
监控方面,Manager自带/status页面能看连接器和线程池状态,但更推荐开JMX接监控系统。生产开JMX必须加认证,否则等于给内网审计留后门。平时快速定位问题,工具链里一定要有jstack看线程栈、jstat看垃圾回收,这两个比任何监控面板都好使。
6.3 关于升级,我给出的个人路线
如果你的代码还停留在javax.servlet命名空间,我的建议是直接从8.0.47迁到9.0.x,跳过8.5,因为8.5也已经到了生命周期末端,别从一个坑挪到另一个坑。迁移前重点看三处:BIO的protocol配置要换成NIO写法;旧版SSL连接器属性要改成SSLHostConfig嵌套元素;应用如果用了Tomcat私有API或自定义Valve,要逐个对照新版本接口变化。至于Tomcat 10,命名空间整体改成jakarta,除非应用本身已经迁移到Jakarta EE 9,否则不要轻举妄动。
我手里那个老项目当年花了两周在测试环境做灰度,真正改代码的时间很少,大部分精力花在依赖版本和JDK环境对齐上。所以别怕升级,真正麻烦的从来是应用里那些「能跑就行」的历史包袱,而不是Tomcat本身。真要在这个版本上继续维持,那也请把第6部分的加固动作全部做完,至少别让一个早就断更的组件成为整个系统的短板。
本文还有配套的精品资源,点击获取