拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Tomcat部署前后端分离项目:从war包到资源托管全攻略

Tomcat部署前后端分离项目:从war包到资源托管全攻略

1. 部署前的基础认知:Tomcat 与前后端分离项目

前后端分离项目本地部署这件事,说简单也简单,说麻烦是真麻烦。很多人第一次接触时,脑子里默认“前后端分离”就是把前端打包后丢进 Tomcat 的 webapps 里,再把后端接口地址指过去——结果启动后访问前端页面发现一堆资源 404,或者接口全部跨域报错,半天排查不出头绪。

先把这个项目到底要解决什么问题说清楚:这是一个基于 Tomcat 的本地部署实践,核心目标有两个,一是让 Spring Boot 后端以 war 包形式跑在外部 Tomcat 上,二是把前端构建产物(比如 Vue 打包后的 dist 目录)以一种合理的方式交给 Tomcat 托管,最终实现前后端在本地环境里的联通访问。适合谁来参考?如果你是刚接触前后端分离开发、想把本地联调环境弄得更贴近生产环境的人,或者你所在团队的生产服务器还在用传统 Servlet 容器而非内嵌 Tomcat,这篇内容就是写给你的。

我之所以强调“用外部 Tomcat 而非 Spring Boot 内嵌 Tomcat”,是因为很多人在本地开发时直接java -jar app.jar就能跑,但一旦涉及部署到老一批服务器、或需要运维用统一脚本管理多个 Web 应用时,外部 Tomcat 依然是绕不开的环节。而且外部 Tomcat 的坑——端口冲突、编码乱码、内存溢出、war 包热更新不生效——基本是固定那么几个,踩过一次就记住了。

在动手之前,你得先想清楚一个问题:你的前后端分离项目,准备用哪种方案放进 Tomcat?这事不提前想清楚,后面全是返工。

2. 三种部署思路,先选好再动手

2.1 方案一:纯净前后端分离,各自独立部署

后端 Spring Boot 项目打成 war 包,扔进 Tomcat 的webapps目录;前端 Vue/React 项目构建出的 dist 目录,也丢进webapps下的另一个目录(比如webapps/ROOT或webapps/front)。前端通过绝对路径请求后端接口,比如http://localhost:8080/api/user/list。

这个方案的优点是结构清晰,前后端互不干扰,后端升级时只需替换 war 包,前端改版时只替换静态文件。缺点是必须解决跨域问题,因为前端页面地址是http://localhost:8080/front/index.html,后端接口地址是http://localhost:8080/api/...,虽然同一个端口,但如果前端静态资源和后端接口不在同一个 Servlet 上下文里,浏览器依然视其为跨域请求。

2.2 方案二:前端打入后端 war 包,单包部署

把前端构建后的 dist 目录里的文件,复制到后端项目的src/main/resources/static目录下,然后整个后端打成 war 包。这样 Tomcat 只需部署一个 war,前端页面和后端接口同源,没有跨域问题。

优点是部署最简单、本地联调最省心;缺点是前后端耦合,前端每次改动都要重新进后端项目里复制文件、重新打包,开发效率低。适合小项目、演示项目或临时联调。

2.3 方案三:后端 war 包 + 前端通过 Tomcat 虚拟目录映射

Tomcat 支持在conf/server.xml里通过<Context>标签配置虚拟目录,把磁盘上的前端 dist 目录直接映射为一个访问路径。这样做的好处是前端构建产物不用复制进 webapps,构建完刷新就能生效;缺点是 server.xml 是全局配置,不同团队协作时容易各改各的、冲突不断。

我个人在本地部署时最常用的是方案一,理由很简单:它最接近真实生产环境的前后端分离架构。生产环境里前端往往用 Nginx 托管,后端用 Tomcat,本地用方案一模拟的是“前端静态服务 + 后端接口服务”的分离状态,后续迁移生产时遇到的坑会更少。方案二适合赶时间的时候用,方案三我基本只在调试静态资源路径问题时临时启用。

想清楚用哪个方案之后,下一步就是搭建一个可靠的本地 Tomcat 环境。

3. 本地 Tomcat 环境准备:下载、目录与配置

3.1 下载与版本选择

