上个月帮人排查一个线上Java服务,应用明明启动成功了,浏览器却一直404,折腾了一个下午才发现:打的是jar包,不是war包,外置Tomcat压根没识别到Web应用。这种问题太典型了。大家现在把大量精力花在Spring、MyBatis这些代码层,对Tomcat本身的了解却停留在"它就是用来跑应用的"。然而Tomcat作为Java Web领域最常见的Servlet容器,配置、目录、日志、调优,每个环节都有能卡住人的地方。这篇就按我实际部署和维护的经验,从选版本开始,到目录、配置、部署、排错、调优、安全防护,把Tomcat从零到上线的完整操作规程写出来,尽量做到超详细,适合第一次接触Tomcat的开发者,也适合部署过但没深究过原理的运维和测试同学。
1. 选对版本:JDK与Tomcat的匹配关系比你想的更敏感
1.1 版本号背后其实是Servlet规范
打开Tomcat官网下载页面,会看到Tomcat 9、10、11等版本并列,很多人随手选个最新的就装,结果启动报错或者应用跑不起来。Tomcat的版本并不只是一个数字,每个大版本对应一个Servlet规范和一套Java版本要求。Tomcat 9对应Servlet 4.0,Tomcat 10对应Servlet 5.0,Tomcat 11对应Servlet 6.0。这里最要紧的变化是:从Tomcat 10开始,Java EE正式移交为Jakarta EE,包名从javax.servlet.*改成了jakarta.servlet.*。
我见过不少人把老项目直接扔到Tomcat 10上,启动就报ClassNotFoundException: javax.servlet.Filter。那不是Tomcat坏了,是老代码里import javax.servlet.*的方式在Tomcat 10+上已经行不通。如果你的项目是老技术栈(Spring 4、Struts2这种),老老实实用Tomcat 9最省事;如果是Spring Boot 3之后的项目,它本身用的就是jakarta.*,搭配Tomcat 10或11都没问题。
下面是常见的版本对应关系:
| Tomcat版本 | Servlet规范 | JSP规范 | Java版本要求 | 命名空间 |
|---|---|---|---|---|
| Tomcat 9 | 4.0 | 2.3 | Java 8+ | javax.* |
| Tomcat 10 | 5.0 | 3.0 | Java 8+ | jakarta.* |
| Tomcat 11 | 6.0 | 3.1 | Java 17+ | jakarta.* |
顺带说一句,Tomcat是以Java版本最低要求来标的,不代表不支持更高版本。我的习惯是:长期稳定项目选Tomcat 9,新项目且JDK在17以上才考虑Tomcat 11,Tomcat 10作为一个介于两者之间的过渡版本,除非团队已经踩过坑,否则没必要特意选。
1.2 JAVA_HOME是启动脚本的命门
Tomcat本身是Java写的,启动脚本startup.sh/startup.bat的第一件事就是找JAVA_HOME环境变量。如果这个变量没配或者指向了JRE而不是JDK,Tomcat通常也能启动一部分,但很多管理功能(比如JSP编译、Manager控制台)会出问题,因为JRE里没有javac编译器。配变量本身不难,但有几个细节值得注意。
在Linux下,我一般这样确认当前环境:
echo $JAVA_HOME java -version which java如果java -version显示是OpenJDK,但echo $JAVA_HOME为空,需要修改.bash_profile或/etc/profile加一行:
export JAVA_HOME=/usr/local/jdk-17 export PATH=$JAVA_HOME/bin:$PATH改完后记得source /etc/profile让它立即生效。Windows下就是在系统环境变量里新建JAVA_HOME,值填JDK的安装根目录(不是bin目录),然后在Path里加%JAVA_HOME%\bin。有一个判定技巧:JDK解压之后,目录下能看到bin、lib、jre(老版本)这些文件夹,而bin目录里才有javac.exe/javac。只要javac -version能执行,说明JAVA_HOME指向基本正确。
下载版本我推荐官方提供的zip/tar.gz包,解压即用,路径干净,升级也方便。Windows下有人喜欢装那种带安装向导的exe版,会注册系统服务,对服务器来说反而不好控制,我基本不用。
2. 装好之后先认识目录:七个文件夹分别管什么
解压Tomcat之后,你会看到一个结构非常清晰的目录树。很多人一上来就去翻conf里的配置文件,其实先把七个基础目录弄明白,后面所有操作都有坐标了。
| 目录 | 职责 | 日常操作 |
|---|---|---|
| bin | 启动、关闭、维护脚本 | startup.sh/shutdown.bat都在这,Windows和Linux脚本区分清楚 |
| conf | 全部核心配置 | server.xml、web.xml、context.xml、tomcat-users.xml |
| lib | Tomcat及应用共用依赖 | 数据库驱动这类公共jar放这里 |
| logs | 运行日志 | catalina.out在这里,排错主战场 |
| webapps | Web应用部署根目录 | 放war包或解压目录 |
| work | JSP编译产物 | 可删除,会自动重建 |
| temp | 临时文件 | 别乱删,但里面有垃圾可以清 |
2.1 bin:启动脚本的门道
Linux下用startup.sh启动,Windows下用startup.bat,关闭则对应shutdown.sh和shutdown.bat。有一个新手容易踩的坑:在Linux上直接执行./catalina.sh run会卡在前台不退出,那是开发调试模式;正常部署场景用startup.sh即可,它会后台启动并把日志写到logs目录。
如果需要调试启动过程,catalina.sh run反而更适合,因为日志直接打到当前终端。另外,bin目录里还有version.sh,可以快速查看当前Tomcat版本和JVM版本:
./version.sh输出里会同时显示Server number和JVM版本,排查环境问题的时候先跑这个命令,效率很高。
2.2 conf:配置文件中哪些该动、哪些别动
conf目录是Tomcat的核心,也是大多数人又爱又怕的地方。server.xml是主配置,端口、虚拟主机、连接器都在这里;tomcat-users.xml管用户角色权限;context.xml定义全局JNDI资源和Context默认行为;web.xml是全局Servlet规则。
我的建议是:日常使用只动server.xml和tomcat-users.xml,全局web.xml除非确定要做什么,否则别乱改。改任何文件之前先备份一份,比如cp server.xml server.xml.bak-20250101,改动后如果发现异常可以快速回滚。这个习惯我坚持了很多年,救过不少次场。
2.3 webapps、work和logs的关系
webapps目录是用户最常接触的部署区。Tomcat默认自带ROOT(默认站点)、docs、examples、host-manager、manager这几个应用。其中ROOT是访问http://ip:8080/时展示的那个页面;docs是文档站;examples是示例Servlet/JSP;manager和host-manager是管理控制台。生产环境里这几个默认应用都属于潜在风险,建议上线前清理掉,后面安全那一节会专门说。
work目录平常不用管,它存放JSP编译后的Java和class文件。如果你改了JSP页面发现不生效(排除缓存问题后),删掉work目录下对应应用目录再重启,是最暴力的刷新手段。logs目录才是排错的主战场,下一节会展开讲。
3. 改配置前的必备功课:server.xml里三处关键结构
server.xml是Tomcat的"总装配图",看着长,但真正天天要改的就三块:Connector(连接器)、Host(虚拟主机)、Context(上下文)。把这三块读明白,99%的端口、域名、路径问题都能自己解决。
3.1 Connector:用户的HTTP请求从哪个门进来
默认配置里长这样:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />port="8080"是HTTP访问端口,connectionTimeout="20000"表示20秒超时,redirectPort="8443"是把需要SSL的请求转发到8443端口。日常最常改的就是这个8080,很多开发机8080已经被别的服务占了,改成8081或9090都行。
改端口有个细节:不光要改Connector的port,如果用了Tomcat自带的SSL配置,redirectPort也要跟着调整,否则HTTP会自动跳转到错误的HTTPS端口。另外,protocol="HTTP/1.1"这里的1.1不是HTTP版本,而是Tomcat内部的Connector实现类型。默认就是用NIO模型,性能足够,不需要为了"升级"乱改成别的协议。
还有一个常见问题是GET请求中文乱码。Tomcat 8之后的默认URI编码已经是UTF-8了,但老版本不是。如果你自己改了Connector,明确加上一行最保险:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />3.2 Host:一个Tomcat跑多个域名的基础
Host元素在server.xml里位于<Engine>之下,默认只有一个:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">name是主机名,appBase指向应用程序的基础目录(默认就是webapps),unpackWARs表示打好的war包要自动解压,autoDeploy表示热部署。很多人不理解"为什么我访问http://ip:8080/应用名就能进应用",因为默认Host接收所有来自本机的请求,并把应用映射到路径上。
如果你需要在一台服务器上用Tomcat同时跑多个网站,可以加Host:
<Host name="blog.example.com" appBase="webapps_blog" unpackWARs="true" autoDeploy="true"> </Host>这样一个Tomcat进程可以通过不同域名访问不同站点。需要提醒的是,域名解析和Host配置是对应的,你在浏览器里用IP直接访问就看不到这几个Host的效果。
3.3 Context、docBase和path的关系
Context的核心作用是把"URL路径"映射到"磁盘目录"。在server.xml里通常这样写:
<Context path="/blog" docBase="/data/webapps/blog" />意思是:访问http://ip:8080/blog时,实际上读取/data/webapps/blog目录下的内容。这里的path是浏览器里的访问路径,docBase是磁盘上的实际路径。不写path时,表示这是Host下的根Context,访问时不需要带路径前缀。
实际部署时我更推荐用独立描述符文件,而不是直接改server.xml里的Context,后面第4节会讲具体做法。统一路径管理的好处是:多个应用之间的目录隔离更清晰,升级某个应用时也不用手忙脚乱地改主配置文件。
3.4 那个不常用的AJP端口
server.xml里默认还有一个监听8009的Connector:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />AJP是为Apache HTTP Server做正向代理时用的协议,如果没用到了这个场景,纯粹用Nginx或者直接暴露Tomcat,我的建议是直接注释掉这段。AJP协议历史上出现过不止一次安全漏洞,而且多数业务流量根本走不到它,留着等于多一条攻击路径。删掉后重启Tomcat,用ss -lntp | grep 8009确认端口已经关闭即可。
4. 部署Web应用的四种方式与切换细节
Tomcat部署应用的方式不止一种。不同场景选对方式,可以少踩很多坑。
4.1 方式一:war包直接扔webapps,最省事但别蛮干
把项目的war包复制到$CATALINA_HOME/webapps目录,然后启动Tomcat。只要unpackWARs="true"(默认开启),Tomcat会自动解压创建同名的目录。访问路径就是war包文件名去掉.war,比如myapp.war对应http://ip:8080/myapp。
这里有个隐藏规则:war包更新时,如果只看文件名没注意文件完整度,Tomcat可能解压失败。建议的操作顺序是:先停Tomcat,删掉webapps下旧的war和解压目录,再放入新war包,最后启动。直接在运行中覆盖war,虽然autoDeploy会检测到变更,但class文件、配置文件混着新旧版本的情况非常难排查。
4.2 方式二:Context描述符指向外部目录,适合和代码仓库联动
在conf/Catalina/localhost/目录下新建一个xml文件,比如blog.xml,内容:
<Context docBase="/data/projects/blog/target/blog" path="/blog" />文件名blog决定了访问路径,所以必须和希望访问的路径一致。这种方式最实用的一点是:应用可以放在Tomcat安装目录之外的任意位置,比如和Git仓库检出的构建产物目录联动,更新代码后不用拷贝war包,重启Tomcat或触发热部署就能生效。
docBase指向解压后的Web应用目录(包含WEB-INF的那一层),不是包含子模块的父目录,这个层级很多新手搞混,写错直接404。
4.3 方式三:Manager控制台,适合临时场景和快速验证
使用Manager控制台需要先配置用户权限。编辑conf/tomcat-users.xml,加一个角色为manager-gui的用户:
<role rolename="manager-gui"/> <user username="admin" password="你的密码" roles="manager-gui"/>重启后可访问http://ip:8080/manager/html,在界面上直接上传war包或部署已解压的应用。Manager还提供了文本接口/manager/text/deploy,可以配合脚本做自动化部署,不过这个场景一般都有更好的CI/CD方案,日常用得不多。
我很少在生产环境开ManagerGUI,因为存在暴力破解风险。仅在开发或测试环境临时用用,用完后及时删掉这个用户,或者限制访问来源。
4.4 方式四:IDE集成运行,适合本地开发调试
IDEA、某国产IDE等工具虽然都有内嵌Tomcat配置,但本质上都是让Tomcat以独立进程方式运行,然后把项目里的Web模块作为已解压部署目录挂进去。这种方式的优势是修改代码后可以热加载、断点调试,方便之处在于配置界面帮用户生成了部署描述,不用手动写文件。
有一个常见问题是本地调试时访问路径多了一层上下文,比如http://localhost:8080/项目名/。这其实是因为IDE默认把部署的Application context设成了项目名。在部署配置里把Application context改成/,访问时就不用带前缀了。
4.5 覆盖部署的大坑
这四个方式里最容易翻车的是覆盖部署。Tomcat在运行中检测到war包变化,会尝试自动解压替换,但它不会清理旧解压目录里已失效的class文件。如果你删了某个类,老class还在,应用可能报一些莫名其妙的NoSuchMethodError或行为不符合预期。我遇到过最典型的一次:同事只覆盖了war包,没有停服务,结果新旧版本的定时任务跑了两遍。
所以我的部署规范很简单:所有生产环境的部署必须走"先停、再清、后放、最后启"的四步流程。war包版本要在文件名上带时间戳或者Git commit号,例如myapp-20250115-8f2a1.war,这样万一要回滚,一目了然。
5. 出问题怎么查:日志体系与三类高频报错
Tomcat出问题不可怕,可怕的是不知道去哪里看错误。把logs目录下的日志文件对应关系整理清楚,排错效率能翻一倍。
| 日志文件 | 内容 | 什么时候看 |
|---|---|---|
| catalina.out | Tomcat标准输出,启动信息、JVM崩溃、System.out打印 | 启动失败、进程退出 |
| localhost.日期.log | Host级别日志,Context启动/停止、部署报错 | 应用部署失败、上下文加载异常 |
| localhost_access_log.日期.txt | HTTP访问日志,记录IP、时间、请求URL、状态码 | 排查404、500、外部访问问题 |
| manager.日期.log | Manager控制台相关操作 | 使用Manager部署排查 |
5.1 端口被占用是最常见的启动失败原因
启动时报Address already in use:8080,优先怀疑端口被占。Win下用netstat -ano | findstr 8080,Linux下用ss -lntp | grep 8080就能看到占用进程。如果端口被其他服务占用了,要么停掉那个服务,要么改Tomcat的Connector端口。
这里有个容易忽略的点:改了server.xml后必须重启Tomcat才生效,而不是等它自动加载。server.xml在运行期间基本不支持热更新。
5.2 404的本质:谁没找到谁
404分两种:Tomcat层面的404(找不到应用或路径)和应用内部的404(应用路由没匹配上)。前者访问时会看到Tomcat自带的404错误页,后者通常带应用自定义的错误页或返回JSON。看localhost_access_log里请求路径的返回码,如果是404,先用排除法确认:
http://ip:8080/能访问默认页吗?不能,说明Tomcat没起来或端口不对。- 默认页能访问但
http://ip:8080/myapp不行?说明应用没有部署成功,看localhost.日期.log里的Context加载信息。 - 应用路径能访问但某个子路径404?那多半是应用内部路由或前端路由刷新问题,和Tomcat关系不大。
5.3 ClassNotFoundException和依赖冲突
这种报错通常是应用依赖没打包完整,或者lib目录下放了不兼容的jar。Tomcat有自己独立的类加载机制:lib目录下的jar对所有应用可见且优先,应用WEB-INF/lib下的jar仅供自身使用。
所以数据库驱动这种公共组件,放在lib目录没问题;业务相关的库放在应用的WEB-INF/lib里。两边都放了同一组件的不同版本,就会出现一个要2.x一个要3.x的冲突。排这类问题的快捷方式:找到WEB-INF/lib,对比里面jar和lib目录jar是否有重复,重复的优先移除lib里的旧版本。
5.4 中文乱码问题
乱码分两个来源。第一个是Tomcat控制台输出乱码,Windows下是catalina.bat默认用了系统编码,而Java默认UTF-8所致,修改bin目录下的catalina.bat,在JAVA_OPTS里加-Dfile.encoding=UTF-8。第二个是请求参数乱码,请求体是POST在应用层做编码处理,GET参数则靠Connector的URIEncoding="UTF-8",前面配置Connection时已经提过。
顺带分享一个比较隐蔽的乱码来源:JSP页面虽然报了pageEncoding="UTF-8",但物理文件其实是GBK编码保存的。这种配置和实际内容不一致的情况,在IDE里打开看不到问题,部署到正式服务器就乱了。规范做法是IDE里设置所有源文件默认UTF-8,再配合Maven打包插件强制UTF-8编码,从源头解决。
6. 上线前的性能设置:JVM、线程池与连接器参数
Tomcat跑得稳不稳、性能好不好,一半取决于JVM和连接器参数。
6.1 JVM参数写在setenv脚本里,别去改catalina.sh
老教程会让你直接改catalina.sh里的JAVA_OPTS,我不推荐。Tomcat启动脚本在版本升级时会被覆盖,而且多实例环境下每改一次都得重来。官方支持的机制是:在bin目录下创建setenv.sh(Linux)或setenv.bat(Windows),启动脚本会自动读取。一个常见的Linux配置:
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8"-Xms是初始堆大小,-Xmx是最大堆大小,两者设为一致可以减少Java运行时的堆扩容/收缩开销。-XX:MaxMetaspaceSize限制元空间,防止类加载过多导致内存无限涨。内存给多大,取决于机器配置和应用类型:4GB物理内存的机器,建议-Xms1024m -Xmx2048m;16GB机器给到-Xms4096m -Xmx8192m也有不少团队在用。关键是别一次性把物理内存占满,要给操作系统和其他进程留余量。
6.2 Connector连接器和Executor线程池
Tomcat默认连接器在并发高的时候可能成为瓶颈。核心参数有这么几个:
| 参数 | 含义 | 参考值 |
|---|---|---|
| maxThreads | 处理请求的线程数上限 | 200-500 |
| minSpareThreads | 最小空闲线程数,保证突发请求时快速响应 | 25 |
| acceptCount | 等待队列长度,队列满了才会拒绝请求 | 100 |
| connectionTimeout | 建立连接后的等待超时 | 20000 |
| maxConnections | 最大连接数,超过后放入等待队列 | 10000 |
把连接器配置成:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="300" minSpareThreads="50" acceptCount="100" maxConnections="10000" />线程数不是越大越好,线程切换有开销,IO密集型的应用300左右一般够用,CPU密集型反而要结合机器核数来定。如果想更精细,可以单独配置Executor线程池,再把Connector绑定到Executor:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="50"/> <Connector port="8080" executor="tomcatThreadPool" protocol="HTTP/1.1" connectionTimeout="20000"/>6.3 压缩和会话保持
静态资源比较大时,开启HTTP压缩能显著减少带宽占用。在Connector上设置:
compression="on" compressionMinSize="2048" compressibleMimeType="text/html,text/xml,text/plain,text/css,application/javascript"session超时时间的修改位置是conf/web.xml:
<session-config> <session-timeout>30</session-timeout> </session-config>注意这里单位是分钟。很多业务要求会话能保持长时间有效,这时候除了调大session-timeout,还要确认服务器有没有定时清理session的任务在作怪。另外,如果你用的是Spring Session或者Redis做会话共享,Tomcat自带的这个配置就不起作用了,要以应用层配置为准。
6.4 一句话认识Spring Boot内嵌Tomcat
现在很多项目用Spring Boot开发,默认内嵌Tomcat直接启动。这种情况下不需要安装外部Tomcat,但同样受这些参数影响。内嵌模式下在application.yml里配置server.tomcat.threads.max、server.tomcat.accept-count等即可。区别是:内嵌Tomcat只是应用进程的一部分,没有了独立的server.xml,调整方式转为代码或配置文件,几种配置方式对Tomcat性能约束逻辑是一样的。
7. 安全加固与维护习惯:那些教程里不写但你迟早会碰到的
Tomcat用起来不难,难的是在真实环境里保证它既稳定又安全。最后这部分是我日常维护时积累的习惯,不少是在被扫描工具提醒之后才补的窟窿。
7.1 清掉默认应用
安装好的Tomcat自带站点、示例、文档和管理后台。这些默认应用在生产环境是最容易出问题的一批:文档和示例可以作为信息探测的入口,管理后台一旦密码泄露等于门户大开。我建议部署前直接把webapps下的docs、examples、ROOT、host-manager、manager全部移除,只留ROOT做一个最简单的占位页也可以,不暴露任何默认页面。
7.2 关闭没用的端口和管理入口
8009(AJP)在上文已经提过,注释掉。8443如果没配HTTPS也不需要开。Manager入口不是必需品的话,直接用防火墙限制外部访问:Linux上可以用iptables或firewalld只允许内网段访问管理接口。写代码的人常常觉得"内网没问题",但真实网络世界里内网渗透几乎都优先拿这些管理器开刀。
7.3 用普通用户跑Tomcat
我接手过的服务器里,把Tomcat跑在root下的情况并不少见,一旦应用被攻破,整个主机就沦陷了。正确做法是创建一个专用服务用户,比如appuser,然后把Tomcat安装目录和logs目录的所有者改成它,用该用户启动。涉及使用docker运行Tomcat时同理,容器内也用非root用户跑,这是容易被忽略的一环。
7.4 备份和变更记录
每次改动server.xml、context.xml、web.xml前备份一份带日期的文件出来,这个动作看起来简单,但关键时刻能省下整个晚上的排查时间。更推荐的是把这些配置纳入版本管理,除了配置文件本身所属的服务器环境参数外,Tomcat的版本号、JDK版本、关键参数组合都值得记录。我常做的是一张环境卡片:
| 实例名 | Tomcat版本 | JDK版本 | 端口 | JAVA_OPTS摘要 | 部署方式 |
|---|---|---|---|---|---|
| app-01 | 9.0.89 | OpenJDK 17 | 8080 | -Xms1g -Xmx2g | war包+外部目录 |
每次更换一台机器、升级一次版本,都同步更新这张表。看起来是"流程",实际上是为了让你在下一次出问题时,不用靠回忆来定位环境差异。
8. 最后想说的经验
这套内容看起来章节很多,但把Tomcat真正吃透之后,你会发现每天真正碰到的操作就那么几类:启动、查日志、改配置、部署、重启。我个人的体会是,Tomcat这类基础设施真正难的不是某个配置项,而是你出了问题之后能否快速定位"是我配置错了、是应用代码问题、还是环境问题"。日志日志还是日志,把catalina.out、localhost和access_log这三种日志看明白,再配上一份完整的版本和配置记录,绝大多数故障都能在半小时内找到方向。
最后分享一个我在实际维护中养成的小习惯:每次启动前跑一遍./version.sh,同时curl -I http://localhost:8080确认页面有响应。这两个命令加起来不超过10秒,却能避免"我以为它没启动"和"它其实没启动但我不知道"这两种最尴尬的局面。你踩过的很多坑,往往都源于对一个基础组件的了解只停留在表面,把它当黑盒久了,出问题时自然无从下手。