简介:面向使用 SpringBoot 与 Vue 前后端分离架构的 Java Web 开发者,尤其适合需要在 Windows 环境下借助 Tomcat 完成项目上线、排查部署问题的初中级人员。资源为一份 doc 文档,压缩包共 1 个文件,大小约 72KB,篇幅精炼,便于随项目归档和查阅。内容围绕 Tomcat 部署 SpringBoot+Vue 的关键环节展开,涉及后端 WAR 包改造、前端 dist 静态资源放置、server.xml 中 Context 路径配置,以及刷新页面 404 的 web.xml 兜底处理;同时补充调用百度地图等第三方 API 时的跨域配置思路。已有 1458 人学习,可作为前后端分离项目从本地运行转向服务器部署时的参考手册,帮助读者快速定位空白页、404 与跨域等常见问题的处理方向。
1. 从一次「登录成功、进去白屏」说起:Tomcat 部署 SpringBoot + Vue 的环境画像
vue tomcat 部署 springboot 这件事,我第一次做的时候以为半小时能收工:pom 里把 jar 改成 war,打好的包丢进 webapps,dist 一拷,重启 Tomcat。结果登录页出来了,账号密码也对,点进去是一片白,F12 里 index.html 加载完了,路由那一层什么也没有。
这套组合在企业内网系统里出现频率极高:后端 SpringBoot 打 war,前端 Vue 打 dist,不挂 Nginx,一台 Windows 服务器上一个 Tomcat 承担全部工作。省掉反向代理这一层,代价是路由回退、静态资源映射、跨域规则都得在 Tomcat 里手工写清楚。适合手上有现成 SpringBoot + Vue 项目、服务器上只有一个 Tomcat 可用的人。
2. 环境先列清:JDK、Tomcat、Maven、Node 的版本怎么定
这套部署看起来只有「一个 Tomcat」,实际上要装四个东西,而且它们之间的版本关系是隐式的:JDK 决定 SpringBoot 能不能起,SpringBoot 版本决定 war 里的 Servlet 描述符长什么样,Servlet 描述符又决定 Tomcat 最低要用哪一代。这些约束在装的时候没人提醒,全在部署到最后一步才一起暴露,报错还都不指向版本。
先把四个组件的版本关系摊开,后面每一步才有依据。
2.1 四个组件的版本对照与推导依据
| 组件 | 建议版本 | 依据 | 版本不对的典型表现 |
|---|---|---|---|
| JDK | 8 或 11;SpringBoot 3.x 必须 17 | SpringBoot 2.x 的编译基线是 8,3.x 抬到 17 | 启动即UnsupportedClassVersionError |
| Tomcat | 8.5 或 9 | 资源里的 web.xml 用的是 Servlet 3.1 描述符,Tomcat 8.0 起才认 | 启动报Unable to process Jar entry或 web.xml 解析失败 |
| Maven | 3.6 及以上 | 只负责把源码打成 war | 依赖下载不全,war 里缺 jar |
| Node | 16 或 18,跟前端脚手架要求走 | 只负责npm run build出 dist | build 阶段ERR_OSSL_EVP_UNSUPPORTED |
判断 Tomcat 该用哪一代,最直接的办法是打开项目里现成的WEB-INF/web.xml,看version属性和xsi:schemaLocation里那个 xsd 文件名。写的是web-app_3_1.xsd和version="3.1",说明这个应用至少要求 Servlet 3.1 容器,Tomcat 8.0 起步,选 8.5 或 9 都能跑。资源里给的就是这份描述符,照它推就行,不用猜。
JDK 这边有个容易忽略的点:Tomcat 自身和你的应用跑在同一个 JVM 里。Tomcat 9 要求 JDK 8 以上,你的 SpringBoot 项目如果用了 JDK 11 编译的语法,那 Tomcat 就必须跑在 11 上。我一般的做法是先确认项目的maven.compiler.source,再把 Tomcat 的JAVA_HOME对齐到同一个大版本,别让 Tomcat 用 8、项目用 11,那种NoClassDefFoundError查起来能耗掉半天。
2.2 Tomcat 选解压版还是安装版,目录先认全
Windows 上 Tomcat 有两种给法:一是apache-tomcat-x.x.x.zip解压即用,二是exe安装包注册成系统服务。部署调试阶段我一律用解压版,原因很实际——改server.xml改错的时候,解压版直接覆盖回去就行,安装版改注册表和服务项,回滚成本高。
解压后先认这几个目录,后面每一章都会用到:
# Tomcat 解压后的目录结构(Windows) apache-tomcat-9.x/ ├── bin/ # startup.bat / shutdown.bat / service.bat ├── conf/ # server.xml、web.xml、context.xml、Catalina/ ├── logs/ # catalina.out、localhost.yyyy-mm-dd.log ├── webapps/ # war 包和 dist 都落在这里 └── work/ # JSP 编译产物,出玄学问题时可以先清空启动前先做两件事,这两步不做,后面所有报错都可能是假的:
# 1. 确认 8080 没被别的进程占着 netstat -ano | findstr :8080 # 2. 确认 JAVA_HOME 指向 JDK 目录,而不是 JRE echo %JAVA_HOME%第一条命令如果返回了 PID,说明端口已经有人在用,startup.bat会一闪而过直接退出,日志里只有一行Address already in use。第二条如果JAVA_HOME指向的是jre子目录,Tomcat 启动时会报找不到tools.jar一类的错,这个坑在只装了 JRE 的机器上特别常见。我习惯在bin/setenv.bat里显式写死JAVA_HOME,避免系统环境变量被别的软件改掉之后自己还不知道。
2.3 Maven 只干一件事:把源码变成 war
Maven 在这套流程里不参与运行,它的全部职责是把src编译、依赖打进去、产出一个能丢进 webapps 的 war。装完先验一下版本和本地仓库位置:
mvn -v # 输出里要能看到 Maven 版本、Java version、Maven homemvn -v里那行Java version必须和你打算让 Tomcat 使用的 JDK 一致。不一致的话,编译出来的 class 文件版本会比 Tomcat 支持的更高,部署时才炸。真遇到这种情况,不用重装 Maven,改MAVEN_HOME/conf/settings.xml里的<profile>或者直接在命令行临时指定JAVA_HOME更快。
内网环境还有个现实问题:默认的中央仓库拉不动。常见做法是在settings.xml里配一个内网镜像地址,<mirrorOf>写central,然后先跑一次mvn dependency:go-offline把依赖拉全,再去打包。这一步在能联网的机器上做一次,把~/.m2/repository整个拷到目标机,比在现场等依赖超时省事得多。
2.4 Node 只干一件事:把源码变成 dist
前端侧只有一条命令值得记:npm run build(Vite 项目是npm run build,输出目录默认dist)。但在敲这条命令之前,有一个参数必须提前定下来,否则 dist 拿到 Tomcat 里就是白屏。
这个参数在 vue-cli 里叫publicPath,在 Vite 里叫base。它决定打包出来的index.html里引用 js/css 时前面带什么前缀:
// vue.config.js —— 前端部署在根路径 / 时 module.exports = { // 保持默认 '/',打包后 index.html 里是 /js/app.xxx.js publicPath: '/', outputDir: 'dist' }逻辑说明:publicPath为/时,index.html引用的是/js/app.xxx.js,浏览器会去ip:端口/js/app.xxx.js找文件。如果后面 server.xml 里把 Context 的path配成了/app,那这个/就错了,正确值应该是/app/,否则所有静态资源全 404。这个参数和 Tomcat 的path是一对,改一个必须改另一个,我见过太多只改一边然后排查一整天的。
顺带提一句 Node 版本:Node 17 以上默认用 OpenSSL 3,老版本 webpack 会报ERR_OSSL_EVP_UNSUPPORTED。真撞上了,要么把 Node 降到 16,要么在package.json的 build 脚本里加NODE_OPTIONS=--openssl-legacy-provider。这不是 Tomcat 的问题,但十有八九会在这套流程里第一次遇到。
3. 后端改成 war:pom.xml 三处改动与 prod-api 的命名
资源里写得很干脆:「修改 pom 文件,改 jar 为 war」。这句话是对的,但只说了一半。光把<packaging>改成war打出来的包,丢进 Tomcat 大概率起不来,因为 SpringBoot 默认把 Tomcat 嵌在自己肚子里,现在外面又有一个 Tomcat,两个容器会打架;另外启动类如果还是普通的main方法入口,外置 Tomcat 根本不会去调它。
先把这三处改全,再谈打包。
3.1 packaging 改了之后,还有两处必须跟着改
第一处是<packaging>,从jar变成war,这决定 Maven 用哪个插件打包。
第二处是内嵌 Tomcat 的依赖,要把它标成provided。意思是「编译时要,运行时由外部容器提供」,这样打出来的 war 里不会带tomcat-embed-core.jar,避免和外置 Tomcat 的类冲突。
第三处是启动类,要继承SpringBootServletInitializer并重写configure。外置 Tomcat 启动时是通过 Servlet 3.0 的ServletContainerInitializer机制找到应用的,SpringBoot 提供SpringBootServletInitializer就是干这个适配的。不继承它,war 部署上去日志里一片安静,访问就是 404。
3.2 pom.xml 的完整改动片段
<!-- pom.xml --> <project> <!-- 1. 打包方式从 jar 改成 war --> <packaging>war</packaging> <dependencies> <!-- 2. 内嵌 Tomcat 标为 provided,运行时交给外置 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <!-- 其他依赖保持不变 --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!-- 3. 指定最终 war 的名字,省得打包后再手动改名 --> <finalName>prod-api</finalName> </build> </project>参数说明:<scope>provided</scope>是这三处里最容易漏的,漏了之后 war 包体积会大出十几兆,部署上去可能报ClassNotFoundException: javax.servlet.Filter或者直接java.lang.IllegalStateException。<finalName>prod-api</finalName>是可选项,但强烈建议加,它让mvn package直接产出prod-api.war,不用每次打完包再手动改名——手动改名这件事,改错一次就要多排查半小时。
3.3 启动类的改动
// Application.java @SpringBootApplication public class Application extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 外置 Tomcat 启动时走这里,把主配置类交给容器 return builder.sources(Application.class); } }逻辑说明:main方法保留着,本地用java -jar或者 IDE 直接跑的时候还走它;configure方法是给外置 Tomcat 用的,容器启动时回调它来加载 Spring 上下文。两个入口并存不冲突,这也是 SpringBoot 官方推荐的 war 部署写法。如果项目里已经有一个继承SpringBootServletInitializer的类,就别重复建,把@SpringBootApplication那个类改掉即可。
3.4 mvn package 之后怎么确认产物是对的
# 跳过测试打包,第一次部署不建议带测试跑 mvn clean package -DskipTests # 看产物 dir target\*.wartarget目录下应该出现prod-api.war。但这个文件名对不代表内容对,真正要确认的是 war 里面的结构。用解压工具打开看一眼,必须有这三样:
WEB-INF/classes/下有你的Application.class和业务代码WEB-INF/lib/下有 Spring 相关 jar,但没有tomcat-embed-core-*.jarWEB-INF/web.xml存在(SpringBoot 2.x 会自动生成一个 Servlet 3.1 描述符)
如果WEB-INF/lib里还能看到tomcat-embed-core,说明provided没生效,回去检查 3.2 那段是不是写在了<dependencies>里而不是<dependencyManagement>里。如果WEB-INF/web.xml不存在,看看 pom 里有没有配maven-war-plugin的failOnMissingWebXml为 false,那个配置会让 Maven 放行,但部署到严格模式的 Tomcat 上可能出问题。
3.5 war 改名 prod-api:上下文路径就是这么来的
Tomcat 的规则很朴素:war 包的文件名去掉.war后缀,就是这个应用的上下文路径(context path)。prod-api.war丢进 webapps,应用对外就是http://ip:端口/prod-api/。
这条规则直接决定了前端要怎么配。后端所有接口的真实地址会从本地开发的http://localhost:8080/user/login变成http://ip:端口/prod-api/user/login。前端如果在axios的baseURL里写死了/,登录请求就会打到http://ip:端口/user/login,Tomcat 找不到这个路径,返回 404——注意,这个 404 是接口 404,页面本身是正常的,很容易和前端的路由 404 混淆。
所以改名的动作要在打包阶段完成(用finalName),并且把prod-api这个字符串记下来,第 4 章配前端的时候还要用。
3.6 丢进 webapps 后的第一次启动看什么
# 把 war 拷进 webapps copy target\prod-api.war %CATALINA_HOME%\webapps\ # 启动 %CATALINA_HOME%\bin\startup.bat # 盯日志 type %CATALINA_HOME%\logs\catalina.yyyy-mm-dd.log启动后 Tomcat 会把 war 解压成webapps/prod-api/目录,日志里出现Deployment of web application archive [...] has finished in [x] ms才算部署成功。这一步如果卡住或者报错,先看日志里有没有Unable to process Jar entry,那是 JDK 和编译版本不一致;如果有ClassNotFoundException且类名带javax.servlet,那是 3.2 那处provided配反了。
4. 前端落地与 server.xml:Context、端口、web.xml 三处配置
后端跑起来只是「接口能用」,前端 dist 怎么和它共存于同一个 Tomcat、共用同一个端口,才是这套部署真正要解决的问题。资源里给了两个关键动作:在Host节点下加Context,以及在 dist 下补WEB-INF/web.xml。这两步之外还有一处没写但必须做——端口。
顺带纠正一个文件名:配置在conf/server.xml,不是service.xml。这个拼写错误在传抄的文档里很常见,第一次照着找文件会找不到。
4.1 dist 里到底有什么,别整个目录塞进 webapps
npm run build之后dist目录里通常是这些:
dist/ ├── index.html ├── favicon.ico ├── js/ ├── css/ └── img/要复制进 Tomcat 的是dist里面的内容,不是dist这个目录本身。常见错误是把整个dist文件夹拷进webapps,结果访问路径变成ip:端口/dist/index.html,然后Context的docBase又指着dist,多套了一层,页面能开但资源全 404。
我的习惯是先在webapps下建一个空目录,把dist里的东西全部倒进去,目录名随意但要记住,比如webapps/frontend/。这样docBase写相对路径的时候不会绕晕。
4.2 Host 节点下加 Context:path 与 docBase 怎么填
打开conf/server.xml,找到<Host name="localhost" appBase="webapps" ...>这一段,在它内部、</Host>之前插入:
<!-- conf/server.xml,Host 节点内部 --> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <!-- 前端 dist 的映射:path 是对外访问路径,docBase 是磁盘上的真实目录 --> <Context path="/" docBase="frontend" reloadable="true" crossContext="true" /> </Host>参数说明:
path="/":对外访问路径,/表示根路径,也就是http://ip:端口/直接打开前端首页。如果写成/app,访问地址就变成http://ip:端口/app/,同时前端的publicPath也必须改成/app/。docBase="frontend":指向磁盘目录。写相对路径时,基准是appBase目录,也就是webapps/frontend。也可以写绝对路径D:/deploy/frontend。reloadable="true":Tomcat 监视WEB-INF/classes和WEB-INF/lib的变化,有改动就重新加载。调试阶段开着方便,生产环境建议关掉,它会带来额外的类加载开销。crossContext="true":允许同一个 Tomcat 内的应用通过ServletContext.getContext()互相访问。
这里要说一句实话:资源里认为crossContext="true"是解决白屏的关键,实际上不是。白屏的真正原因是Context本身——它让 Tomcat 知道docBase指向的那个目录是一个 Web 应用根目录,前端路由的/xxx才有机会被应用接管。crossContext管的是跨应用上下文访问,跟前端路由回退没关系,开不开都不影响白屏。这个属性留着无害,但别把它当成解决方案。
4.3 docBase 的三种写法与取舍
| 写法 | 示例 | 适用场景 | 注意点 |
|---|---|---|---|
| 相对 webapps | docBase="frontend" | 目录在webapps下面 | 最省事,迁移机器时不用改 |
| 绝对路径 | docBase="D:/deploy/frontend" | 前端产物放在 Tomcat 目录之外 | 路径分隔符用/或\\,别写单反斜杠 |
| 相对 Tomcat 根 | docBase="../frontend" | 目录和 Tomcat 平级 | 容易数错层级,不建议 |
我一般选绝对路径,把前端产物放到 Tomcat 目录外面,比如D:/deploy/frontend。理由是升级 Tomcat 的时候直接整个目录替换,不会把前端文件一起删掉。代价是每次换机器要改server.xml,所以我会在部署脚本里用变量替换这一段,而不是手工敲。
再补一个进阶写法:不改server.xml,改成在conf/Catalina/localhost/下建一个ROOT.xml文件,内容只写<Context docBase="..." />。这种写法的好处是应用配置和 Tomcat 主配置分离,server.xml升级时不会丢。要注意的是,在这种方式下path属性会被忽略,文件名本身就是访问路径——ROOT.xml对应/,app.xml对应/app。这个细节在很多老文档里没有,踩过一次就记住了。
4.4 Connector 的端口:对外只暴露一个口
<!-- conf/server.xml,Connector 节点 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />参数说明:port就是对外访问端口,前后端共用这一个。资源里说「启动端口为后台的启动端口」,落到配置上就是这一行——SpringBoot 应用本身在 war 模式下不再监听端口,所有请求都进 Tomcat 的 Connector,再由 Tomcat 按 context path 分发到/prod-api或/。
URIEncoding="UTF-8"建议显式加上。Tomcat 8 以后默认就是 UTF-8,但老版本或者被改过的server.xml不一定是,中文参数乱码十有八九出在这里。redirectPort在没有 HTTPS 需求时留着默认值即可。
4.5 刷新 404:dist 下补 WEB-INF/web.xml
前端用的是 history 路由模式(vue-router的mode: 'history'),浏览器地址栏里是ip:端口/user/list这种路径。用户在这个页面上按 F5,浏览器会向 Tomcat 请求/user/list,而磁盘上根本没有这个文件,Tomcat 返回 404。
解法是在 dist 根目录下建WEB-INF/web.xml,把所有 404 都导向index.html,让前端路由自己去解析路径:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <display-name>Router for Tomcat</display-name> <!-- 所有 404 都回首页,交给前端路由处理 --> <error-page> <error-code>404</error-code> <location>/index.html</location> </error-page> </web-app>参数说明:metadata-complete="true"表示不扫描注解,纯静态目录不需要扫描,开着能加快启动。<error-page>只处理 404,<location>用相对于应用根的路径,所以写/index.html而不是磁盘绝对路径。
这份 web.xml 有个副作用要提前知道:它会把所有404 都导向首页,包括接口 404。如果前端请求了一个后端不存在的接口,返回的不是 JSON 而是index.html,前端解析时会报Unexpected token <。排查这类问题时,先在 F12 的 Network 里看响应体开头是不是<!DOCTYPE html>,是的话就说明被这条规则改写了,别去后端找原因。
还有一个维护上的坑:每次npm run build都会清空dist,手工放进去的WEB-INF会被删掉。解决办法是把它放进public/WEB-INF/web.xml,vue-cli 和 Vite 都会把public下的内容原样拷进构建产物。这个动作做一次,后面就不用再记了。
4.6 更省事的两个替代方案
如果对路径没有特别要求,其实有比改server.xml更轻的做法。
第一种:把 dist 里的内容直接放进webapps/ROOT/。ROOT 是 Tomcat 默认的根应用,放进去就是ip:端口/,连Context都不用加。缺点是升级 Tomcat 时要重新放一遍,而且 ROOT 目录容易被误删。
第二种:用 Tomcat 自带的 RewriteValve 做规则回退,效果和 web.xml 一样,但规则更灵活,可以排除/prod-api/*不参与回退。在conf/context.xml里加<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />,再在应用目录下写WEB-INF/rewrite.config:
# WEB-INF/rewrite.config # 接口路径不参与回退,避免接口 404 被改成 index.html RewriteCond %{REQUEST_URI} !^/prod-api/.*$ RewriteCond %{REQUEST_URI} !^/js/.*$ RewriteCond %{REQUEST_URI} !^/css/.*$ RewriteRule ^(.*)$ /index.html [L]这段规则比<error-page>精确的地方在于:它只对「非接口、非静态资源」的路径做回退,接口 404 仍然是 404,前端能正常拿到错误码。代价是多一个配置文件和几个正则,适合项目已经稳定、开始讲究错误处理的时候再上。
5. 五个翻车现场:Tomcat 部署 SpringBoot + Vue 的排查清单
下面这五条,是我在这套部署里反复撞到的。每条按「现象 → 原因 → 解决」写,照着对号入座基本能定位。
5.1 现象:登录成功,进去白屏,F12 里没有报错
原因:server.xml里没加Context,或者docBase指错了目录。Tomcat 把前端目录当成普通静态资源目录,/index.html能直接访问,但/user/list这种路由路径找不到对应文件,返回 404,而页面已经跳转过去了,所以是一片空白。资源里那段「个人猜想」其实猜对了方向——本地开发时是ip:端口/router,部署后 Tomcat 不认识这个 router。
解决:按 4.2 加Context,path="/"、docBase指向前端目录。加完重启,直接访问ip:端口/index.html确认能打开,再访问ip:端口/某个前端路由,如果还是 404,说明Context没生效,去logs/catalina.yyyy-mm-dd.log里搜Context看有没有解析报错。
5.2 现象:页面能打开,一按 F5 就 404
原因:前端是 history 路由模式,刷新时浏览器请求的是真实路径,Tomcat 找不到文件。这和 5.1 是不同的两个问题:5.1 是路由根本没接管,5.2 是接管了但没有回退规则。
解决:按 4.5 在 dist 下补WEB-INF/web.xml。补完之后如果刷新变成白屏,检查<location>是不是写成了绝对磁盘路径,必须是/index.html这种应用内路径。另外确认WEB-INF是直接放在 dist 根目录下,不是放在dist/static/WEB-INF里。
5.3 现象:war 丢进 webapps,后端接口全 404,或者应用压根没起来
原因:通常有四种。一是packaging改了但内嵌 Tomcat 没标provided,两个容器冲突;二是启动类没继承SpringBootServletInitializer,容器找不到 Spring 上下文;三是 war 名和前端baseURL对不上,接口请求打到了/而不是/prod-api;四是 JDK 版本和编译版本不一致,class 文件版本过高。
解决:按顺序排查。先看logs/catalina.yyyy-mm-dd.log有没有Deployment of web application archive这行,没有就是没部署成功;再看webapps/prod-api/WEB-INF/lib里有没有tomcat-embed-core,有就是provided没生效;然后在浏览器 F12 的 Network 里看登录请求的完整 URL,路径前缀不对就改前端baseURL;最后java -version和mvn -v两边的版本对一遍。
5.4 现象:调第三方地图 API 报跨域,控制台一行红字
原因:很多人以为前后端都放在同一个 Tomcat 上就没有跨域了,这个理解只对了一半。浏览器的同源策略看的是「协议 + 域名 + 端口」三元组,页面在http://ip:8080,请求发往https://api.某地图服务.com,协议和域名都不同,照样跨域。资源里第二部分专门讲这个,方向是对的。
解决:跨域要由服务端放行,前端改配置没用。在 SpringBoot 里加一个全局 CORS 配置:
// CorsConfig.java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 生产环境建议写具体域名,不要用通配符 .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") // 允许携带 Cookie 时必须为 true,且 origin 不能用 "*" .allowCredentials(true) .maxAge(3600); } }参数说明:allowedOriginPatterns和allowedOrigins的区别在于前者支持通配符且能和allowCredentials(true)共存,后者在携带凭证时会被浏览器拒绝。maxAge(3600)是预检请求的缓存时间,能减少 OPTIONS 请求次数。如果第三方接口不支持 CORS,那前端直连无解,只能让后端做一层转发,把请求变成同源。
5.5 现象:端口被占、启动窗口一闪而过、字体图标加载失败
原因与解决:
- 端口被占:
netstat -ano | findstr :8080找到 PID,tasklist | findstr PID看是哪个进程,确认能杀再杀,不能杀就改Connector的port,改完记得同步前端baseURL里的端口。 - 启动窗口一闪而过:九成是
JAVA_HOME指向了 JRE 或者路径里有空格没加引号。在bin/setenv.bat里写set "JAVA_HOME=C:\Program Files\Java\jdk-11",引号必须包住整个赋值。 - 字体图标 404 或 MIME 报错:Tomcat 的
conf/web.xml里默认没有woff2的映射。在conf/web.xml的<web-app>里补一段<mime-mapping><extension>woff2</extension><mime-type>font/woff2</mime-type></mime-mapping>,重启即可。这个坑在用了图标库的项目里几乎必踩。
6. 验收这一步:六项自检把「能打开」变成「能交付」
部署完能打开首页,只说明静态资源通了,离「能交付」还差得远。我现在固定走六步,每一步都对应上面某一章配错时的表现,走完心里才有底。
# 1. 启动日志里确认两个应用都部署成功 findstr /C:"Deployment of web application archive" %CATALINA_HOME%\logs\catalina.yyyy-mm-dd.log # 2. 确认 war 解压后的目录结构完整 dir %CATALINA_HOME%\webapps\prod-api\WEB-INF\classes dir %CATALINA_HOME%\webapps\frontend\index.html第 1 条命令要能看到两条部署记录,一条prod-api.war,一条前端目录;只有一条说明另一个没加载。第 2 条确认WEB-INF/classes里有 class 文件,前端目录里有index.html,这两个路径错一个,后面全是白费。
第 3 步,浏览器打开http://ip:端口/,确认首页出来,F12 的 Console 没有红色报错,Network 里 js/css 全是 200。
第 4 步,在首页按 F5 刷新。这一步专门验web.xml的回退规则,刷新后还是首页才算过。
第 5 步,登录进去,随便点几个菜单,然后在任意一个内页地址上再按一次 F5。这一步验的是深层路由直连,比首页刷新严格得多,很多配置只做了一半的项目就是在这里露馅。
第 6 步,F12 打开 Network,找一条业务接口请求,看它的完整路径。正确形态是http://ip:端口/prod-api/xxx/xxx,前缀不对就回去改前端baseURL;如果返回的是index.html的内容而不是 JSON,说明 4.5 那条 404 规则把接口也吞了,换成 4.6 的 rewrite 方案。
下面这张表是我贴在部署记录里的对照,验收时逐行打勾:
| 检查项 | 期望结果 | 不通过时先看哪里 |
|---|---|---|
| 启动日志 | 两条 deployment finished | 3.6 / 4.2 |
| 首页 | 200,无 Console 报错 | 4.1 / 4.3 |
| 首页刷新 | 仍是首页 | 4.5 |
| 内页刷新 | 仍是当前页 | 4.5 |
| 接口路径 | 带/prod-api前缀 | 3.5 / 前端 baseURL |
| 接口响应 | JSON,不是 HTML | 4.5 / 4.6 |
这套流程里最容易省掉的是第 5 步。首页刷新通过就以为万事大吉,结果用户第二天在二级页面按了个 F5,电话就打过来了。从那以后我每次部署完,都会把「首页刷新 + 内页直连 + F12 看接口前缀」这三下强制走一遍,缺一个都不签字。希望帮到你。
本文还有配套的精品资源,点击获取