Tomcat 下载地址是官方站点,注意别去第三方下载站。选择版本时有个基本原则:Tomcat 的主版本要和 JDK 版本匹配。比如 JDK 8 配 Tomcat 8.5/9.0 都没问题;JDK 11 建议 Tomcat 9.0+;JDK 17 则建议 Tomcat 10.1+,因为 Tomcat 10 开始用 Jakarta EE 命名空间,javax.servlet变成了jakarta.servlet,Spring Boot 2.x 用的还是javax,直接扔到 Tomcat 10 里会启动失败。

这里有个很多人踩过的坑:Spring Boot 2.x 项目打包 war 后,部署到 Tomcat 10 会报ClassNotFoundException: javax.servlet.Filter。解决办法要么升级到 Spring Boot 3.x(它已经切换为jakarta.servlet),要么老老实实用 Tomcat 9。所以选版本前,先确认你项目的 Spring Boot 大版本,再反推 Tomcat 版本,顺序千万别反了。

Windows 用户下载 zip 包,解压即用;macOS/Linux 用户下载 tar.gz,解压后把目录放到固定位置,比如~/dev/tomcat/apache-tomcat-9.0.98。解压完成第一步就是把 bin 目录下的catalina.bat(Windows)或catalina.sh(Linux)打开,检查JAVA_HOME是否指向 JDK 路径,如果没设置,启动时会直接报错。

3.2 核心目录结构速览

刚接触 Tomcat 的人容易把文件到处乱放,其实它的目录结构非常固定:

  • bin:启动、关闭脚本(startup.sh/startup.bat、shutdown.sh/shutdown.bat)
  • conf:全局配置文件,重点是server.xml、web.xml、tomcat-users.xml
  • lib:Tomcat 自身的 jar 包,不要把你的项目依赖 jar 扔进来
  • logs:运行日志,排错的第一现场
  • webapps:部署目录,war 包或解压后的应用放这里
  • work:JSP 编译后的临时文件目录,遇到“改了代码不生效”可以先清这里

3.3 server.xml 里我最常改的三处

conf/server.xml是 Tomcat 的核心配置文件,但绝大多数情况下你只需要关注三个点:

端口配置。默认 HTTP 端口是 8080,如果被占用,把<Connector port="8080" protocol="HTTP/1.1" ... />里的端口号改掉。注意一次改三处:HTTP 端口、redirectPort、以及Server port="8005"(这是 Tomcat 的关闭指令端口)。只改 HTTP 端口不改Server port容易出幺蛾子。

编码配置。在 HTTP Connector 标签里加上URIEncoding="UTF-8"。本地部署前后端分离项目时,URL 参数里如果带中文——比如搜索关键词——不加这个配置,Tomcat 默认按 ISO-8859-1 解析请求参数,直接乱码。

部署目录配置。<Host name="localhost" appBase="webapps" ...>里的appBase指向 webapps 目录。如果你希望前端静态资源从外部磁盘目录加载,可以在这个 Host 节点下加<Context path="/front" docBase="/Users/me/project/front/dist" reloadable="true" />,这样访问http://localhost:8080/front/就是直接访问那个磁盘目录,不用复制文件。

3.4 启动脚本与 JVM 参数

Windows 下用startup.bat启动,macOS/Linux 下用startup.sh。但很多人的本地项目在 Tomcat 里跑起来后,控制台中文乱码,这大概率不是代码问题,而是 JVM 默认字符集跟终端不一致。可以在catalina.sh的JAVA_OPTS里加上:

JAVA_OPTS="-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8"

Windows 下还需要注意conf/logging.properties里java.util.logging.ConsoleHandler.encoding,默认是 UTF-8,但如果你的 Windows 控制台代码页是 GBK,日志照样乱码,把它改成GBK,或者把控制台代码页切到 UTF-8 都行。这块我后面在问题排查章节会展开讲。

4. IDEA 中配置 Tomcat:本地部署的第一步

4.1 关联本地 Tomcat

用 IDEA 做本地部署,我强烈建议先在 IDEA 里把 Tomcat 关联起来,这样调试时能直接看到堆栈信息,不用靠猜。

打开 IDEA,进入File -> Settings -> Build, Execution, Deployment -> Application Servers,点击+选择Tomcat Server,然后在Tomcat Home里选择你本地 Tomcat 解压目录,Tomcat base directory通常会自动识别,保持默认即可。这里有一点要注意:IDEA 必须使用和项目 JDK 匹配的 Tomcat 版本,否则部署时 IDEA 会提示版本不兼容。

