简介:面向初级与中级开发、运维人员的SpringBoot+Vue前后端分离项目Tomcat部署操作文档,详细说明了一套可复用的部署流程。内容从修改Maven打包方式开始,覆盖将后端打包为WAR包并放入Tomcat的webapps目录、构建Vue前端并将dist产物部署到指定位置、修改server.xml配置Context访问路径、处理刷新页面出现的404错误等关键环节;同时给出了web.xml和CORS跨域配置代码示例,帮助解决前端路由刷新失败与调用第三方接口被拦截的问题。文档按步骤顺序展开,对容易出错的配置点单独说明,便于读者对照完成部署,并在文末整理了常见故障的排查思路,可减少上线阶段的踩坑。资源包仅1个doc文件,大小约72KB,适合在本地打开查阅。目前已有1449人学习下载,适合正在学习前后端分离项目部署或负责中小型项目发布的工程师参考。
1. 一台 Tomcat 撑起前后端:这个部署文档解决的是什么
后端 SpringBoot 加前端 Vue,开发环境跑起来轻轻松松,但一旦要把整套系统放到一台 Linux 服务器、装进同一个 Tomcat 里,问题就来了:后端该打 jar 还是 war、前端构建产物放到哪、为什么访问页面是 404、为什么刷新一下就白屏。这些不是代码问题,而是部署结构与容器行为的匹配问题。标题里这个.doc 文档,本质就是一套「让 SpringBoot 和 Vue 共用一个 Tomcat 实例、合并部署」的可复现方案。它适合某高校毕业设计、某公司内部管理系统、个人练手项目这类不需要高并发、但要求一台服务器就能稳定跑的体量。读完这篇,你应该能独立完成从改 pom 到浏览器验证的全过程,并且知道出问题时第一步该查哪里。
2. 部署选型:war 合并部署还是前后端独立部署
2.1 jar 包内嵌 Tomcat 与外部 Tomcat 的根本差异
SpringBoot 项目默认用java -jar启动,这个 jar 包内部自带一个内嵌 Tomcat。内部 Tomcat 的端口、上下文路径全部由application.yml控制,部署时只要机器有 JDK 就能跑。而标题点名「tomcat 部署」,意味着要把应用放进一个独立安装的 Tomcat 容器里,由这个外部容器负责接收 HTTP 请求、加载应用、管理生命周期。这两者最核心的区别在于:内嵌方式下应用自己说了算,外部方式下容器说了算。外部 Tomcat 会固定使用webapps目录下的应用上下文路径,端口由server.xml决定,应用自身的server.port配置会被忽略。对 SpringBoot 项目来说,放进外部 Tomcat 就必须打 war 包,并且要在启动类上扩展SpringBootServletInitializer,否则外部容器根本不知道从哪里启动 Spring 上下文。
这里有个常见的决策误区:只要项目需要部署到 Tomcat,就盲目打成 war。实际上,如果服务器上没有强制要求使用外部 Tomcat,java -jar的方式永远更省事,因为它不需要关心容器版本、不需要处理 jar 和 war 打包带来的依赖差异、不需要手动配置 JVM 参数。那什么时候值得用外部 Tomcat?一是公司运维体系统一用 Tomcat 管理多个 Java 应用,二是需要利用 Tomcat 的manager图形界面做应用上下线,三是项目里还有纯 Servlet 或 JSP 模块需要和 SpringBoot 共存。如果只是「别人让我用 Tomcat」,建议先问清楚理由,否则就是在给自己制造不必要的部署复杂度。
2.2 合并部署 vs 前后端分离部署的取舍
SpringBoot + Vue 的部署有两种主流拓扑。第一种是前后端完全分离:后端打 war 或 jar 放进 Tomcat,Vue 构建产物放到 Nginx 里,前端通过/api反向代理到后端端口,前端页面走 Nginx 的 80/443,后端接口走 Tomcat 的 8080。第二种是合并部署:Vue 执行npm run build后,把dist目录里的文件复制到 SpringBoot 的src/main/resources/static目录,然后整体打进 war 包,Tomcat 同时提供静态页面和接口。标题这个文档大概率是合并部署方案,因为它在「Tomcat 部署」这个约束下最直观——一个 Tomcat 实例就够了,不需要再装 Nginx,前后端天然同源,没有跨域问题。
我个人的建议是:只有一台低配服务器、访问量极小、前端不需要单独灰度发布时,合并部署是对的。一旦前端要频繁改版、后端要独立扩缩容,或者需要给前端配置缓存策略和 CDN,就必须拆开走 Nginx 分离部署。判断标准很简单:问一句「前端页面更新会不会要求后端重新发包」,会的话就别合并。
3. 把 SpringBoot 打成 war 包:一份能直接抄的改造清单
3.1 修改 pom.xml 的三个关键点
第一步改packaging类型。SpringBoot 默认是jar,要改成war,这决定了 Maven 最终构建产物的格式。第二步处理内嵌 Tomcat 依赖。SpringBoot 的spring-boot-starter-web会传递引入spring-boot-starter-tomcat,打 war 包时必须把它的scope改成provided,意思是「这个 Tomcat 由外部容器提供,不需要再打进包里」。如果不改,war 包里就会同时存在内嵌 Tomcat 和外部 Tomcat,启动时很容易出现类加载冲突、端口被占或者各种莫名其妙的报错。第三步是确保spring-boot-maven-plugin存在且版本与 SpringBoot 父依赖一致,它负责生成可执行的 war 包清单。不过在外部 Tomcat 环境下,我们并不需要 war 包可执行,mvn package打出的普通 war 同样能被 Tomcat 正常加载。
<packaging>war</packaging> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <build> <finalName>myapp</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>finalName参数非常关键。在外部 Tomcat 下,war 包文件名就是访问路径的一部分,比如打进webapps后文件名叫myapp.war,那访问地址就是http://ip:8080/myapp/。如果你想用根路径直接访问(即http://ip:8080/),需要把 war 改名为ROOT.war,或者提前把文件放在webapps/ROOT目录下。我通常会在打包前就确定好上下文路径,避免部署后又因为路径不对去搬文件。provided的作用范围只影响依赖是否进入最终包,不影响编译期使用,所以代码里照常使用 Tomcat 相关类没有任何问题。
3.2 启动类必须继承 SpringBootServletInitializer
这是整个改造里最容易漏的一步。没有这一步,war 包扔进 Tomcat 后,Tomcat 能正常解压,但访问端口会一直 404,因为 Spring 的ApplicationContext从来没有被初始化。SpringBootServletInitializer是 SpringBoot 给外部 Servlet 容器准备的入口,它相当于把main方法启动时做的事,迁移到了容器启动阶段。具体来说,Tomcat 启动时会扫描每个 webapp 的ServletContainerInitializer,这个类会回调我们的配置类,我们在它提供的SpringApplicationBuilder里指明主配置类,Spring 容器就能在这个 webapp 的上下文里被创建。
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } }注意这里的事件顺序:外部 Tomcat 启动后,如果webapps下有多个 war,它们是被并行初始化的。如果某个应用初始化耗时较长,Tomcat 不会等它完成再返回响应,所以刚启动的一两分钟内访问出现「服务器未就绪」的 503 或者连接重置是正常的。另外,原主类里的main方法可以保留,也可以删掉,两者不冲突。保留的好处是本地开发依旧可以用main启动,不依赖外部容器。我见过有人把main方法里的启动逻辑写到configure里,导致重复创建上下文,最终容器里有两套 Spring 容器,资源翻倍且接口行为错乱。保持职责分离即可:main只服务本地,configure只服务外部容器。
3.3 打包与本地校验:用真实 Tomcat 验证而不是 IDE 的假 Tomcat
改完 pom 和启动类,执行mvn clean package,在target目录下会生成myapp.war。验证 war 包是否可用的最好方式,不是直接上传服务器,而是先在本地装一个和服务器同版本的 Tomcat,把 war 放进去启动,然后访问http://localhost:8080/myapp/看是否返回页面或接口数据。本地验证能快速确认三类问题:war 包是否完整解压、依赖是否缺失、上下文路径是否符合预期。
一个非常容易翻车的点:很多人用 IDEA 里的「Tomcat Server」集成方式直接运行 SpringBoot 项目,那个是 IDE 插件模拟的部署环境,它不会严格按 war 包逻辑加载,你在 IDE 里跑通了不代表 war 包在独立 Tomcat 里能跑通。我处理过的部署事故里,有三分之一是因为「本地 IDE 正常、真实容器报错」造成的。所以本地校验务必用独立下载的 Tomcat,把解压后的目录挂到一个非 IDE 的环境,比如命令行里bin/startup.sh启动,用tail -f logs/catalina.out观察日志。这一步花十分钟,能避免服务器上来回传包试错。
4. Vue 构建产物与后端资源的合并策略
4.1 前端构建的 baseURL、publicPath 与路由模式三件套
Vue 项目的构建产物要合并到 SpringBoot,必须先处理三个配置:接口请求的baseURL、静态资源的publicPath、路由的history模式。baseURL决定前端发请求的根路径。开发环境用/api配合代理,生产环境在合并部署下应该直接指向后端 contextPath,即http://ip:8080/myapp。如果不改,前端会去请求http://ip:8080/api/xxx,而后端接口实际在/myapp/api/xxx,必然 404。publicPath决定 JS/CSS/图片等静态资源在页面里的引用路径。默认/在独立部署时没问题,但合并部署后页面路径带myapp上下文,绝对路径会指向 Tomcat 根,导致样式和脚本 404。最简单的方案是把publicPath设为./相对路径,让浏览器基于当前页面路径解析资源。
路由模式这里我强烈建议:合并部署且不做特殊配置时,直接改用hash模式。history模式是 HTML5 的 History API,它要求服务器对所有前端路由都返回index.html。Tomcat 默认没有这个能力,后端也不需要在SpringMVC里写一个 catch-all 控制器,没必要为部署场景引入额外的代码复杂度。如果业务上必须用history模式(比如 URL 美观需求),常见做法是在后端加一层资源映射,让 Tomcat 对未匹配的路径返回前端入口页,但这个方案会引入额外的过滤器和路径匹配问题,非必要不碰。
4.2 把 dist 目录合并进 war 包的两个方案
方案一最简单:执行npm run build生成dist目录后,手动把里面的文件复制到src/main/resources/static,再执行 Maven 打包。这种方式适合纯手工部署的场景,缺点是每次前端改动都要前后端一起重新打包。方案二用 Maven 插件在构建阶段自动完成前端构建和资源复制,适合想要一条命令出包的情况。我一般会写进 pom 的profile里,做自动化,但因为这类需求里前后端经常由不同的人维护,放不放 pom 取决于团队约定,没有绝对标准。无论哪种方案,本质上都是把静态文件挪进静态资源目录,所以这里不谈自动化插件的具体配置,把注意力放在目录路径本身。
# 在项目根目录下执行 npm run build # 手动合并到后端静态目录 cp -r dist/* ../backend/src/main/resources/static/ # 回到后端项目执行打包 cd ../backend mvn clean package目录结构上有个坑需要特别提一下:src/main/resources/static下如果存在index.html,它会被当作应用根页;但如果你把 Vue 的dist整个复制进去,dist里本身也有一层index.html,复制后它就变成static/index.html,SpringBoot 默认的欢迎页机制会优先找static/index.html,此时访问http://ip:8080/myapp/会命中它,这是符合预期的。但如果你把dist下的文件再嵌套一层放在类似static/vue/的目录下,那访问根路径就会失效,必须访问/myapp/vue/才能进页面。很多人的部署事故就出在这个多余的嵌套目录上。合并前先看清复制后的实际层级,用tree命令确认index.html是否位于static的下一层根上。
4.3 为什么刷新就 404:history 模式与 Tomcat 的默认行为
如果前端坚持使用history模式,刷新行为的差异会立刻暴露。用户在/myapp/home页面按 F5,浏览器带着这个路径再次请求 Tomcat,Tomcat 去 webapp 目录里找是否有home这个静态资源或对应的 Servlet 映射,没有就直接返回 404。这不是前后端代码 bug,是容器对未知路径的默认处理策略。hash模式下,URL 是/myapp/#/home,#后面的内容浏览器不会发给服务器,Tomcat 永远只收到/myapp/,自然永远命中index.html。所以如果你不想改后端、不想改 Tomcat 配置,hash是唯一稳的选择。
如果必须用history模式,有一个相对可靠的绕法:在后端配置一个WebMvcConfigurer,对非静态资源、非 API 的路径做 forward 到/index.html。这里要注意的是,SpringBoot 对静态资源的处理优先级高于自定义的view-controller,所以只有当真正找不到资源时才会落到这个兜底逻辑,看似合理,但实际会遇到:API 请求路径也会被 forward 到index.html,导致接口返回的是 HTML 而不是 JSON;前端路由路径和实际后端接口路径前缀必须严格区分。所以每次有人让我评估「要不要上 history 模式」,我的回答都是:合并部署场景下,收益极低,成本很高,不值当。
5. Tomcat 部署避坑:五个反复出现的现场问题
5.1 启动报错:内嵌 Tomcat 和外部 Tomcat 的依赖冲突
现象:war 包放入webapps后,Tomcat 启动过程中catalina.out里出现类似Caused by: java.lang.LinkageError或者ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet之类的异常,接口全部 503。
原因:spring-boot-starter-tomcat没有被排除或scope不是provided,导致内嵌 Tomcat 的类被一起打进了WEB-INF/lib。外部 Tomcat 加载应用时,自己lib目录下的类和应用内部的类发生冲突,加载顺序不同时结果也不同,表现就是时好时坏。
解决:pom 里把spring-boot-starter-tomcat的scope改成provided,同时检查spring-boot-starter-web是否通过排除方式间接引入了其他容器类。如果使用的是Undertow或Jetty作为默认容器,也一样处理,统一用provided让外部容器来提供。
5.2 页面 404:访问路径没有带上下文路径
现象:部署后访问http://ip:8080/白屏或 404,但访问http://ip:8080/myapp/正常。如果finalName是myapp,Tomcat 会把myapp.war解压后注册成/myapp上下文,根路径只会去找ROOT.war或webapps/ROOT目录。
原因:没有理解 Tomcat 的上下文路径规则。Tomcat 对每个 webapp 的访问路径默认等于 war 包文件名,掌握这个规则后该问题就不会出现。
解决:想要根路径访问,把 war 改名为ROOT.war再放入webapps,前提是webapps下没有多余的旧目录;或者不强制要求根路径,所有前端接口、页面访问都写全myapp前缀。我一般选择后者,因为改名时有概率撞上残留的旧ROOT目录,处理起来更麻烦。
5.3 页面能打开但接口请求 404:baseURL 忘了带 contextPath
现象:首页和静态资源正常加载,但登录、列表等所有接口请求都返回 404。打开浏览器开发者工具查看 Network,发现请求路径是http://ip:8080/api/login,而后端接口实际注册在/myapp/api/login。
原因:Vue 项目开发时baseURL是/api,配合 devServer 代理,开发环境下代理会把/api转发到localhost:8080,看起来没毛病。合并部署后没有代理了,/api就原样请求到 Tomcat 根上下文,出错。
解决:把生产环境的baseURL改为${ contextPath },也就是/myapp,请求路径自然变成/myapp/api/login。不要试图在后端加全局前缀处理器,那会让本地开发和线上逻辑不一致,最好在前端构建时区分环境,或直接用环境变量配置。
5.4 刷新后 404:这个锅后端不背
现象:从首页进入二级页面一切正常,但停留在二级页面按 F5,直接 404。前端使用history模式,路径/myapp/list在首次进入时由前端 SPA 路由渲染,刷新时浏览器用相同路径向 Tomcat 发起请求,Tomcat 找不到对应资源。
原因:Tomcat 没有配置 fallback 机制。Vue 的history模式依赖服务端支持,在 Nginx 里配一条try_files规则就能解决,但在 Tomcat 里没有对应的成熟模块,需要手动写控制逻辑。
解决:最省事的是把路由模式改成hash,改一行createRouter配置即可,URL 从/list变成/#/list,功能不受影响,刷新稳定。如果产品接受不了#,就在前端构建时用base参数加上 contextPath,再在后端加兜底控制器,但这个方案的稳定性需要足够多的测试用例支撑。
5.5 上传文件失败:Tomcat 临时目录和大小限制
现象:接口能通,但上传图片或 Excel 时偶发报错,错误信息里出现The temporary upload location ... is not valid,或者上传大文件直接被拒绝。
原因:Tomcat 默认处理 multipart 请求时需要临时目录来存中间文件。这个目录通常在$CATALINA_BASE/temp,如果磁盘权限不对、目录被清理或者并发写太多造成 IO 瓶颈,就会触发异常。另外 SpringBoot 默认spring.servlet.multipart.max-file-size是 1MB,超过限制的请求直接被拒。
解决:排查时先看是不是大小限制触发的,application.yml里按需调大max-file-size和max-request-size;再看临时目录,手动在 Tomcat 的server.xml的Context节点里配置workDir,或者用-Djava.io.tmpdir指定一个应用专属目录,避免和系统默认临时目录混在一起被系统清理掉。这个坑在长时间运行的 Tomcat 上出现频率很高,越早改越好。
6. 部署完之后的调优与验证:从「能打开」到「跑得稳」
部署成功只是第一步,后续的稳定性更依赖 Tomcat 和 JVM 参数。先说连接器参数。server.xml里Connector的maxThreads默认 200,对多数内部系统够用,但如果项目里涉及前端长轮询或文件上传,connectionTimeout和maxPostSize需要调大。注意maxPostSize默认 2MB,表单 POST 和文件上传大小超过 2MB 时会直接被容器拒绝,这个行为容易和后端的max-file-size混淆,实际上两个限制都会生效,需要同时调整。JVM 参数方面,catalina.sh里的JAVA_OPTS至少要设置-Xms和-Xmx一致的大小,避免堆动态伸缩造成的性能抖动。我处理过的项目里,一台 2G 内存的服务器配-Xmx1024m跑一个 SpringBoot + Vue 的合并部署是够用的,但如果同时跑多个 webapp 就得重新核算内存预算。
验证部署是否成功,不能只看浏览器里页面出来了。我一般按这个顺序做一套检查:先看catalina.out日志,确认 Spring 容器初始化完成、没有异常堆栈;再访问/myapp/页面,按 F12 确认静态资源的响应码都是 200;然后是接口级验证,登录一个真实账号,跑一遍增删改查的核心链路;最后做一次刷新测试,分别在hash路由模式下的不同页面按 F5,确认不会 404。如果页面资源有缓存相关的问题,还要用无痕窗口重新访问,排除浏览器缓存干扰。
这里要专门提出一个我踩过的坑:不要在生产环境用 IDEA 的Run按钮直接运行 SpringBoot,然后声称部署在 Tomcat 上。内嵌 Tomcat 启动的应用没有经过webapps目录,也没有经过容器的Context管理,它的生命周期和外部 Tomcat 完全不同。有人用这种方式「验证」完就交付,结果服务器上放了真正的外部 Tomcat,应用却用内嵌方式跑在别的端口,运维人员查端口时完全定位不到。后来我养成一个习惯:每次部署完,用ps -ef | grep tomcat和lsof -i:8080确认进程和端口对应关系,同时把catalina.out里的启动时间记下来,下次发现异常时能判断是不是这次重启造成的。
其次是定期检查webapps目录,Tomcat 会保留解压后的目录。如果多次替换 war 包,旧版本的文件可能残留,尤其是前端静态资源这种按文件名哈希的产物,容易旧文件和新文件混在一起导致页面错乱。我一般会写一个简单的发布脚本,先删旧目录再传新 war,避免增量覆盖带来的脏状态。
能走到这里,说明这套合并部署流程已经跑通了。这些坑大部分都是部署方式选择、路径规则理解、依赖 scope 三件事引发的,记住它们比记住命令更有用。希望帮到你。
本文还有配套的精品资源,点击获取