
简介围绕 SpringBoot 与 Vue 前后端分离项目在 Tomcat 上的部署落地这份文档面向需要把开发成果发布到 Windows 服务器运行的 Java Web 开发者、运维人员及课程实训读者聚焦后端打包、前端静态资源托管、访问路径配置和页面刷新异常等常见问题。压缩包共 1 个文件为 doc 文档约 72KB内容以部署流程说明、关键配置片段和问题排查提示为主便于按环节对照查阅。文档覆盖打包方式调整、Vue 构建产物放置、Tomcat 的上下文配置、错误页处理以及调用百度地图等第三方接口时的跨域配置思路能帮助读者理解前后端分离项目在传统容器中的发布要点与排错方向。已有 1458 人学习适合具备 SpringBoot、Vue 和 Tomcat 基础、希望减少部署踩坑时间的读者参考。1. 一个只有 Tomcat 的 Windows 环境怎么把前后端塞进去接到过这么一个活内网几台 Windows Server运维给的说法很干脆机器上跑着一堆老系统装 nginx 要提工单、开端口要审批、装容器想都别想现成能用并且有人会重启的就一个 Tomcat。项目本身是 springboot vue 前后端分离按理说该 jar 包一挂、nginx 一配就完事结果环境把这条路堵死了。所以这份文档解决的就是这么个具体问题不借助 nginx把后端打成 war 丢进 webapps把 vue 的 dist 静态资源也丢进去让 tomcat 同时托管页面和接口。好处很实在——页面和接口同域同端口前端写相对路径就能调通代理和跨域那一圈麻烦直接省掉。适合手里有现成前后端分离项目、部署环境受限、又不想把技术栈推倒重来的开发者。2. 部署链路拆解war 包和 dist 在 webapps 里怎么各管一段2.1 一次页面加载背后的两次请求分发先把链路走通后面配错的时候才知道该查哪一段。浏览器访问http://ip:8080/Tomcat 的 Connector 收到请求交给 Host 处理Host 下面挂着一个path/的 Context它的 docBase 指向 dist 目录。因为路径是根Tomcat 默认的 DefaultServlet 会去找 dist 下的index.html找到就返回页面框架起来了剩下的路由交给 Vue Router 在浏览器里接管。页面渲染完代码里发起/prod-api/system/user/list这种请求这时命中的是另一个 Context——就是 webapps 下面那个prod-api.war解压出来的目录war 的名字天然就是 Context path。请求进到 war 内部被 SpringBoot 注册的DispatcherServlet接住再按RequestMapping分发到 Controller。整个过程里浏览器看到的 origin 始终是http://ip:8080协议、域名、端口三样全一样同源策略根本不会触发。这就是不用 nginx 也能跑通的核心原因不是绕过了跨域是从一开始就不存在跨域。理解这一点后面所有配置项的意义就清楚了——Context 配的是「页面从哪来」war 名配的是「接口从哪来」两者必须在同一个 Host、同一个端口下。2.2 目录落位与服务端口安排落位其实就三样东西但每一样的细节都能让人栽跟头。我一般部署完先dir一下 webapps确认结构跟预期一致再启动比事后看日志高效得多。位置内容说明webapps/prod-api.war后端 war 包文件名即 Context path改名就等于改接口前缀webapps/prod-api/Tomcat 自动解压目录每次替换 war 前建议先删掉避免残留旧 classwebapps/dist/Vue 打包产物index.html必须直接在这一层不能多套一层端口在conf/server.xml的 Connector 上改把这台机器对外开放的端口写进去就行。这里有个 Windows 特有的坑改成 80 端口看着很美但 80 常被系统里的其他 Web 服务占着而且绑定 1024 以下端口在某些环境下需要额外权限启动日志里会直接抛BindException。我一般先用 8080 跑通确认业务没问题再去谈端口的事把变量拆开验证。另外一个容易被忽略的点是unpackWARs。默认是 trueTomcat 会把 war 解压成同名目录再加载改配置文件、换静态资源都方便如果设成 falsewar 直接内存加载改一个字符都得重新打包。部署调试阶段务必保持 true等稳定了再考虑要不要动。2.3 path 与接口前缀的对应关系把映射关系列清楚配错了对着表一查就知道错在哪。浏览器地址谁在处理落到什么http://ip:8080/Contextpath/webapps/dist/index.htmlhttp://ip:8080/static/js/app.xxx.jsContextpath/dist 下的静态资源文件http://ip:8080/prod-api/user/listContext/prod-apiwar 内的 Controllerhttp://ip:8080/system/userContextpath/磁盘上没这个文件404最后一行是后面所有 404 问题的总根源Vue Router 用的是 history 模式时用户在页面里点跳转没问题因为路由是 JS 拦下来的但一按 F5浏览器真的会拿/system/user去问服务器要文件服务器当然没有。这个问题在第 4 章和第 5 章还会反复出现先记住它的成因。3. 后端改造把 springboot 项目从 jar 调成能在外部 Tomcat 跑的 war3.1 pom.xml打包方式、内嵌容器作用域和产物名默认的 SpringBoot 工程打出来是 fat jar里面自带一个内嵌 Tomcat直接扔进 webapps 是不认的——外置 Tomcat 需要的是标准 war 结构WEB-INF/web.xml或者 Servlet 3.0 的注解扫描加上WEB-INF/classes、WEB-INF/lib。所以 pom 里至少有三处要动。!-- pom.xml 关键片段 -- packagingwar/packaging build !-- 产物名直接定成 prod-api省得打完包再手工改名 -- finalNameprod-api/finalName plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build dependencies !-- 关键把内嵌 Tomcat 标成 provided运行期由外置 Tomcat 提供 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency /dependenciespackaging从 jar 改 war 是最基础的一步改完 Maven 才知道要按 war 的生命周期走war:war插件。finalName定成prod-api是个小技巧这样打出来的就是target/prod-api.war拷进 webapps 不用改名接口前缀天然就是/prod-api前后端约定的路径一次对齐。provided这个作用域值得多说两句。它的意思是编译期需要、运行期由容器提供打 war 时不会把这个依赖塞进WEB-INF/lib。不设它的话war 包里会同时存在内嵌 Tomcat 的 jar 和外置 Tomcat 的类两边版本一旦对不上启动时就是一堆NoSuchMethodError或者ClassNotFoundException日志刷得满屏红但报的类名跟你写的业务代码毫无关系第一次遇到很容易怀疑人生。提示SpringBoot 2.x 内嵌的是 Tomcat 9Servlet 4.0javax.servlet包名SpringBoot 3.x 换成了 Tomcat 10Servlet 5.0jakarta.servlet。外置 Tomcat 的大版本必须和后端对齐Boot 3 往 Tomcat 9 里扔启动直接报类找不到这不是配置问题是方言不通。3.2 启动类继承 SpringBootServletInitializer光改 pom 还不够启动类也得改。标准 SpringBoot 的入口是main方法里SpringApplication.run但部署到外置 Tomcat 时容器不会去调你的 main——它是通过 Servlet 3.0 的ServletContainerInitializer机制扫描META-INF/services找到 Spring 的实现类然后回调SpringBootServletInitializer的configure方法。所以启动类得继承它并重写。SpringBootApplication public class ProdApiApplication extends SpringBootServletInitializer { /** * 外置 Tomcat 启动时走这里。 * 等价于 main 方法里的 SpringApplication.run只是入口换了。 */ Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(ProdApiApplication.class); } /** * 内嵌 Tomcat 启动本地开发、main 方法直接跑时走这里。 * 两个入口并存本地开发和服务器部署用同一份代码。 */ public static void main(String[] args) { SpringApplication.run(ProdApiApplication.class, args); } }这段代码的要点在于两个入口并存。configure负责外置容器main负责本地 IDE 里直接跑两者都保留开发的时候不用改代码部署的时候也不用改代码靠运行环境自动选择。我见过有人把 main 删了结果本地调试只能每次打包扔 Tomcat一轮下来多花十几分钟纯粹自找麻烦。另外注意builder.sources(...)里的类必须是被SpringBootApplication标注的那个写错成别的配置类会出现「启动成功但一个 Bean 都没有」的诡异现象表现是接口全 404日志却干干净净。3.3 打包、校验与丢进 webapps改完这两处就可以打包了。构建命令本身没什么好说的重点是产物要验一眼别急着拷。# 清理后重新打包跳过测试省时间正式发版建议别跳 mvn clean package -DskipTests # 看产物结构应该能看到 WEB-INF/classes 和 WEB-INF/lib unzip -l target/prod-api.war | head -30验产物这一步是血泪经验。曾经有一次打完包直接拷上去Tomcat 起了半天报ClassNotFoundException: javax.servlet.Filter排查半小时才发现 pom 里provided加错了位置war 里根本没有 servlet-api。用unzip -l看一眼WEB-INF/lib下面该有的业务依赖在不在、不该有的内嵌 Tomcat 在不在十秒钟的事能省半小时。拷过去之后的动作顺序也有讲究。如果 webapps 下面已经有旧的prod-api目录先把它删掉再放新 war——Tomcat 判断是否重新解压的依据是 war 的时间戳但残留的旧目录里可能留着上次的配置文件或者编译产物混在一起排查起来极其难受。# Windows 下的操作假设 Tomcat 装在 D:\apps\tomcat-9.0 copy target\prod-api.war D:\apps\tomcat-9.0\webapps\ rd /s /q D:\apps\tomcat-9.0\webapps\prod-api删目录要在 Tomcat 停掉的时候做运行中删会被文件占用卡住。启动之后留意日志里有没有Deployment of web application archive ... has finished in xxx ms出现这行基本就说明 war 被正常识别了。3.4 把接口前缀和前端约定对齐war 的名字决定了 Context path也就决定了接口前缀。prod-api.war意味着所有后端接口都在/prod-api下面这个信息必须传到前端去。最容易出事的是application.yml里那个server.servlet.context-path。内嵌 Tomcat 跑的时候配个/api很常见但打成 war 扔进外置 Tomcat 后war 名已经贡献了一层前缀配置文件再贡献一层实际路径就变成了/prod-api/api/user/list前端按/prod-api/user/list去请求必然 404。server: port: 8080 servlet: # 部署到外置 Tomcat 时这一项必须注释掉前缀由 war 包名决定 # context-path: /api处理方式很简单部署前把这一行注释掉让 war 名成为唯一的前缀来源。如果项目里有多处依赖这个前缀比如 Swagger 的 basePath、拦截器的 excludePathPatterns要一起改别只改一处。我的习惯是在application-prod.yml这类部署专用配置里显式写一行注释说明「前缀由 war 名提供」下次接手的人一眼就懂。4. 前端构建与静态资源落位vue 的 dist 目录怎么放、路径怎么写4.1 publicPath、路由模式和接口 baseURLVue 打包出来的index.html里js 和 css 的引用路径是构建时写死的由publicPath决定。这个值配错表现就是页面一片空白控制台里一堆GET http://ip:8080/js/app.js 404。// vue.config.js module.exports { // 部署在根路径Context path/时用 / // 如果整个前端挂在 /web 这种子路径下这里要同步改成 /web/ publicPath: /, outputDir: dist, assetsDir: static, productionSourceMap: false, devServer: { port: 8081, proxy: { // 开发期把 /prod-api 代理到本地后端路径和生产保持一致 /prod-api: { target: http://localhost:8080, changeOrigin: true } } } }publicPath设成/的前提是 Context 的 path 也是/两边是一对绑定关系改一个必须改另一个。devServer.proxy那段是为了让开发环境的请求路径和生产完全一致都走/prod-api这样代码里axios.defaults.baseURL /prod-api写一次就够了不用在打包前手工切换地址。路由模式这块有两个选择各有代价。history 模式地址干净http://ip:8080/system/user这种 URL 看着舒服但刷新会 404得靠后面的 web.xml 兜底hash 模式地址带个#http://ip:8080/#/system/user刷新永远不会 404因为#后面的内容浏览器不会发给服务器。内网系统、对 URL 美观没要求的场景我一般直接上 hash省掉一整类问题如果甲方明确要求地址不能带井号再走 history 兜底那条路。4.2 dist 落位与 server.xml 的 Context 配置逐字段说明打包命令npm run build跑完产物在dist目录下。把整个 dist 目录复制到 webapps 里注意是「dist 目录本身」不是目录里面的内容——复制完的路径应该是webapps/dist/index.html。# 前端构建 npm run build # 复制到 TomcatWindows 下也可以用资源管理器直接拖 xcopy /E /I /Y dist D:\apps\tomcat-9.0\webapps\dist然后是conf/server.xml里的 Context 配置。这一步是整个部署里最关键、也最容易漏的一环。加在Host节点内部、/Host之前。Host namelocalhost appBasewebapps unpackWARstrue autoDeploytrue !-- path浏览器访问的路径/ 表示根路径直接访问 ip:端口 -- !-- docBase静态资源所在目录可以写绝对路径也可以写相对 webapps 的目录名 -- !-- reloadable改 class 自动重载调试期开、生产期关 -- !-- crossContext允许同一 Host 下的应用互访 ServletContext -- Context path/ docBasedist reloadabletrue crossContexttrue / /Host四个属性逐一说明。path决定用户在地址栏里敲什么写/就是http://ip:8080/直接进首页写/web就得访问http://ip:8080/web/同时前端的publicPath也要跟着改成/web/。docBase写相对路径dist时是相对 Host 的 appBase也就是 webapps解析的写D:\apps\tomcat-9.0\webapps\dist这种绝对路径也行好处是不受 appBase 变更影响坏处是换台机器就得改。reloadabletrue在调试阶段很香改了后端 class 文件 Tomcat 会自己重新加载不用重启但它是有代价的Tomcat 会起一个后台线程定期扫描 class 变化生产环境开着纯属浪费资源而且偶尔会出现加载到一半的诡异状态。上线前记得关掉。crossContexttrue允许同一个 Host 下的应用通过ServletContext.getContext()拿到彼此的上下文。前后端在同一个 Tomcat 里时这个配置能避免一类跨上下文取资源失败的问题开着的成本很低一般顺手就带上。注意如果 webapps 下还留着 Tomcat 自带的 ROOT 目录它会和path/的 Context 抢同一个路径表现是访问首页出来的是 Tomcat 的欢迎页而不是你的 Vue 页面。部署前把 ROOT 目录删掉或者改个名。改完 server.xml 要重启 Tomcat 才生效而且这个文件改坏了整个容器都起不来改之前先备份一份是我的固定动作。4.3 刷新 404 与 WEB-INF/web.xml 的回退方案history 模式下按 F5 得到 404前面说过成因浏览器真的去请求/system/user这个路径磁盘上没有对应文件Tomcat 的 DefaultServlet 找不到就返回 404。解决办法是给这个 Context 配一个错误页让它把所有 404 都重定向回index.html页面重新加载后再由 Vue Router 根据地址栏的路径渲染对应组件。具体做法是在 dist 目录下新建WEB-INF文件夹里面放一个web.xml。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 metadata-completetrue display-nameRouter for Tomcat/display-name !-- 所有 404 统一回退到 index.html交给前端路由处理 -- error-page error-code404/error-code location/index.html/location /error-page /web-appmetadata-completetrue告诉容器这个 web.xml 已经是完整描述不用再去扫描注解启动能快一点。error-page那段是核心error-code指定拦截哪个状态码location指定跳转目标写/index.html是相对当前 Context 根的路径。这个方案有个副作用要提前知道它拦截的是这个 Context 下所有的 404包括静态资源缺失。如果index.html引用的某个 js 文件名写错了请求返回的不是 404 而是index.html的内容浏览器解析 JS 时抛的是语法错误排查方向会被带偏。所以看到「控制台报 JS 语法错误」的时候先打开 Network 看那个请求的响应体是不是 HTML八成就是这个原因。还有一种情况如果后端接口也部署在同一个 Context 下比如把 war 直接解压合并进 dist那后端返回的 404 JSON 也会被这条规则吞掉接口调试时看不到真实的错误信息。这时候就得把前后端的 Context 分开保持 war 独立就是第 2 章那张表里的结构。4.4 多环境打包的注意点如果项目要同时在开发机、测试机、生产机上跑接口地址别写死在代码里。用.env.production这类文件控制打包时自动注入。# .env.production # 生产环境接口前缀与 war 包名保持一致 VUE_APP_BASE_API/prod-api代码里统一用process.env.VUE_APP_BASE_API取这样换环境只改这个文件、重新 build不用去翻request.js。我见过打完包手工改 dist 里 js 文件的做法一次两次还行第三次就忘了改上线之后接口全打到测试环境排查起来绕一大圈。5. 排查清单部署到 Tomcat 之后最容易翻车的 5 个现场5.1 登录能进进去之后一片空白现象登录页正常账号密码提交成功跳转之后白屏控制台没有明显报错或者只有一条路由相关的警告。原因Context 的path/没配或者前端publicPath与实际访问路径不一致。前者导致页面里的相对路径资源全部解析到错误的位置后者导致index.html里的 js 引用地址带着错误的前缀。原文里提到「不添加 Context 配置登录是可以的但登陆之后是空白页」根子就在这里——登录页可能是直接访问的路由跳转之后的页面依赖的资源路径对不上。解决打开浏览器 Network筛 JS看请求地址是不是 404对不上就回头改vue.config.js的publicPath和 server.xml 的path两边保持一致后重新打包、重启。5.2 页面能开一按 F5 就 404现象站内点击跳转一切正常只要刷新当前页面立刻变成 Tomcat 的 404 页面。原因Vue Router 用了 history 模式地址栏里的/system/user被浏览器当成真实路径发给服务器磁盘上没有这个文件或目录。解决按 4.3 的做法在 dist 下加WEB-INF/web.xml配 404 回退或者干脆把路由模式改成 hash从根上避开这个问题。两条路选一条别混着来。5.3 启动日志满屏红报的类名完全没见过现象Tomcat 启动时抛ClassNotFoundException或NoSuchMethodError报的类是 Spring 内部的跟业务代码无关。原因两个方向。一是spring-boot-starter-tomcat没标providedwar 里同时存在内嵌容器和外置容器的类版本冲突二是 SpringBoot 大版本和外置 Tomcat 大版本不匹配Boot 3 用jakarta.*Tomcat 9 只认javax.*。解决先看 pom 里provided加没加再看两边版本对不对得上。对照表很简单——Boot 2.x 配 Tomcat 9Boot 3.x 配 Tomcat 10 及以上没有中间地带。5.4 接口路径里出现了两层前缀现象Network 里看到的请求是/prod-api/prod-api/user/list返回 404。原因application.yml里的server.servlet.context-path没注释掉war 名贡献了一层配置文件又贡献了一层。解决注释掉 context-path让 war 名成为唯一前缀来源重新打包。顺手检查一下 Swagger 配置、拦截器路径、静态资源映射里有没有硬编码这个前缀的地方一起改掉。5.5 调第三方服务时浏览器报跨域现象后端用 Postman 直连第三方接口一切正常页面里调就报 CORS 错误控制台提示缺少Access-Control-Allow-Origin。原因跨域是浏览器施加的限制Postman 这类工具不做同源检查所以服务端测不出问题。第三方服务比如地图、短信、OCR 这类开放平台通常只对白名单里的域名放行本地或内网地址不在名单上就会被拦。解决按顺序试三招。先去第三方控制台把当前访问域名加进白名单这是最正规的做法如果对方只支持固定域名、内网地址加不进去就在后端加一层转发前端调自己的/prod-api/proxy/xxx后端用RestTemplate或WebClient去请求第三方再返回浏览器看到的始终是同源请求本地开发阶段临时用devServer.proxy代理过去但这条只解决开发环境上线还是得靠前两招。6. 二次开发把手工搬运变成可重复的交付流程上面这套流程跑通一次之后最该做的不是庆祝是把它脚本化。手工拷 war、手工删目录、手工重启做一次两次没问题做到第五次一定会在某个环节手滑——要么忘了删旧目录要么 war 拷到一半 Tomcat 还在跑文件被占用。我的做法是写一个 bat把「停服务、清旧包、拷新包、起服务」四步固定下来。echo off setlocal set WARtarget\prod-api.war set CATALINA_HOMED:\apps\tomcat-9.0 set SERVICETomcat9 if not exist %WAR% ( echo [ERROR] 没找到 war 包先执行 mvn clean package exit /b 1 ) :: 停服务避免文件占用 net stop %SERVICE% :: 清掉旧 war 和解压目录防止残留 class 干扰 del /q %CATALINA_HOME%\webapps\prod-api.war rmdir /s /q %CATALINA_HOME%\webapps\prod-api :: 拷新包Tomcat 启动时自动解压 copy %WAR% %CATALINA_HOME%\webapps\prod-api.war net start %SERVICE% echo [OK] 部署完成访问 http://127.0.0.1:8080/这个脚本里SERVICE变量要求 Tomcat 是以 Windows 服务方式注册的用startup.bat手动起的那种用不了net stop得改成shutdown.bat加超时等待。服务方式的好处是开机自启、日志统一内网环境基本都这么装值得花十分钟配一次。往前再走一步是把 server.xml 里那段 Context 挪出来。直接改conf/server.xml的风险在于这个文件承载着整个容器的配置改错一个标签整个 Tomcat 都起不来而且某些版本的 Tomcat 在重启时会对 server.xml 做规范化处理手工加的 Context 有被覆盖的可能。更稳的做法是在conf/Catalina/localhost/下建一个ROOT.xml内容就是那个 Context 节点文件名对应 path/对应ROOT。这样配置集中、互不干扰出问题删掉这个文件就能回退。再往后就是部署粒度的取舍。如果内网机器上要同时跑好几个项目path/只能给一个其余项目就得挂子路径比如/web-a、/web-b对应的publicPath和 war 名都要跟着调整。这时候建议把「项目名、war 名、Context path、publicPath」这四个值做成一张对照表贴在部署文档里改一个牵动四个靠脑子记迟早出错。最后提一个反向的思路。这套方案的本质是用 Tomcat 一个容器兜住前后端适合环境受限的场景但如果哪天运维松口了、端口可以自由申请其实把后端改回 jar 内嵌 Tomcat、再用 winsw 之类的工具注册成 Windows 服务前端继续由 Tomcat 托管静态资源反而是更轻的组合——少一层 war 打包改造启动类不用继承SpringBootServletInitializer日志也更好找。改造方向不是只有「往 Tomcat 里塞」一条看清约束条件再选路。从那以后我每次交付这类部署都会先在一台干净的虚拟机上从零走一遍完整流程——不是在自己那台配了一堆环境变量的开发机上验证是真正的干净系统。因为踩过的坑里至少有一半是「我机器上好好的」造成的。希望帮到你。本文还有配套的精品资源点击获取