4.2 创建 Artifact:war 还是 war exploded

Tomcat 部署项目前,IDEA 要求你指定一个 Artifact。对 war 包部署,有两种形态:

  • war:完整的 war 包,部署时 Tomcat 会解压,适合最终要扔到服务器上的场景
  • war exploded:展开的目录结构,IDEA 直接把编译后的target/classes和静态资源映射给 Tomcat,适合本地开发调试

本地调试我用war exploded居多,因为它配合reloadable="true"能实现改代码后自动热部署,不用每次重启 Tomcat。IDEA 打开Project Structure -> Artifacts -> + -> Web Application: Exploded -> From Modules,选你对应的模块就行。

4.3 配置 Deployment 上下文路径

IDEA 的 Run/Debug Configurations 里,Tomcat Server 的Deployment选项卡,Application context默认是/,也就是访问http://localhost:8080/直接进项目根。先后端联调时,建议设置成有意义的上下文路径,比如/api,这样后端接口地址就是http://localhost:8080/api/xxx,前端代理或请求路径也更清晰。

设置上下文路径时,一定要和后端项目的server.servlet.context-path(Spring Boot 配置)保持一致,否则会出现“接口访问 404 但配置文件里明明写了路径”的诡异情况。我在实际项目里就见过有人 IDEA 的 Deployment 上下文写了/api,但 Spring Boot 的application.yml里也写了context-path: /api,结果访问http://localhost:8080/api/api/login才通——这就是双重前缀叠加,排查了半小时才反应过来。

4.4 通过 IDEA 启动 Tomcat 后看不到日志

IDEA 启动 Tomcat 后,底部Console窗口应该能看到清晰的启动日志。如果你点了启动按钮后,IDEA 面板里什么日志都没有,多半是Tomcat Server -> Logs里的日志级别或控制台配置问题。进入 Run 配置,找到Logs选项卡,勾选Show console when a message is printed to stdout,然后把Tomcat Localhost Log、Tomcat Catalina Log、Tomcat Manager Log点成可用的状态,日志就会正常输出了。

5. 后端改造:从 Spring Boot 内嵌容器到外部 Tomcat

5.1 改造步骤:jar 转 war 的三个关键点

如果你原本是java -jar启动,要改成部署到外部 Tomcat,Spring Boot 项目需要做三处改动,缺一不可。

第一处:打包方式改为 war。在pom.xml的<packaging>标签里,把jar改为war。

第二处:排除内嵌 Tomcat。Spring Boot 默认依赖内嵌 Tomcat,如果不排除,部署到外部 Tomcat 时同一个应用会有两个 Tomcat 在抢。在spring-boot-starter-web依赖里用<exclusions>排除spring-boot-starter-tomcat:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>

这里有个容易忽略的点:排除内嵌 Tomcat 后,本地用java -jar直接跑会报NoClassDefFoundError: ServletWebServerFactory,因为内嵌容器没了。解决办法是给provided作用域加上 Tomcat 依赖,让它只在编译时参与,运行时不打包进去:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>

第三处:修改启动类。主类要继承SpringBootServletInitializer并重写configure方法。这一步的底层逻辑是:外部 Tomcat 启动时不会像java -jar那样运行main方法,它需要通过SpringBootServletInitializer这个类来引导 Spring 容器启动。onStartup钩子会由容器调用,进而触发 Spring ApplicationContext 的初始化。注意这里继承的是哪个 Spring Boot 版本对应的类——Spring Boot 2.x 里这个类在org.springframework.boot.web.servlet.support包下,别误导成 1.x 的org.springframework.boot.web.support包。

@SpringBootApplication public class Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(Application.class); } }

5.2 外部 Tomcat 下配置文件的加载顺序

改成 war 包部署后,配置文件的加载规则会变化。java -jar启动时,application.yml在 classpath 根目录;war 包部署时,配置文件同样在WEB-INF/classes下,如果application.yml在src/main/resources里,打包后会在正确位置,所以大部分情况下无需额外处理。

但如果你部署到不同环境,要覆盖配置,优先级和 Java 进程启动不一样。外部 Tomcat 部署时,常见做法是把外置配置文件放在 Tomcat 的conf目录下,然后通过JAVA_OPTS指定:

