1. Tomcat的本质先搞清楚它到底在替你干嘛说起Tomcat搞Java Web的几乎没有不知道的。但你要是拦住一个刚入门的朋友问一句Tomcat里到底有哪些核心组件大概率得到的答案是端口8080或者webapps文件夹。这不怪谁因为平时我们用IDEA一键部署、点个绿色小三角就完事Tomcat内部的运转机制对我们来说是透明的。可一旦线上出问题比如并发上不去、新项目刚启动就闪退、war包怎么都部署不上去这时候再不理解Tomcat的组件结构排查起来就是两眼一抹黑。Tomcat这东西本质上是两个身份的合体。第一身份是Servlet容器也就是说它负责加载、管理、执行ServletJava Web里的Controller、Filter、Listener最终都是运行在它管理的容器里第二身份是HTTP服务器它能监听端口、接收HTTP请求、解析请求报文再交给Servlet处理完了把响应包送回客户端。理解了这两个身份Tomcat的核心组件其实就围绕着怎么把HTTP请求接进来、怎么找到对应的处理逻辑、怎么把结果发回去这条主线展开。为什么理解组件这么重要我给你说个真实的场景。之前有朋友部署了一个应用刚开始访问正常跑了一个多月以后突然频繁报错说连接池爆了、响应超时。他查了很久业务代码都没发现问题最后发现是Tomcat默认配置的线程数太少高并发下请求全堵在队列里。这就是典型的不了解Connector线程模型导致的线上事故。你只有知道Tomcat的请求处理链路是由哪些组件承担的才能在最合适的环节去做调整。这篇文章不打算把所有犄角旮旯的细节都翻一遍我会把那些真正影响你日常开发、部署、排查的核心组件讲透该给配置给配置该讲原理讲原理。2. 一眼看穿Tomcat的家谱从Server到Wrapper的嵌套逻辑2.1 最顶层的Server和Service是什么关系Tomcat的组件模型是一层套一层的很像俄罗斯套娃。最外面那个壳叫Server一个Server代表一个完整的Tomcat实例也就是你启动的那个进程。每个Server里面可以包含多个ServiceService这个名字有点抽象你可以把它理解成一组干活的人马。一个Service由两部分组成一个或多个Connector负责接收请求以及一个Engine负责真正处理请求。平时我们启动一个标准Tomcat默认就是一个名为Catalina的Service在干活。你可能见过conf/server.xml里那个Service nameCatalina标签Catalina这个名字其实是Tomcat内部组件库的代号。注意一台机器上可以同时跑多个Tomcat实例多个Server进程但那就需要在不同端口和不同目录上做隔离。而单个Server内部搞多个Service的场景相对少除非你想让同一份容器同时监听HTTP和HTTPS各自走不同的协议处理链路。2.2 Connector和Container一个接客一个干活Service内部的两位主角一个是Connector一个是Container。Connector负责接客——它蹲在指定的端口上等着HTTP请求上门然后把请求报文解析成一堆Java对象比如HttpServletRequest再交给Container去处理。Container这个统称下面还藏着一串子组件按层级往下排是Engine、Host、Context、Wrapper。这一条链是Tomcat里最核心的处理管道所有请求最终都要从Engine一路下沉到某个具体的Wrapper再由Wrapper去执行对应的Servlet逻辑。我打个比方Engine就像是总公司不管哪个城市的业务进来先过总公司这关Host像分公司每个分公司管一类域名虚拟主机Context像分公司里的具体部门一个部门对应一个Web应用Wrapper就是部门里真正干活的员工——一个Servlet实例。每次请求进来等于从总公司一路往下点名最后点名到某个具体的员工头上。2.3 组件之间的层级关系用server.xml看最直观Tomcat的conf/server.xml文件把上面这套嵌套关系写得很清楚。标准配置缩略下来是这个样子的Server Service nameCatalina Connector port8080 protocolHTTP/1.1/ Engine nameCatalina defaultHostlocalhost Host namelocalhost Context path/myapp docBasemyapp/ /Host /Engine /Service /Server层级关系一眼就能看明白Server包含ServiceService把Connector和Engine包在一起Engine下面挂着HostHost下面挂着Context。你平时部署war包实际上就是为某个Host下面多了一个Context。理解了这张图后面改配置、加虚拟主机、加应用都不会迷路。3. 逐个拆解关键组件这些才是你真正需要关心的3.1 Connector端口怎么选、协议怎么定、线程池怎么调Connector是整个Tomcat的门面也是最容易被我们直接触碰到的组件因为改端口就是改它。先看一段常见的生产配置Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 minSpareThreads25 acceptCount100/这里面每个属性都值得琢磨。port不用说了就是监听端口注意一台机器同一个端口只能被一个进程占用这就是Tomcat启动报Port already in use的原因。protocol决定Tomcat用哪种方式解析HTTP协议老版本常见的是HTTP/1.1对应BIO或NIO模式看版本新版本默认走NIO并发能力比BIO强好几个量级。maxThreads是连接器线程池里最多同时处理请求的线程数。这个参数很关键它决定了同一时刻能并行处理多少个请求。如果设置太小请求就要排队设置太大CPU频繁切换线程反而变慢。我之前调过一个接口maxThreads从默认的200调到400配合长连接超时优化吞吐量涨了将近一倍但再往上调就没意义了因为瓶颈转移到了数据库。acceptCount表示请求排队的队列长度。当线程池全忙的时候新的请求先进入这个队列等着队列也满了就直接拒绝连接。生产环境一般给100到200如果qps压测时大量报Connection refused多半是maxThreads和acceptCount都压满了。3.2 Container四兄弟从Engine到Wrapper的向下点名Connector把请求接进来之后就轮到Container家族登场了。先说Engine。Engine是容器链的最顶层它本身不做具体业务处理主要职责是拿到请求后判断该交给哪个Host。一个Service里只有一个Engineengine的defaultHost属性指定了当请求的域名匹配不到任何Host时默认丢给谁。Host代表虚拟主机。平时我们用http://localhost:8080访问走的就是localhost这个Host。如果你想让一台Tomcat同时跑aaa.com和bbb.com两个网站即使两个站点共用IP和端口也可以通过配置两个Host来区分。Host的appBase属性指定这个主机下存放Web应用的基础目录默认就是webapps。Context代表一个Web应用是很多排查工作真正关注的层级。每个Context可以被一个pathURL访问路径唯一标识。举个例子Context path/order docBaseorder/表示访问http://localhost:8080/order时就走order这个应用。Wrapper是容器链最底层的组件它负责管理一个Servlet实例。一个Wrapper对应一个Servlet类包括Servlet的加载、初始化、执行、销毁全生命周期。平时我们写的Controller最终就是被某个Wrapper包起来管理的。你可以在web.xml里或者通过注解WebServlet配置Servlet的映射Tomcat启动时会构建好Wrapper和URL的映射关系请求来了才能快速定位。3.3 藏在应用里的Servlet、Filter、Listener上面说的都是Tomcat自身的组件而运行在你应用里的Servlet、Filter、Listener则属于应用级组件。它们虽然不是Tomcat服务器的内建组件但是通过Servlet标准接口和Tomcat深度绑定所以你拆解Tomcat组件时它们绝对绕不开。Servlet处理请求的核心逻辑单元。现在大家开发基本不直接写Servlet了而是通过Spring MVC的DispatcherServlet间接使用。Filter过滤器。请求进Servlet之前先过一遍过滤器链可以做登录校验、编码设置、日志记录响应出去之前也能拦截处理。Filter的执行顺序和它在web.xml/注解里声明的顺序一致。Listener监听器。监听Tomcat或Web应用的生命周期事件比如应用启动、停止、Session创建、属性变化等常用于在应用启动时加载缓存、初始化线程池。这三个家伙配合起来几乎覆盖了一个Web应用从请求进入到结果返回的所有旁路逻辑。理解Tomcat时千万别把它们漏掉因为很多灵异现象比如过滤器莫名其妙执行了两遍其实都跟这批组件的生命周期有关。3.4 请求从进入到返回完整走一遍为了把上面这些组件串起来我们跟一个请求走一遍完整链路。用户在浏览器输入http://localhost:8080/order/list请求先到达Tomcat的ConnectorConnector在8080端口上把HTTP报文解析成Java的请求对象。接着请求进入EngineEngine看请求的域名是localhost就转交给名为localhost的Host。Host查看请求路径发现/order前缀对应order应用于是把请求交给这个Context。Context内部再根据/list路径找到对应的Servlet映射也就是某个Wrapper最终调用这个Servlet的service()方法进入你的业务代码。业务返回结果后数据沿着这条链原路返回经过Filter如果有、Context、Host、Engine、Connector最后通过HTTP响应报文回到浏览器。顺着这条路去看就能回答很多问题。比如为什么修改了server.xml要重启Tomcat因为组件层级是在启动时构建好的运行期间不会重新解析配置文件。为什么应用部署了war而访问不到大概率是Context的映射路径和你期望的URL路径不一致。这些组件模型不是考试用的死概念而是日常排障的地图。4. 实战里的组件配置从安装、JVM参数到IDEA配置4.1 安装和环境变量JDK 25配Tomcat要注意什么热词里有个jdk 25 tomcat安装配置这个组合其实是比较新的。Tomcat对JDK版本有要求不同版本的Tomcat支持的最低JDK版本不同。比如Tomcat 10.1最低要求JDK 11Tomcat 11最低要求JDK 17。如果你用的是JDK 25这种特别新的版本我建议优先选择Tomcat 11或更新版本避免出现编译级别不匹配、某些类库不兼容的问题。安装本身不复杂去官网下载对应系统架构的压缩包Linux服务器就解压到/opt/tomcat之类的目录然后配置环境变量CATALINA_HOME指向这个目录。Windows下通常还会配CATALINA_BASE这两个变量要分清CATALINA_HOME是Tomcat安装目录CATALINA_BASE是实例的运行目录。开发单实例无所谓但生产环境如果做多实例运行用CATALINA_BASE隔离各个实例的配置和部署目录是最干净的做法。有个细节容易踩坑解压后bin目录下的startup.shWindows是startup.bat启动脚本默认去$CATALINA_HOME/bin下找catalina.sh如果环境变量没配好脚本会直接退出但没有任何明显的提示看起来就像启动了个寂寞。先跑一下echo $CATALINA_HOME确认目录存在再执行启动能省很多时间。4.2 设置JVM参数别只改catalina.sh一处以前经常帮朋友看线上Tomcat性能问题很多人的JVM参数设置方式都不对。最常见的是直接修改catalina.sh里的JAVA_OPTS这也行但更推荐的做法是把参数写进setenv.shWindows对应setenv.bat。Tomcat启动时会自动检测这个文件如果存在就执行它再启动。这样你升级Tomcat版本时只要保留setenv.sh配置就不会丢。一个比较完善的JVM参数模板是这样的CATALINA_OPTS-Xms1024m -Xmx1024m -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:UseG1GC -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/tomcat/logs/heapdump.hprof这里的-Xms和-Xmx我一般设置成一样避免JVM在运行期动态扩容堆大小减少GC停顿。MetaspaceSize是元空间初始大小类加载多的应用建议512m起步不然频繁触发Full GC。生产环境最好加上-XX:HeapDumpOnOutOfMemoryError这样OOM的时候会自动生成堆转储文件后面排查内存泄漏全靠它。还有一个和JVM参数相关的热词是tomcat启动设置jvm参数。除了启动脚本IDEA里也有单独的配置入口主要是用于本地调试Run/Debug Configurations里选中Tomcat Server在VM options栏填入-Xmx512m之类的参数这个只对IDEA启动的实例生效不会影响命令行启动的Tomcat。4.3 IDEA和VSCode里怎么配TomcatIDEA里配置Tomcat本质上就是告诉IDEA三件事Tomcat装在哪、用哪个JDK启动、部署哪个应用。菜单路径是File - Settings - Build, Execution, Deployment - Application Servers点加号选Tomcat填上CATALINA_HOME路径。然后创建Run Configuration在Deployment标签页里把当前项目打包成war或者exploded war添加进去Application context设置成访问路径比如填/order访问就是http://localhost:8080/order。热词里有tomcat配置idea和idea配置tomcat两个说明这确实是新手高频操作。我见过不少人在IDEA里点了运行控制台刷了一堆日志但浏览器打不开页面多半是Application context配了个奇怪的路径或者端口被其他程序占了。VSCode里配置Tomcat通常是靠扩展插件完成的比如Tomcat for Java这个插件。装好后在活动栏找到Tomcat图标添加Tomcat路径然后给项目创建一个launch配置指定tomcat.port和tomcat.uriEncoding等参数。VSCode的路数比IDEA轻量适合临时调试但真要搞复杂部署还是IDEA或者直接命令行更舒服。4.4 多应用部署Context,HOST与war上传限制热词里有一条tomcat后台页面上传war被限制ip这说的是Tomcat Manager这个管理后台。默认情况下Manager只允许本机回环地址访问也就是127.0.0.1和localhost。想远程上传war需要改conf/manager.xml或者context.xml里的Valve配置把allow改成你的IP比如Context path/manager docBase${catalina.base}/webapps/manager Valve classNameorg.apache.catalina.valves.RemoteAddrValve allow127\.0\.0\.1|192\.168\.1\.100/ /Context这里要特别提醒生产环境的Manager尽量别开远程访问。有这个需求的一般是内网环境而且开这个前最好确认你的IP段足够收敛。远程部署更推荐的方式是用tomcat-maven-plugin或者写个CI脚本走HTTP API或者直接用curl调Manager接口比手动浏览器上传可控得多。还有一种是部署多个应用时的端口选择。如果你一台服务器要跑多个应用但不想加端口暴露面常规做法是用同一个Tomcat给不同应用配不同Context路径。如果应用之间隔离要求高或者要跑不同JDK版本编译的程序那就干脆起多个Tomcat实例用不同的CATALINA_BASE目录端口也错开。5. 启动和运行中的高频坑闪退、端口占用、双亲委派5.1 Tomcat闪退先从日志找真相tomcat闪退大概是所有Tomcat新手都遇到过的噩梦。在Windows上双击startup.bat窗口一闪就没了好像什么事都没发生。这时候第一反应不是谷歌而是去logs目录下翻catalina.202x-xx-xx.log或者localhost.202x-xx-xx.log。闪退最普遍的原因有这几个端口被占用。启动时报java.net.BindException大概率是8080被别的进程占了。用netstat -ano | findstr 8080查占用PID再确认是什么程序。JAVA_HOME配置错误。Tomcat启动时找不到正确的JDK路径脚本直接退出。Windows下尤其要注意JAVA_HOME别带分号路径别带空格。内存不足。如果启动日志里能看到Could not reserve enough space for object heap说明-Xmx给得太大了超过本机物理内存或虚拟机的可用内存。权限问题。Linux下startup.sh没有执行权限也会一闪而过这时先chmod x再跑。闪退场景下日志文件比控制台信息有价值得多。我习惯不管出什么问题先tail -100 logs/catalina.out看一眼大部分答案都在最后100行里。5.2 双亲委派机制Tomcat为什么非要打破它热词里tomcat打破双亲委派机制是个相当有深度的点。JVM默认的类加载机制是双亲委派一个类加载器先让父加载器尝试加载父加载器找不到才轮到子加载器去加载。这样做是为了保证核心类库的安全避免java.lang.String被自定义类替换。但Tomcat偏偏要打破它原因很现实一个Tomcat里能跑多个Web应用如果应用A依赖commons-lang的2.4版本应用B依赖3.0版本双亲委派机制下两个版本没法共存——因为类加载器发现类已经被自己加载过了或者父加载器加载过了不会再次加载。于是Tomcat给每个Web应用创建了一个独立的WebAppClassLoader它不再严格遵守先父后子的顺序而是优先加载自己WEB-INF/classes和WEB-INF/lib下的类。如果自己这里没有才委托给父加载器。用大白话说双亲委派是先问长辈长辈不会才自己来Tomcat的WebAppClassLoader是先自己来自己真不会才问长辈。这个打破是怎么实现的关键在findClass和loadClass方法的顺序重写WebAppClassLoader重写了loadClass方法先尝试用findLoadedClass查缓存再尝试findClass找自己目录下的类找不到才走super.loadClass。这就是打破的实际含义。理解这个机制有什么用排查类冲突的时候特别有用。比如你在Tomcat里跑两个应用一个用了socket.io相关库另一个用了不同版本的netty就可能出现NoClassDefFoundError或ClassCastException。这时候你要意识到是类加载隔离没做好两个应用的依赖互相干扰了解决办法通常是把公共的、版本冲突的Jar放到Tomcat的lib目录或者反过来强制让它们各自隔离。5.3 修改配置后不生效也许只是没清理缓存还有一个高频现象改了代码、改了web.xml重启Tomcat后仍然跑旧逻辑。这种情况大概率不是Tomcat组件的问题而是work目录下的缓存没清。Tomcat会把JSP编译后的class文件缓存在work/catalina/localhost对应的应用目录下。JSP改动后如果编译缓存没刷新Tomcat可能继续跑旧的class。遇到JSP相关改动不生效直接清掉work目录再重启是最高效的方案。类似的还有lib目录的Jar替换。有时候你往CATALINA_HOME/lib里放了一个新版本的Jar以为重启就生效结果还是跑老版本。检查一下是不是有多个位置放了同名Jar——Tomcat自带的lib目录和应用自己的WEB-INF/lib都能被加载同名类的加载顺序会直接影响实际生效的版本。这个问题在类加载机制那一节已经提过这里是它最典型的落地场景。6. 按需调整不同场景下的组件配置侧重6.1 开发环境图省事但要保留基本分层本地开发时很多人的Tomcat配置就是默认的能用就行。我的建议是至少做两件事一是把maxThreads稍微调大一点防止本地调试时请求稍微多一点就排队二是确保URIEncoding设置为UTF-8不然GET请求带中文参数分分钟乱码。前者改Connector后者改Server配置都是几分钟的事但能省掉后续大量灵异问题的排查时间。6.2 测试环境要能还原线上问题测试环境一般会尽量贴近生产。Connecttor的线程数、超时时间、JVM堆大小、GC策略这些尽量和生产保持一致。如果测试环境的maxThreads和生产不一样压测结果根本没有参考价值。另外测试环境建议开启Tomcat JMX这样能用VisualVM之类的工具直接看线程数、内存、类加载情况提前发现问题。6.3 生产环境并发、安全和日志一个不能少生产环境的关注点更偏向稳定性。Connector要重点调线程池和超时时间建议配合压测工具逐步调整千万别一上来就拍脑袋给maxThreads1000瓶颈总在别处。安全方面除了关掉Manager远程访问还建议把server头信息隐藏掉避免暴露Tomcat版本号给外部。日志方面至少保证localhost_access_log开启方便按IP和URL统计访问量遇到问题也能回溯请求轨迹。生产环境容易出现的一个隐蔽坑是Connector的maxPostSize默认只有2MB如果业务里有大文件上传或者POST大量JSON数据的接口请求会直接被拒。需要上传大文件时把这个参数调大或者设为-1不限制不然排查半天都查不到原因。6.4 虚拟主机和端口扩展Host和Connector的组合玩法如果你想让一个Tomcat实例同时跑多个站点有两条路。一条是给同一个Connector配多个Host通过域名区分。另一条是启动多个Connector监听不同端口每个Connector可以对应不同的默认Host或应用。前者适合对外域名多的场景后者适合内部端口隔离的场景。举个实际例子公司内网有一个Tomcat要跑内部管理系统和自动化运维平台两个系统用不同的端口比如8080和8081。这时候可以在Service下面加两个Connector让它们共用同一个Engine。请求进来时Engine根据端口和域名决定交给哪个HostHost下面再挂不同应用。这个做法避免了在一台机器上起两个Tomcat实例减少内存占用管理也更方便。7. 个人经验这些坑我踩过你遇到了可以少走弯路最后分享几个我实际操作中的体会。第一个是关于修改Connector参数的很多配置看着有道理但改完效果不明显原因在于参数之间是联动的。比如只调大maxThreads但acceptCount没调线程多了队列不够照样扛不住并发。正确做法是把maxThreads、acceptCount、connectionTimeout、以及JVM堆大小放在一起调全部压测验证后再上生产。第二个是关于ClassLoader的多应用环境下类冲突是真的让人头疼。我有个项目两个应用都带了自己版本的guava部署上去以后一个应用功能正常另一个应用偶尔报NoSuchMethodError。查了一天才发现是两个应用的guava版本不一致Tomcat的WebAppClassLoader优先加载各自目录下的类但有些场景下父加载器先加载了公共lib里的guava导致某一个应用走的版本不对。解决办法是检查两边类路径把公共版本统一放到Tomcat的lib目录或者用类加载器的隔离策略强制各自加载。这类问题表面上像是业务代码bug实际是容器类加载机制在作祟。第三个是关于JVM参数生效位置的。如果你既改了JAVA_OPTS又改了CATALINA_OPTS注意这两者最后都会被拼进启动命令但CATALINA_OPTS只在执行catalina.sh start或run时使用JAVA_OPTS会传递到其他工具命令。Tomcat官方推荐把运行期参数写到CATALINA_OPTS里而不是JAVA_OPTS这样避免启动脚本执行其他命令时也莫名其妙带上大堆JVM参数。还有一个建议平时多翻翻Tomcat的logs目录。catalina.log、localhost.log、manager.log各有分工遇到问题按日志找线索比瞎猜高效太多。尤其线上环境我一般习惯把access_log打开记录请求的IP、时间、状态码和耗时很多诡异问题靠这个日志能直接定位根本不用上APM工具。这些习惯不一定多高级但都是真实项目里沉淀下来好用的东西写出来给同路人参考。