如果你是一名Java后端开发者,那Tomcat这名字你肯定不陌生。就算你用的是Spring Boot内嵌的容器,也总会有那么几个项目需要你在服务器上部署一个独立Tomcat,或者接手遗留系统时被迫和它打交道。这篇教程就是写给所有被Tomcat"支配"过的人——不管你是刚接触JavaWeb的新手,还是工作几年想彻底搞懂配置逻辑的老手,这篇文章都会把Tomcat从安装、目录结构、核心配置到部署、调优、排错从头到尾讲一遍,而且是以一个实际用下来的人的角度去讲,不是我抄官方文档那种。
我最早接触Tomcat时也被搞得很懵:目录一堆、配置一堆,改个端口都要百度半天,更别提部署war包后经常遇到404或者启动失败。后来用得多了才悟到一个道理——Tomcat的本质没那么复杂,它就是一个"Java Web应用的楼宇物业"。你的Servlet、JSP是房客,HTTP请求是访客,物业负责开门(连接器)、安排房间(容器)、维护秩序(生命周期管理)。你把它的架构逻辑理清了,剩下的配置就都是填空题。
这篇教程不需要你有很深的底子,我会从安装JDK开始一步步带,但内容也不全是大白话,配置参数的原理、部署机制的原因我都会解释清楚,这样你即便换了一个新的容器中间件,也能举一反三。全程没有任何废话,能直接抄作业,也会告诉你哪些坑是我踩过之后才明白的。
1. 从零认识Tomcat:它究竟解决了什么问题
1.1 Servlet容器是什么,和普通Java程序有什么不一样
很多人上来就直接用Tomcat,但没想过一个问题:为什么我的Java Web应用不能像普通Java程序一样直接java -jar或者java Main.class跑起来?为什么非要塞进一个Tomcat里才能被浏览器访问?
答案在于:一个Web应用本质上要处理的是"HTTP请求",而不是单纯的"方法调用"。浏览器发来一个HTTP请求,里面带着URL、请求头、Cookie、参数,你的应用需要解析这些信息,找到对应的业务逻辑去执行,再把结果包装成HTML返回。这一整套流程如果每个应用都自己手写,那太痛苦了。Servlet规范就定了一个标准:你只需要写一个继承HttpServlet的类,实现它的doGet、doPost方法,剩下的解析、封装、调用这些事情全交给Servlet容器来做。
Tomcat就是这样一个"Servlet容器实现"。它负责的事情包括:
- 监听端口,接收HTTP请求
- 解析请求,创建
HttpServletRequest和HttpServletResponse对象 - 根据URL找到对应的Servlet类,加载并实例化它
- 调用Servlet的业务方法
- 管理Servlet的生命周期(初始化、服务、销毁)
- 管理会话(Session)、处理并发、支持JSP解析
所以Tomcat不是一个Web服务器那么简单,严格来说它叫"Servlet容器 + HTTP服务器"二合一。你可以只用它当静态文件服务器,但它的核心价值是让Servlet和JSP跑起来。
打个比喻:Servlet就是你写的业务代码,Tomcat是那个负责接待客人(HTTP请求)、安排服务员(线程)、管理餐厅库存(会话状态)的餐厅经理。你只需要把菜做好(写业务逻辑),剩下的事经理全包了。
1.2 什么时候用独立Tomcat,什么时候用内嵌的
现在有个很普遍的情况:很多人用Spring Boot框架开发,启动方式就是mvn spring-boot:run或者打包后在命令行java -jar app.jar,里面内嵌了一个Tomcat,你甚至感受不到它的存在。那为啥还要懂独立部署?
我的经验是分场景来看。开发调试阶段,内嵌确实方便,改完代码重启一下就完事。但一旦到了生产环境,区别就出来了:
- 独立Tomcat更适合多应用共存的场景。比如一台服务器上同时跑三个Web系统,内嵌的方式就得开三个Java进程,各占各的内存和端口,管理起来很分散;独立Tomcat在配置里开三个
Context或三个虚拟主机就搞定了。 - 独立Tomcat方便统一管理连接数、JVM参数、日志策略。所有应用的连接池、线程配置都集中在同一份server.xml里,运维同学一看就懂。
- 有些老项目用的SSH框架或者纯JSP项目,压根不支持内嵌运行方式,必须打war包扔进Tomcat的webapps目录。
所以我个人观点是:内嵌适合开发和新项目快速交付,独立Tomcat适合生产环境稳定部署和运维管理。不要觉得学会了内嵌就万事大吉,独立Tomcat这套逻辑你真的绕不开。
1.3 Tomcat版本那么多,怎么选才不会踩坑
Tomcat的版本号看起来乱,实际就三条主线:8.5、9.0、10.1(以及更新的11.x)。不同版本对应的JDK版本和Servlet规范不同。
| Tomcat版本 | 对应Servlet规范 | 最低JDK版本 | 适合场景 |
|---|---|---|---|
| Tomcat 7 | Servlet 3.0 | JDK 6 | 太老,千万别再用 |
| Tomcat 8.5 | Servlet 3.1 | JDK 7 | 老项目的经典搭配 |
| Tomcat 9 | Servlet 4.0 | JDK 8 | 最常见的生产选择 |
| Tomcat 10.1 | Servlet 5.0 | JDK 11 | 新项目就用它 |
| Tomcat 11 | Servlet 6.0 | JDK 17 | 实验性/特新项目 |
这里面有一个极易踩的坑:Tomcat 10开始包名变了。原来的javax.servlet全改成了jakarta.servlet。如果你从旧项目拷贝一个war包直接扔进Tomcat 10,大概率直接报ClassNotFoundException: javax.servlet.*。所以在选择Tomcat版本时,先确认你的项目依赖的是javax还是jakarta。这也是为什么很多公司明明有新版本,却死守Tomcat 9的原因——老代码迁移的成本太高了。
2. 环境准备与安装:从下载到启动一条龙
2.1 JDK安装与JAVA_HOME配置要点
Tomcat是Java程序,所以它自己跑起来也需要一个JDK或者JRE。安装之前建议先检查一下本机Java版本:
java -version如果没装或者版本太低,先去官网装一个合适的JDK。注意不是装完就完事,环境变量必须配置好。Tomcat启动脚本(startup.sh / startup.bat)会去读JAVA_HOME这个系统变量,读不到就直接报"找不到JAVA_HOME"退出。
Windows系统配置环境变量的方式是:右键"此电脑" → 属性 → 高级系统设置 → 环境变量。新增一个变量JAVA_HOME,值是你的JDK安装目录(注意不要带后面的\bin)。然后在Path变量末尾追加%JAVA_HOME%\bin。
Linux系统则在/etc/profile或者/root/.bashrc里面写:
export JAVA_HOME=/usr/local/jdk-17 export PATH=$JAVA_HOME/bin:$PATH配完后在命令行输入java -version能正常输出版本信息就说明环境没问题。有个细节容易忽略:如果你装了多个JDK,一定要检查java -version指向的版本和你设置的JAVA_HOME一致。有时候系统里早就装了一个旧JDK在别的路径,导致Tomcat启动时用了错误的Java版本。
注意:配置完环境变量后,新开的终端窗口才会生效。不要在旧窗口里浪费时间排查"为什么改了不生效"。
2.2 下载Tomcat与目录摆放的讲究
下载Tomcat没什么难度,去官网找对应的"Binary Distributions",选tar.gz(Linux)或zip(Windows)。不要下src,那是源码包。
下载完解压后,建议放一个不含空格的路径,比如Linux下的/opt/tomcat、Windows下的D:\tomcat9。路径带空格的坑在配置CATALINA_HOME、执行脚本时都可能出现,极其烦人。
顺便解释一下CATALINA_HOME:Tomcat内部通过这个环境变量来定位它自己。启动脚本都会引用它,比如运行/opt/tomcat/bin/catalina.sh时,脚本里的逻辑会用到$CATALINA_HOME/bin、$CATALINA_HOME/conf。你可以不显式配置,Tomcat会自动根据脚本所在位置推导,但如果挪动了目录或者用服务方式启动,显式配一颗更稳。方法跟JAVA_HOME一样:
export CATALINA_HOME=/opt/tomcat2.3 启动、关闭与启动失败的快速判断
Linux启动Tomcat,执行:
cd /opt/tomcat/bin ./startup.shWindows则是双击startup.bat。正常启动后控制台会输出"Tomcat started"类似的话。
但是!这里有个极易误导人的点:startup.sh出来说"启动成功"不代表一切OK了。Tomcat的日志输出在logs/catalina.out(Linux)或者logs/catalina.log(Windows)。遇到"started"但浏览器访问不了的情况,第一步永远应该是去看日志文件,尤其是最后几百行。我见过很多人卡在"为什么启动成功但访问404"上,结果一看日志早就有异常了。
验证Tomcat是否真正起来,最直观的方式:
curl http://localhost:8080/如果返回了Tomcat默认首页的HTML,说明进程已经正常监听8080端口。如果curl都没反应,大概率进程压根没起来(可能是端口被占用,也可能是JVM参数配错直接启动失败)。
关闭Tomcat用shutdown.sh/shutdown.bat。强制杀进程的方式是ps -ef | grep tomcat找到PID后kill,但这不是常规路,顺序关不掉才考虑硬杀。
经验之谈:Linux下如果报"Permission denied",是bin目录下的.sh脚本没有执行权限。执行
chmod +x /opt/tomcat/bin/*.sh就好,这种问题在Windows下不会遇到,在Linux下几乎必踩一次。
3. 目录结构拆解:Tomcat每个文件夹是干嘛的
3.1 bin与conf:决定Tomcat"能不能跑"和"怎么跑"
bin目录存放的是启动/关闭脚本和Tomcat自身依赖的一些类。除了startup.sh、shutdown.sh,还有Linux下运维常用的catalina.sh。这个脚本的重要之处在于:你配置JVM参数、JMX监控、调试端口时,改的都是它。
conf目录是整个Tomcat的"大脑枢纽",配置文件不多,但个个都管核心事:
server.xml:主配置,端口、虚拟主机、连接器、上下文全在这。web.xml:全局的Servlet映射规则、MIME类型映射、欢迎文件(欢迎页面)的默认设置。context.xml:全局数据源、资源定义的配置。tomcat-users.xml:管理Tomcat后台管理页面的用户和角色。catalina.properties:包扫描路径、字符集等内部属性。
我建议新手先把server.xml完整读一遍,标记出每一块的用途,再动手改配置。很多莫名其妙的问题,根源都是对conf文件里某个节点含义一知半解改坏了。
3.2 webapps与work:你的应用在这里被处理
webapps是部署目录。你把war包丢进去,Tomcat会自动解压成同名目录,并把它注册为一个Web应用(Context)。也可以直接放一个已经解压好的文件夹,效果一样。
一个常见的问题是:一个war包在Tomcat里对应一个应用,应用路径默认就是war包的名字。比如你部署一个demo.war,访问路径就是http://localhost:8080/demo/。如果你希望应用直接跑在根路径(http://localhost:8080/),可以把war包改名为ROOT.war,或者把解压后的目录改名为ROOT。这是Tomcat的特例规则。
work目录是Tomcat的工作目录。JSP文件首次被访问时,Tomcat会把它翻译成Java源文件,再编译成Class文件,这些中间产物就放在work/Catalina/hostname/应用名/下。一个常见现象:你修改了JSP后刷新浏览器发现没变化,除了浏览器缓存之外,也可能是work目录里的旧Class缓存导致。删掉整个work目录然后重启Tomcat能解决大部分JSP不生效的灵异问题。
3.3 logs与lib:日志、依赖和那些"看不见但离不开"的文件
logs目录存放Tomcat运行日志。典型文件包括:
catalina.out:主输出日志,控制台打印的内容都在这里。localhost.log:应用级别日志,有时候异常堆栈在这里而不在catalina.out。manager.log、host-manager.log:管理后台的操作日志。
排查问题时的经验是:先看catalina.out的结尾,然后grep异常关键词,再针对性搜localhost.log。很多人在catalina.out里找不到关键异常,是因为Tomcat把不同类别的输出分开写了。
lib目录存放Tomcat运行期依赖的jar包。这里有一个所有人都该知道的禁忌:不要随便往lib目录里扔你自己的jar包。Tomcat的类加载策略是"父加载器优先",你往全局lib里塞一个版本不对的依赖,可能污染所有应用。该放应用的WEB-INF/lib就放应用自己的目录,别偷懒。
4. server.xml主配置逐行解析:端口、域名、连接器一次摸透
4.1 整体结构:Server、Service、Engine、Host、Context的层级关系
打开server.xml,第一眼会觉得标签嵌套得眼花缭乱。实际拆开以后就是一个树状结构:
<Server> <Service> <Connector /> <Engine> <Host> <Context /> </Host> </Engine> </Service> </Server>我用一句话解释它们的关系:Server是一个独立的Tomcat实例;Service是这个实例内部提供的一组服务;Connector是服务对外开的门(端口);Engine是请求处理引擎,负责分发;Host是虚拟主机,类似Nginx里的server块,区分不同域名;Context是一个具体的Web应用实例。
正常情况下,一个Tomcat实例就一个Server,一个Server里一个Service,一个Service里一到两个Connector(HTTP和AJP),一个Engine下若干Host,一个Host下若干Context。你把它想成一个公司(Server)有一个销售部(Service),销售部有几个对外窗口(Connector),窗口接待的客户按区域分给不同销售小组(Host),每个销售小组手里管着不同的客户项目(Context),一下子就清晰了。
4.2 Connector节点:端口、协议与连接参数详解
Connector是server.xml里改动最频繁的节点。我先贴一个最常用到的HTTP/1.1连接器配置:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="200" acceptCount="100" URIEncoding="UTF-8" />逐个说含义:
port:监听端口。改端口改这里,比如改成80。protocol:用的协议。历史版本里曾出现过protocol="HTTP/1.1"和protocol="org.apache.coyote.http11.Http11NioProtocol"两种写法,效果大体相同,现在的版本直接用HTTP/1.1即可。NIO和新版本的NIO2是默认IO模型,无需手动指定。connectionTimeout:建立连接后,如果客户端一直不发请求,等待多少毫秒后断开。设太短会出现"稍微想想就断线",设太长会占用服务端资源。20000毫秒(20秒)是个中庸值。redirectPort:当请求是HTTPS但当前Connector是HTTP时,重定向到哪个端口。配合8443这个默认HTTPS端口。maxThreads:处理请求的最大线程数。这个反而不是越大越好,取决于CPU核数和业务耗时,后文调优环节细说。acceptCount:当线程池打满后,还能排队等多少请求。超出这个数量会拒绝连接并报错。URIEncoding:URL的字符编码。如果你在地址栏传中文参数(比如?name=张三),不设置这个经常会出现乱码。建议写成"UTF-8"。
一个极易踩的坑:把port改成80后程序启动直接失败。很大原因是80端口已经被Nginx或者Apache占用了,你需要先检查端口被谁占用,再考虑改端口。不要为了"让用户不用输入端口"就直接改80,生产环境常见的做法是80端口交给Nginx,然后Nginx反向代理转发到Tomcat的8080端口。
4.3 Engine与Host:域名、默认站点与应用路径的关系
<Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> </Host> </Engine>Engine是整个处理引擎的名字,默认值Catalina不用动。它的defaultHost属性非常关键:当客户端请求的域名没有匹配到任何Host时,请求就会落到这个默认Host上。
Host可以配多个,实现"一台Tomcat跑多个域名"。比如:
<Host name="www.example.com" appBase="/data/app1" ... /> <Host name="admin.example.com" appBase="/data/app2" ... />appBase是应用目录的根路径。注意它和Context的关系:Host顺着appBase找Web应用(war包或目录),而Context是在路径映射层面精确指定一个个应用。
unpackWARs设为true表示war包自动解压成目录,"true"在开发阶段方便但部署太多应用时会稍微增加启动时间。autoDeploy设为true表示Tomcat会定时扫描appBase,发现新的war包就自动部署,生产环境如果希望严格手动控制,可以设false。
4.4 一个多Context应用的真实配置示例
现在假设你开发了一个后台管理系统,war包叫admin.war,但你希望访问路径展示为/backend,而不是默认的/admin。有两种做法:
第一种,直接把war包改名为backend.war,简单粗暴,路径就变成/backend。
第二种,在conf/Catalina/localhost/目录下新建一个XML文件,文件名就叫backend.xml,内容:
<Context docBase="/data/apps/admin.war" reloadable="true" />这样Tomcat启动时会读取这个XML文件,自动注册一个路径为/backend的Context,指向/data/apps/admin.war。好处是war包可以放在任意磁盘目录,不必在appBase里面,特别适合那种"war包很大,不想拷贝多份"的场景。
这种外部Context文件也支持数据源定义,可以给特定应用配专属数据库连接池:
<Context docBase="/data/apps/admin.war" reloadable="true"> <Resource name="jdbc/mysql" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" maxWaitMillis="10000" username="root" password="你的密码" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/admin_db?useUnicode=true&characterEncoding=UTF-8"/> </Context>在应用里通过jndi方式查这个数据源就可以了,连接池直接由Tomcat来管理,不需要在应用代码里反复创建连接。这个用法比自己在代码里写连接池干净不少,尤其多项目共用一套Tomcat时特别实用。
5. 四种Web应用部署方式实操对比:从war包到热部署
5.1 最朴素的部署:复制war包到webapps目录
这是最直白的方式,没有任何技术含量但也是用的最多的。
操作步骤:
- 把打好的
xxx.war复制到webapps目录 - 启动Tomcat(或者如果已经启动,等它自动扫描检测到新war包)
- Tomcat会自动解压war包成同名目录
- 浏览器访问
http://localhost:8080/xxx/
关键点:war包怎么打出来的?使用Maven的话执行mvn clean package,产物在target/xxx.war。如果是手里只有文件夹,最原始的方式是:
cd 你的项目目录 jar cvf xxx.war *jar是JDK自带工具,cvf是参数:create、verbose、file。这样打出来的war包不一定带正确的目录结构,如果缺WEB-INF/web.xml之类Tomcat会拒绝部署哦。所以更推荐用Maven或构建工具去打包,手动打容易出幺蛾子。
5.2 Manager图形界面部署:适合临时管理
Tomcat自带一个管理后台。要启用它,先编辑conf/tomcat-users.xml,添加一个管理员角色:
<role rolename="manager-gui"/> <role rolename="manager-script"/> <user username="admin" password="admin123" roles="manager-gui,manager-script"/>manager-gui角色可以用图形界面上传war包;manager-script角色允许你用命令行/脚本方式调用管理接口。配置完重启Tomcat,访问http://localhost:8080/manager/html,输入账号密码,就可以在页面上传war包、启停应用、查看应用状态了。
这个方式的好处是零命令操作,浏览器点点就好。坏处是:管理后台暴露出去有安全风险,生产环境建议关闭或者至少限制来源IP。Tomcat的manager页面默认只允许本机访问,如果你从远程访问会报403,需要在webapps/manager/META-INF/context.xml里修改Valve的allow字段,比如:
<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.0\.0\.1|192\.168\.\d+\.\d+" />这个改动有风险,操作前先想清楚你的安全策略。我一般不推荐生产环境开启这个页面,除非你确定只有内网能访问。
5.3 server.xml配置Context:精确指定应用目录
前面提到过,在conf/Catalina/localhost/下新建XML文件,或者直接在server.xml的Host节点内写<Context>,都能注册特定路径的应用。
直接写在server.xml里的方式:
<Host name="localhost" appBase="webapps"> <Context path="/backend" docBase="/data/apps/admin.war" reloadable="false" /> </Host>path是访问路径,docBase是实际war包或目录的位置。这种方式比war包放到webapps更灵活,适合应用目录不在webapps下的场景。但它的缺点是在server.xml里写死了应用路径,每次部署新版本都要改server.xml然后重启,且不适合改动频繁的项目。现在推荐的做法已经是外部Context文件了,可以避免修改server.xml。
5.4 IDE热部署:开发调试阶段的利器
用IDE(比如主流的Java IDE)开发时,项目可以直接以"Tomcat运行"的方式启动,修改代码后按热部署按钮(更新资源和类)让IDE把新Class和资源文件推送到Tomcat里,不需要重启Tomcat。这个功能非常适合写Controller和JSP频繁的阶段,能极大提高开发效率。
但你要知道背后的机制:IDE不是把代码编译成war包再丢给Tomcat,它是在本地启动了一个Tomcat实例,然后把你的项目路径作为Context的docBase挂上去。相当于你的IDE替你管理了配置过程。你改了Java文件,IDE可以编译好类文件并热替换;你改了JSP,Tomcat的work目录会重新生成Class。
这套调试方式非常爽,但它也掩盖了很多部署细节,导致很多新人对"项目是怎么跑在Tomcat里的"没概念。我建议你用IDE跑通一次之后,再手动部署一次war包,把两种方式都经历一遍,实际理解会更上一层楼。
6. 高频问题排查速查:端口占用、404、启动失败一网打尽
6.1 端口被占用:启动时报"Address already in use: JVM_Bind"
这是最常见的启动失败原因。你改了端口,结果端口被别的进程占着,Tomcat起不来。
排查手段:
# Linux查看8080端口被谁占用 lsof -i :8080 netstat -tlnp | grep 8080 # Windows查看8080端口占用 netstat -ano | findstr 8080拿到占用的进程PID之后,Linux用ps -ef | grep PID看看是哪个进程,Windows用任务管理器找PID。如果是别的Tomcat实例在占,把那个先关掉;如果是别的不认识的程序,谨慎判断能不能结束。
很多人在这个坑里反复跌的原因:明明把server.xml监听端口改了,结果启动的还是旧端口。这种情况是因为你启动的根本不是同一个Tomcat,比如机器上装了两份Tomcat,或IDE里配置的Tomcat路径跟你在命令行操作的路径不一致。排查时先echo $CATALINA_HOME,再确认你执行startup.sh的绝对路径,别搞混了。
6.2 启动一闪而过:先学会看日志
Windows双击startup.bat,窗口黑屏一闪就消失,这是新手最绝望的一刻。原因是脚本报错后窗口自动关闭,你根本看不到错误信息。
解决思路很简单:不要双击bat,改用命令行方式手动执行。
cd D:\tomcat9\bin catalina.bat run这样拿到的错误信息就会完整打印在控制台里。同样,Linux下如果startup.sh异常退出,直接执行:
/opt/tomcat/bin/catalina.sh runrun参数是前台运行模式,所有的启动日志都会实时输出。排查启动类问题的首选就是它。
常见的前台启动报错包括:
The CATALINA_HOME environment variable is not defined correctly:JAVA_HOME或CATALINA_HOME配置有问题。Failed to initialize end point associated with ProtocolHandler:端口相关,通常是端口被占用。Caused by: java.lang.OutOfMemoryError: PermGen space/Metaspace:内存不够,需要调JVM参数。LifecycleException: Failed to start component ... Filter:某个Filter启动失败,常见于应用的web.xml里配置了不存在的类。
6.3 页面404:应用路径、上下文路径、欢迎文件三个方向排查
部署成功后访问404,这个问题的定位路径比较固定。先分四种情况:
第一,访问http://localhost:8080/显示404。这可能是根路径没有任何应用(webapps/ROOT没了),也可能是欢迎文件配置问题。
第二,访问http://localhost:8080/demo/显示404。确认webapps下是否真的有demo.war或demo目录;如果war包还没解压完就刷新,偶尔会短暂404。
第三,访问一个具体的Controller地址报404。这种情况多半是应用内部路径映射的问题——你部署成功了,但你的Servlet映射/Spring MVC的@RequestMapping路径输入错了。责任在应用本身,不在Tomcat配置。
第四,Context路径对不上。比如你改名为ROOT.war却还想用/ROOT访问,路径对不上自然会404。
还有一个容易被忽略的点:conf/web.xml里的欢迎文件配置。Tomcat默认在访问目录时顺序寻找index.html、index.htm、index.jsp。如果你的应用首页文件名不是这些,比如login.html,那你访问http://localhost:8080/demo/就是404,必须完整输入http://localhost:8080/demo/login.html才能打开。这种情况可以通过配置欢迎文件解决。
6.4 中文乱码:请求参数和搜索引擎参数一起排查
中文乱码分两类:请求参数乱码和响应内容乱码。
请求参数乱码的根源在于URL编码和解码的字符集不一致。浏览器发送请求时用页面字符集编码URL,Tomcat解析时用URIEncoding,二者不一致就乱码。解决方案很明确:在Connector上加URIEncoding="UTF-8"。
还有一种是POST表单乱码。Servlet API收到POST请求时,需要手动设置:
request.setCharacterEncoding("UTF-8");如果你用Spring MVC,内置的CharacterEncodingFilter已经帮你做了,只需要在web.xml注册这个Filter。
响应乱码通常就是Content-Type的头没带charset,或者JSP页面头部的pageEncoding写的不对。一个全局兜底的方案是修改conf/web.xml,把所有常用MIME类型里加上UTF-8的charset,或者在每个Response里统一设置。
经验:如果改完Tomcat配置后乱码依然存在,优先检查数据库连接串里有没有
characterEncoding,很多时候乱码根本不是Tomcat的问题,而是JDBC连接时没声明UTF-8。
6.5 JSP修改不生效:work目录的缓存陷阱
开发阶段改动JSP文件后,刷新浏览器看到的还是旧页面。这个问题有两个常见原因。
第一个是浏览器缓存。按Ctrl+F5强制刷新,或者打开无痕窗口看看。确认不是浏览器缓存再往下查。
第二个是work目录缓存。JSP文件在Tomcat里会被编译成Java源文件再编译成Class,而且Tomcat默认会检测JSP文件的修改时间,理论上改了就重新编译。但如果系统时间错乱、文件时间戳没更新,或者某些IDE保存文件时不改变时间戳,就可能出现"改了等于没改"的错觉。
解决办法很简单:停掉Tomcat,删除整个work目录,重新启动。Tomcat会自动重新编译所有JSP。
这招在常规手段无效时最管用,属于"重启大法"的变种。顺便提一句:如果真的访问量不大,把JSP页面做修改后直接重启Tomcat是最省心的,不要在这方面花太多时间。
7. 性能调优与生产避坑心得:线程池、连接数与JVM参数
7.1 连接器线程参数:不要盲目调大maxThreads
很多人的第一个调优动作是把maxThreads从默认的200调到1000,以为这样就能扛更多并发。实际效果经常适得其反。
先理解线程的工作原理:Tomcat每收到一个请求,会从一个线程池里分配线程执行Servlet代码。一个线程就是一个栈内存区域,默认每个请求线程占用可能大几百KB到1MB的虚拟机栈空间。1000个线程光空转就占了近1G内存,加上每个线程切换CPU的代价,CPU多了很多上下文切换开销。
正确的姿势是结合业务看:如果你的服务是CPU密集型(比如数据计算、加解密),线程数建议设为CPU核数的1~2倍;如果是纯IO密集型(比如大量查数据库、调第三方接口),线程数可以多些,但也不是无脑加,配合压测来找拐点。
推荐初始配置:
<Connector port="8080" protocol="HTTP/1.1" maxThreads="200" minSpareThreads="20" acceptCount="100" connectionTimeout="20000" redirectPort="8443" />压测工具可以用ab(Apache自带的压测命令):
ab -n 10000 -c 100 http://localhost:8080/demo/-n是总请求数,-c是并发数。跑完看Requests per second和Time per request,再调整参数做对比。数据永远比预估靠谱。
7.2 JVM内存参数:catalina.sh里的JAVA_OPTS
Tomcat默认JVM内存虽然能用,但如果你部署的应用比较大,跑一段时间后会出现OutOfMemoryError: Java heap space或者Metaspace异常。
在catalina.sh里找到JAVA_OPTS那一行,添加参数:
JAVA_OPTS="-server -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8"简单解释:
-Xms512m:堆内存初始大小。-Xmx1024m:堆内存最大值,这两个不要完全相等,因为Tomcat需要一部分非堆内存,但别相差太远,避免频繁扩张收缩带来的性能抖动。生产上常把两者设为一致。-XX:MetaspaceSize:初始元空间值,一般256M起步。-Dfile.encoding=UTF-8:强制默认文件编码,配合前面说的中文乱码预防。
改完JVM参数需要重启Tomcat才能生效。可以在启动后查看:
ps -ef | grep java看重启后的进程命令行里是否包含了这些参数。或者通过jmap -heap PID来确认堆内存配置。
7.3 生产环境必做的三个加固动作
生产环境部署时,有几件小事你很容易忽略,但长期运行后都会变成事故导火索。
第一,禁用没用的AJP连接器。早期版本的server.xml里默认有两个Connector:HTTP/1.1的8080和一个AJP的8009。如果你不用AJP协议(配合Apache HTTP Server时才会用到),建议把它注释掉。安全的角度讲,AJP端口应该被严格防火墙过滤甚至直接关闭,避免不必要的外部暴露面。
第二,设置合理的session超时时间。默认session有效时间是30分钟。如果你的应用不是那种需要长时间保持登录状态的,可以调短一些以减少无谓的session占用内存。改在conf/web.xml:
<session-config> <session-timeout>30</session-timeout> </session-config>单位是分钟,设0表示浏览器关闭就失效,30分钟比较常见。
第三,日志切割。Tomcat自身的日志不会自动切割,运行久了catalina.out可能变成好几个G。一种解决方案是配置系统日志轮转工具如logrotate,另一种是通过catalina.sh启用按天切割的日志策略。至少要把这个事儿提上日程,否则某天排查问题时你连tail都不敢执行,因为文件太大了。
7.4 进程崩溃和假死:为什么memory和GC比cpu占用更重要
Tomcat"假死"的经典现象:进程还在,端口还在,但请求全部超时,页面根本刷不开。这时候很多人习惯性看CPU占用率,发现很低,那问题多半就不在CPU。
优先检查内存和GC。推荐一个快速排查方式,用JDK自带的jstat:
jstat -gcutil PID 1000持续输出里的FGC(Full GC次数)、FGCT(Full GC时间)、O(老年代使用率)如果能看出来老年代长期90%以上且Full GC频繁,那多半是内存泄漏或者堆太小。再配合jmap -dump:format=b,file=/tmp/heap.bin PID导出一份堆转储文件做分析。
做了几年运维和开发,我自己的体会是:Tomcat出问题,90%都出在内存层,而不是并发逻辑。GC导致的停顿、堆内存溢出、Metaspace泄漏,这类问题有一个共同点——它不像端口冲突那么直接,你只能靠着日志和监控工具的蛛丝马迹去定位。所以建议从一开始就给Tomcat配上GC日志参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/opt/tomcat/logs/gc.log等到出问题的时候再临时加参数,你已经错过了事故现场的证据链。GC日志看起来枯燥,但在关键时刻它就是你的救命稻草。
8. 写在最后的一些私货
最后说点教程之外的个人体会。
Tomcat这东西本质上不复杂,但它足够老、足够成熟,所以牵涉到的知识点特别碎。端口、目录、部署方式、连接器、JVM参数、日志,每一项都不难,但串起来以后就形成了一套很庞大的经验体系。很多刚入行的人觉得Tomcat没什么好学的,能启动能部署war包就算会了——这种心态会在第一线上突然出事时让你手足无措。
我自己的学习路径是:先手动部署一个war包走通全流程,然后完整读一遍server.xml,再亲手配置两个虚拟主机和多个Context,最后配合压测工具去调整线程参数,把JVM参数从默认值改到合理值,再跑一轮压测做对比。这个过程走完,你才算真正"用过"Tomcat。不要停留在"能用"的舒适区,多想一想"为什么它能这样工作",你真的会少踩很多坑。