JAVA_OPTS="-Dspring.config.additional-location=/opt/tomcat/conf/application-external.yml"

这里的spring.config.additional-location是 Spring Boot 2.4+ 的用法,它会把外置配置追加到默认配置之后,优先级更高。如果你用的是更早版本,可以直接用--spring.config.location覆盖默认路径,但这样默认的application.yml就不会被加载了,容易把系统配置项弄丢。

5.3 数据库配置加密:本地部署的安全底线

本地部署前后端分离项目时,很多人习惯把数据库用户名密码明文写在application.yml里。本地自测无所谓,但如果这个项目要分享给同事或打包给其他人部署,明文密码就很不体面。

推荐的做法是用jasypt-spring-boot-starter做配置加密。在application.yml里用ENC(...)标识加密后的密码,然后在启动参数里传入解密密钥:

JAVA_OPTS="-Djasypt.encryptor.password=mySecretKey"

这个工具的坑主要体现在两点:一是密钥必须和加密时使用的完全一致,否则启动直接报Decryption failed;二是某些 MySQL 驱动版本对连接串里的特殊字符敏感,加密时如果明文密码里有+、/、=这类字符,加密结果里通常是大写字母和数字,不会出问题,但要保证最终连接串本身没有非法转义。这个内容展开讲又是一篇文章,本地部署阶段只需记住:密钥永远不要提交进仓库,用.env或本机环境变量管理。

5.4 war 包名称与上下文路径的关系

当你把打好的 war 包放进webapps目录后,Tomcat 默认会把 war 包的文件名作为上下文路径。比如文件名是myapp.war,访问地址就是http://localhost:8080/myapp/。如果希望根路径直接访问,要么把 war 包命名为ROOT.war,要么把原来的ROOT目录删掉再替换。

这里有个很实际的建议:不要指望通过修改 server.xml 的path属性来改变上下文路径,虽然 Tomcat 在早期版本支持这么做,但 Tomcat 9 之后对<Context path>属性的约束越来越严格,部署在 Host 下的 Context 不允许指定path属性,否则启动时直接抛异常。正规做法就是让 war 包的名字决定路径。IDEA 里部署war exploded时,如果在 Deployment 配置里指定了 Application context,那么 IDEA 启动时通常会自动调整部署名称,这时候上下文路径由 IDEA 决定,跟 war 包名字无关。

6. 前端部署:构建产物如何交给 Tomcat

6.1 Vue/React 构建前的关键配置

前端项目在构建之前,必须处理接口请求的路径。以 Vue 项目为例,开发环境下用的代理配置.env.development里常常是VUE_APP_BASE_API = '/api',这个相对路径在部署后依然有效——只要前端静态资源和后端接口在同一个域下。

但如果是独立部署,前后端不同端口,构建时就得用绝对路径或完整 URL。这里我踩过一次很深的坑:前端项目构建时,publicPath如果配置成/,部署到 Tomcat 的/front/目录下,访问前端页面时所有 JS/CSS 资源都会从根路径加载,结果 404。解决办法是把publicPath设置为相对路径或者加上子路径。

以 Vue CLI 为例:

module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/front/' : '/' }

如果是 Vite 项目,则是:

export default defineConfig({ base: process.env.NODE_ENV === 'production' ? '/front/' : '/' })

这个配置的本质是:构建产物里的资源引用前缀必须和部署后的访问路径一致。你部署在/front/下,资源就写成/front/static/js/xxx.js,否则浏览器拿着/static/js/xxx.js去请求,Tomcat 里根本没有这个路径。

6.2 部署到 Tomcat 的三种形态对比

部署形态操作方式优点缺点
dist 内容复制到 webapps/ROOT每次构建后手动覆盖 ROOT 目录访问路径简洁(根路径直接进)需要人工操作,易出错
dist 整体复制为 webapps/front复制目录到 webapps 下路径清晰,可多个前端并存上下文路径需要统一管理
server.xml 虚拟目录映射修改 server.xml,docBase 指向磁盘目录前端构建完直接生效多人协作时配置易冲突

我本地最常用的是第二种:webapps/front。好处在于,Tomcat 本来就是多应用部署的,webapps/front和webapps/api可以同时存在,路径一眼就看出哪里是静态资源、哪里是后端接口。生产环境迁到 Nginx 时,也只需要把front目录原样搬到 Nginx 的 html 目录下。

6.3 前端跨域的三大解法

前后端分离部署到 Tomcat 后,跨域是最容易冒出来的问题。所谓跨域,本质是浏览器的同源策略在拦截,不是 Tomcat 在拦截。请求发出去了,服务器也收到了,也会正常返回,但浏览器发现响应头里缺少允许跨域的字段,直接就吞掉了响应,前端看到的就是blocked by CORS policy。

第一层解法:后端开启 CORS。Spring Boot 里写一个全局配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

第二层解法:前端通过代理转发。这个方案更适合在开发环境下使用,生产环境意义不大,因为代理转发的是开发服务器的地址,而部署到 Tomcat 后前端静态资源已经不在 Node 服务下了。

第三层解法:把前后端放在同源下。也就是方案二里说的,前端构建产物的 JS/CSS 走后端静态资源路径,或者用 Tomcat 的虚拟目录让前端页面和后端接口同端口同域名。这就是为什么很多人明明没配跨域,把前端打进 war 包后反而不报错了——不是跨域被解决了,而是请求本来就是同源的。

我的经验是:本地部署阶段,后端开 CORS 最省事;生产环境,优先想办法同源,别人家 Nginx 在外层做反代或转发。

7. 完整部署实操:从零到能访问

7.1 后端打包与上传

在项目根目录执行:

mvn clean package -DskipTests

打包完成后,在target目录下会生成你的项目名.war。这个 war 包内部结构是标准的 Web 应用结构:WEB-INF/classes放着编译后的 class 和资源配置文件,WEB-INF/lib放着项目依赖的 jar 包。检查一下WEB-INF/classes/application.yml是否存在——如果这里没有配置文件,war 包启动后会使用默认配置,可能连数据库都连不上。

把 war 包复制到 Tomcat 的webapps目录,命名要谨慎。假设我想让它以/api路径访问,就把 war 包改名为api.war。接着启动 Tomcat:

cd bin ./startup.sh

启动后观察logs/catalina.out日志文件,看到Deployed application at context path [/api]基本就说明后端部署成功。

7.2 前端构建与部署

以 Vue 项目为例:

npm install npm run build

构建完成后,dist目录里是index.html、static(或assets)等静态文件。在 Tomcat 的webapps下新建front目录,把dist里的内容全部复制进去:

cp -R dist/* apache-tomcat-9.0.98/webapps/front/

然后启动或重启 Tomcat,访问http://localhost:8080/front/,能看到前端页面。如果页面白屏但后端接口正常,先按 F12 看 Network 面板,确认 JS/CSS 资源的请求路径是不是 404,路径 404 就回到上一节讲到的publicPath问题。

7.3 联调验证三步走

部署完之后,不要急着到处点功能,按这个顺序验证:

  1. 先直接访问一个后端接口,确认后端接口本身可用。比如curl "http://localhost:8080/api/user/list",看返回的 JSON 数据是否正常。
  2. 再访问前端页面,确认页面能正常渲染,资源不 404。
  3. 最后在页面上触发一个调用后端接口的操作,观察 Network 面板,看请求的 URL、请求方法、请求头是否符合预期。

第 3 步里最容易暴露问题的是OPTIONS预检请求。前端的 POST 请求如果带了Content-Type: application/json这种非简单请求头,浏览器会先发一个OPTIONS预检,如果你的后端接口没有正确处理OPTIONS,预检失败,后续真实请求根本不会发出,前端看到的报错是Failed to fetch而不是 403/500。这个坑特别隐蔽,因为有些后端的鉴权拦截器会对所有请求做认证,直接 401 掉OPTIONS预检,结果前端怎么调都失败,后端日志却只有一行OPTIONS /xxx 401。

7.4 用 Tomcat Manager 管理应用

如果你需要频繁更新 war 包,建议配置 Tomcat Manager 应用。编辑conf/tomcat-users.xml,加入:

<role rolename="manager-gui"/> <role rolename="manager-script"/> <user username="admin" password="admin123" roles="manager-gui,manager-script"/>

然后访问http://localhost:8080/manager/html,可以可视化地在页面上启动、停止、重新部署应用,不用每次手动复制 war 包再重启 Tomcat。注意manager-gui权限只能从本机访问,manager-script允许脚本远程调用,如果只是本地用,两个都配到本机就够了。

多提一句:Tomcat Manager 的manager-script接口允许通过 HTTP 方式部署 war 包:

curl -u admin:admin123 "http://localhost:8080/manager/text/deploy?path=/api&update=true" --upload-file api.war

这个命令可以在不重启 Tomcat 的情况下热更新 war 包,本地调试时比每次重启快得多。

8. 常见问题与排查技巧实录

把本地部署 Tomcat 时经常遇到、且网上答案往往只讲一半的问题整理在这。每个案例都是我实测过或亲眼见过的,有通用性。

8.1 启动后访问 404

Tomcat 启动后访问http://localhost:8080/xxx报 404,排查顺序要固定,不要瞎猜。

第一步,确认应用是否真的部署了,查看logs/catalina.out和logs/localhost.2025-xx-xx.log。后者专门记录应用部署情况,如果有Exception sending context initialized event之类的信息,说明应用启动中抛出异常,通常页面会显示 Servlet 容器报错页,而不会只是纯 404。

第二步,确认上下文路径。如果你的接口地址是http://localhost:8080/api/login,war 包却是myapp.war,那么实际路径是http://localhost:8080/myapp/login,这就是 404 的最常见原因。

第三步,检查静态资源配置。有时应用能访问/api/xxx,但页面或者静态资源请求 404,看看 Spring Boot 的spring.mvc.static-path-pattern配置是否覆盖到了前端资源路径。

8.2 Tomcat 乱码

乱码问题分成两类:启动日志乱码和页面响应乱码。

启动日志乱码,尤其是 Windows 下,主要原因是日志输出编码和控制台编码不一致。Tomcat 9 的conf/logging.properties里默认ConsoleHandler.encoding = UTF-8,而 Windows 控制台大多数情况是 GBK,双方不匹配就会出现中文注释或中文日志乱码。解决办法是改成:

java.util.logging.ConsoleHandler.encoding = GBK

页面响应乱码,先检查是不是 HTTP 响应头没有指定 charset。Spring Boot 项目通常在application.yml里设置server.servlet.encoding.force=true,这样所有响应用 UTF-8 编码。如果还不行,用浏览器的开发者工具看 Network 面板里响应头的Content-Type: text/html;charset=UTF-8是否正确。

还有一个隐蔽的乱码点在 Tomcat 解析 URL 参数上。前面说过URIEncoding="UTF-8",这个配置必须加在 Connector 上,否则 GET 请求里的中文参数乱码是必现的。POST 请求体乱码则由CharacterEncodingFilter管理,Spring Boot 默认装配了这个过滤器,一般不会出问题。

8.3 端口被占用

启动时提示Address already in use: JVM_Bind或者<null>:8080,说明 8080 被其他程序占了。Windows 下用:

netstat -ano | findstr 8080

Linux/macOS 下用:

lsof -i :8080

找到占用进程的 PID,结束它,或者改 Tomcat 的端口。这里有个容易忽视的场景:上一次 Tomcat 没有正常关闭,残留了 java 进程。比如用 Ctrl+C 强制终止 IDEA 里的 Tomcat,另一个窗口里的 Tomcat 进程可能还活着,端口占着不放。这种情况直接kill -9残留进程就行。

8.4 内存溢出:PermGen 与 Metaspace

本地部署多项目后,Tomcat 经常会出现内存溢出。老的 JDK 8 之前是java.lang.OutOfMemoryError: PermGen space;JDK 8+ 用元空间,报的是Metaspace。

在catalina.sh的JAVA_OPTS里显式设置内存参数:

JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m"

-Xms是初始堆大小,-Xmx是最大堆大小,这两个值不相等时,JVM 会动态调整堆大小,在高并发下可能频繁触发 Full GC。本地调试时直接让两者相等,能减少 GC 停顿带来的干扰。MaxMetaspaceSize控制元空间上限,类加载过多时不够用就报溢出,设个 256m 基本够单项目跑。

8.5 修改了代码但不生效

这是最鬼的问题,看起来像“改了 Java 代码,重启 Tomcat 后还是老逻辑”。排查顺序:

第一,确认 IDE 是否执行了Build。IDEA 里的 Tomcat 跑的是编译后的 class,如果你改了代码没有触发编译,Tomcat 加载的还是旧 class。执行Build -> Rebuild Project再重启试试。

第二,清理 Tomcat 的work目录。JSP 编译缓存在这里,但 Spring Boot 项目的接口代码不走 JSP,所以问题一般不是缓存。真正可能缓存的是 IDE 部署时的临时副本。IDEA 部署war exploded时,如果 output directory 和你的 target 目录不一致,改代码后 IDEA 没有把新的编译结果同步过去,也会出现“没生效”的假象。检查 IDEA Structure 里 Artifact 的 Output directory,确认它指向的是你 Maven 编译的 target 目录。

第三,确认浏览器缓存。前端改了接口地址或代码,HTML/JS 被浏览器缓存,刷新页面加载的还是旧脚本,在开发者工具里勾选 Disable cache,或者用 Ctrl+F5 强制刷新。

8.6 查看 Tomcat 进程状态的实用命令

很多人在排错时喜欢一上来就改配置,我建议先确认运行时状态。Linux/macOS 下:

ps -ef | grep tomcat

这个命令能看到 Tomcat 进程的启动参数,比如-Dcatalina.home指向哪个目录、-Dspring.config.location有没有生效。如果这些参数配置不对,配置了但没加载的排查就是白忙活。

如果在 macOS 上出现“Tomcat 启动后立即停止,没有任何日志”,大概率是$CATALINA_HOME环境变量没设。设置方式:

export CATALINA_HOME=/路径/到/apache-tomcat-9.0.98

Windows 上则是系统变量里加CATALINA_HOME,并在Path中加入%CATALINA_HOME%\bin。

9. 几个一直想强调的细节

上面讲的都是操作层面的内容,最后再补充几个我反复踩过、但经常被人忽略的细节。

第一,war 包的运行环境是独立于 IDE 的。你在 IDEA 里跑得很顺的项目,打成 war 扔进 Tomcat 后,如果发现某些配置没生效,先在启动日志里确认 Tomcat 加载的到底是哪个目录下的配置文件。外部 Tomcat 启动时,user.dir属性指向的是 Tomcat 的bin目录,而不是你的项目目录,所以一切依赖相对路径的配置文件都会以 Tomcat 目录为基准。

第二,本地部署和生产部署的差异要提前摸底。Tomcat 本地部署倒是顺利,但生产环境往往有自己的运维规范——可能限定了 JVM 参数、可能要求用 systemd 服务管理,可能不允许改动 server.xml。我在本地部署时经常会提前把生产环境常用的启动参数、日志轮转配置一并测一遍,省得到生产环境再来回折腾。

第三,war 包部署后的热更新并不完全可靠。Tomcat Manager 的热部署在更新 war 包时会尝试重新加载应用的 ClassLoader,但如果你的应用里有用到第三方库的静态状态、或者自己写了ThreadLocal缓存数据,热部署后状态可能丢失或残留,出现“更新后第一次访问报错,再刷新就好了”这种诡异情况。本地调试没问题,生产环境更新 war 包时最好还是选择一个低峰期重启。

第四,前后端分离部署时的日志要能串起来。本地部署前后端分离项目,前端页面上报错,后端日志里不一定有对应记录,因为跨域请求的Origin头、前端报的 400/500 错误跟后端的异常栈不一定同时出现在同一个终端窗口。我习惯在本地部署时,给后端的 Controller 层加一个简单的请求日志切面,把每个请求的 URL、参数、耗时打出来,前端出了问题直接翻后端日志看有没有请求进来——这一招排查跨域或参数问题特别有效。

第五,Tomcat 10 和 Spring Boot 的版本矩阵,值得在项目开始前就定下来。如果你们项目是 Spring Boot 2.x,局部用 Tomcat 9;如果项目已经升级到 Spring Boot 3.x,那 Tomcat 10.1+ 没问题。这事别拖到部署阶段才想起来,因为发布包一旦打成 war,内部依赖的 Servlet API 版本跟 Tomcat 不完全匹配时,启动过程会报各种奇怪异常,紧赶慢赶再改版本很可能来不及。

本地部署前后端分离项目本身不難,难点在于把“能跑”变成“稳定地跑、可排查地跑”。你把这些基础工作做扎实了,无论后面是上生产还是交给同事维护,都会省心很多。

返回列表