接手一台新电脑,或者重装完系统坐回工位的那一刻,我最怕的不是项目代码看不懂,而是打开 IDEA 之后那一堆红字:找不到 JDK、Maven 依赖全是红线、Tomcat 点启动就报 404、数据库连接直接超时。IDEA、JDK、Tomcat、Maven、MySQL 这五样东西单独拎出来装任何一个都不难,难的是让它们互相认识——IDEA 要知道用哪个 JDK 编译、Maven 要知道去哪个本地仓库找包、Tomcat 要知道读哪个版本的 Servlet 规范、MySQL 要知道用什么字符集和时区跟 Java 驱动握手。任何一个环节的版本没对齐,都会表现在"代码没问题但就是跑不起来"上。
这篇内容面向的是刚上手 Java Web 的开发者,以及隔三差五就要给别人重装一遍环境的运维和老手。我会把整套流程按"先定版本、再装组件、最后串联验收"的顺序拆开讲,重点放在那些教程里不写、但实际一定踩的坑上:环境变量配了却没生效、Maven 镜像写对了却依然下不动、Tomcat 10 之后包名换了导致老项目全崩、MySQL 8 的认证插件把老驱动挡在门外。看完你至少能做到一件事:下次再配环境,不用一边翻教程一边试错,照着逻辑走一遍就能通。
1. 装之前先想清楚的三件事,比装本身更重要
1.1 为什么安装顺序是 JDK 在前、IDEA 在最后
很多人习惯先把 IDEA 装好,再回头补 JDK 和 Maven,结果 IDEA 第一次启动时扫描不到任何 SDK,弹出一堆向导让人手忙脚乱,还容易在向导里随手点个自动下载,最后机器上出现两三个来路不明的 JDK。我更推荐固定一条顺序:JDK → Maven → MySQL → Tomcat → IDEA。
这条顺序背后的逻辑很简单:后面每一个组件都依赖前面的组件,但它不依赖 IDEA。JDK 是根,Maven 是靠 JAVA_HOME 跑起来的,所以 Maven 必须等 JDK 环境变量生效之后再装;Tomcat 虽然自带启动脚本、不强制要 JDK,但它要跑 Web 应用就必须有 JDK,放在 Maven 后面属于顺手;MySQL 跟前面几个完全不耦合,位置随意,我放在中间是为了让它先跑起来,等最后联调时直接就能连。IDEA 放最后,是因为它是个"消费者"——它需要读 JDK 列表、读 Maven 的 settings.xml、读 Tomcat 的安装目录。前面都摆好了,IDEA 里基本只需要点几下确认路径,不用来回切窗口。
提示:如果你是在公司电脑上装,先确认有没有内网私服和统一的 JDK 版本要求。装完再改本地仓库路径,比一开始就配错要麻烦得多。
1.2 版本搭配表:先定组合,再动手下载
版本不是越新越好。我见过太多人一路 Next 装完最新的 JDK 21 + Tomcat 11 + MySQL 8.4,然后拿一份几年前的教程项目导入,结果 servlet 类全部飘红。下面这张表是我自己反复用过的几套稳定组合,直接抄不会翻车:
| 使用场景 | JDK | Tomcat | Maven | MySQL | 说明 |
|---|---|---|---|---|---|
| 学习/老项目维护 | 8 | 8.5 或 9.0 | 3.8.8 | 5.7 或 8.0 | 大量存量项目仍是 javax.servlet |
| 主流新项目 | 17 | 9.0 | 3.9.x | 8.0 | 目前最省心的组合 |
| 尝鲜新特性 | 21 | 10.1 或 11 | 3.9.x | 8.0+ | 注意 jakarta.servlet 包名变更 |
关键分界线有两条:一条在 JDK,8 到 11 是模块化带来的变化,11 到 17 是 LTS 的稳定跨越;另一条在 Tomcat,9.0 及以前用 javax.servlet,10.0 开始改成 jakarta.servlet。这两条线决定了你的项目能不能直接编译过。如果团队没有明确要求,我一般推荐 JDK 17 + Tomcat 9.0 + Maven 3.9 + MySQL 8.0,这套组合的兼容面最宽,网上能搜到的解决方案也最多。
1.3 目录规划:别塞进 Program Files,也别碰中文路径
这一步看起来无关紧要,实际上是后面一半报错的源头。默认安装向导喜欢把 JDK 丢到C:\Program Files\Java\下面,把 MySQL 丢到C:\Program Files\MySQL\,把 Maven 本地仓库丢到C:\Users\你的中文用户名\.m2\。三个问题紧接着就来了:
第一,Program Files中间有空格,某些老脚本、老插件在做字符串拼接时不加引号,路径直接被截断;第二,这些目录受系统权限保护,配置文件和临时文件写入容易被拒;第三,中文用户名会让部分命令行工具在读取本地仓库路径时出现编码异常,报错信息还特别隐晦,只说"找不到文件"。
我自己的习惯是统一放在一个纯英文、无空格的根目录下,比如:
D:\dev\ ├── jdk\jdk-17 ├── maven\apache-maven-3.9.6 ├── m2repo\ ├── tomcat\apache-tomcat-9.0.x └── mysql\mysql-8.0.x好处不只是"看起来整齐"。以后你想换 JDK 版本,只要新增一个jdk\jdk-21目录,改一下 JAVA_HOME 就切过去了;想清空 Maven 本地仓库,直接删m2repo目录,不会牵连到系统盘;备份环境时把D:\dev整个打包拷走,新机器上大部分路径都不用改。
注意:本地仓库目录不要放在会被同步软件(网盘、云盘客户端)实时监控的目录里。Maven 下载依赖时会创建大量小文件,同步进程会锁住文件导致解压失败,报错通常是 "Could not transfer artifact" 或者压缩包损坏。
2. JDK:环境变量配不对,后面全是白干
2.1 选哪个发行版,官网打不开时去哪找
JDK 现在不是一个"唯一答案"。商业版、开源社区版、各家厂商的长期支持版都能用,常见的几个方向:
- 需要一个通用、社区维护活跃的版本,可以考虑 Eclipse Temurin、Amazon Corretto 这类开源构建;
- 需要跟国内镜像源配合加速下载的,用国内厂商维护的构建版会快很多;
- 公司有指定采购的,按公司要求走。
版本上,我的建议是学习阶段直接上 LTS 版本,别去碰非 LTS 的奇数版本。LTS 的补丁周期长、资料多、第三方库支持充分,遇到问题搜一下就有答案;非 LTS 版本生命周期只有半年,等你项目写完了可能都停止维护了。
安装形态上有两种:一种是带安装向导的(Windows 上是.msi或.exe),一种是压缩包(.zip或.tar.gz)。我强烈建议用压缩包。原因有三个:安装向导会把文件散落到系统目录、注册表和环境变量里,卸载时未必干净;向导版有时会自动往系统 Path 里塞一个自己的软链接目录,和你手动配的 JAVA_HOME 打架(后面会细讲);压缩包解压即用,换版本就是换个目录,出问题了直接删掉重来,零残留。
2.2 JAVA_HOME 和 Path 到底要配几个
网上教程里那一长串变量名——JAVA_HOME、Path、CLASSPATH、JRE_HOME——建议只配两个:JAVA_HOME 和 Path。
具体操作(以 Windows 为例,图形界面路径:此电脑右键 → 属性 → 高级系统设置 → 环境变量):
- 在"系统变量"里新建
JAVA_HOME,值填 JDK 的根目录,注意是根目录,不是bin目录,例如D:\dev\jdk\jdk-17; - 找到系统变量里的
Path,编辑,新增一行%JAVA_HOME%\bin,并且把它移动到列表最上方; - 一路确定保存,然后关掉所有已经打开的终端窗口,重新开一个。
第三点特别容易被忽略。环境变量是进程启动时从父进程继承的,已经开着的 CMD 或 PowerShell 窗口不会自动刷新。我见过有人配完变量后一直在旧窗口里敲java -version,看到还是老版本,然后开始怀疑人生。
至于 CLASSPATH,很多老教程会让你配一堆路径,比如.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar。这个在 JDK 9 以后就不需要了,dt.jar和tools.jar在新版本里根本不存在,配了反而会因为指向不存在的路径引发一些奇怪的类加载问题。除非你在维护一个明确要求 CLASSPATH 的极老项目,否则直接跳过。
还有一个容易踩的坑:设置Path的时候,%JAVA_HOME%\bin这一行不要加引号、不要加结尾的分号、不要在值里写完整路径又留了分号。带引号在某些解析场景下会被当成路径的一部分,结果就是"明明目录存在却说找不到命令"。
2.3 javac 不是内部或外部命令:一条完整排查链路
这个报错我帮人处理过不下二十次,排查顺序基本可以固定下来:
第一步,确认变量本身对不对。打开新的 CMD,执行:
echo %JAVA_HOME%如果输出的不是你的 JDK 根目录,说明变量名写错、写在了用户变量而当前账户读取顺序不同,或者压根没保存成功。
第二步,确认 Path 里有没有生效。执行:
echo %Path%把输出内容从头看到尾,找有没有%JAVA_HOME%\bin。注意这里是变量引用,如果它被展开成实际路径了也能用;但如果你看到的是字面量%JAVA_HOME%\bin一字不差地待在那里,说明它前面那个JAVA_HOME变量没被识别,多半是变量名拼错了。
第三步,看系统到底找到了哪个 java。这一步是分水岭:
where java where javac正常的输出应该指向D:\dev\jdk\jdk-17\bin\java.exe。如果你看到排在第一位的是这样一个路径:
C:\Program Files\Common Files\Oracle\Java\javapath\java.exe那问题就找到了。这个目录是某些 JDK 或 JRE 安装包自动塞进系统 Path 的软链接目录,而且它的优先级往往排在你手动加的那行前面。系统先命中了它,你配的 JAVA_HOME 就形同虚设。处理办法很简单:在系统变量 Path 里把这个javapath的条目删掉,或者往下移,确保%JAVA_HOME%\bin在最上面。
第四步,如果 where 找到的是正确路径,但javac还是报错。那说明你装的可能只是 JRE 而不是 JDK。JRE 只有java.exe,没有javac.exe。回头看看你解压的目录里有没有bin\javac.exe,没有就重新下载 JDK 包。
第五步,全都对但 IDEA 里还报找不到 JDK。这通常不是环境变量的问题,而是 IDEA 的项目配置问题,看下一节。
2.4 IDEA 里的三层 SDK 结构,别只配第一层
很多人以为"系统环境变量配好了,IDEA 就自动用上了",其实 IDEA 有自己独立的一套 SDK 配置,跟系统环境变量是两条线。你需要认准三个层次:
| 层次 | 位置 | 作用 |
|---|---|---|
| Project SDK | File → Project Structure → Project | 整个项目默认用哪个 JDK |
| Language Level | 同一个页面下方 | 源码允许使用的语法特性级别 |
| Module SDK | 同一窗口 → Modules → Sources | 多模块项目里每个模块单独指定 |
还有一个隐藏在 Maven 里的第四层:Settings → Build Tools → Maven → Runner里的 JRE 选项,以及Importing里的 JDK for importer。这两个如果没跟上你的 Project SDK,会出现"编译能过但运行报 UnsupportedClassVersionError"这种诡异现象,本质是用 JDK 8 编译出来的 class 想跑在 JDK 17 以外的地方,或者反过来用了高版本 class 文件格式跑在低版本 JVM 上。
经验:报错信息里出现
class file version 61.0这类字样时,别去猜。对照一下也能记住:52 是 JDK 8,55 是 JDK 11,61 是 JDK 17,65 是 JDK 21。看到版本号不匹配,直接去查各层的 SDK 设置,基本一次命中。
3. Maven:难点不在装,而在"三处路径对齐"
3.1 Maven 到底解决了什么问题
刚接触时容易把 Maven 理解成"一个下载 jar 包的工具",这只是它最表层的功能。它实际解决的是三件事:
第一,依赖管理。以前做 Web 项目,你要手工去各个网站下 jar 包,然后一股脑塞进WEB-INF/lib。A 依赖 B、B 又依赖 C 这种传递关系全靠人肉维护,稍微升级一个包就要重新找一圈,还经常出现同一个类存在两份不同版本的情况。
第二,统一的构建生命周期。clean、compile、test、package、install这一套阶段是标准化的,不管换哪台机器、哪个 IDE,执行的流程都一样。这也是为什么 CI 环境里几乎看不到手工打包。
第三,项目结构的约定。src/main/java、src/main/resources、src/test/java这套目录约定,让所有人打开项目就知道代码该放哪。约定优于配置,省下来的沟通成本非常可观。
3.2 settings.xml 里我只改三处
Maven 也推荐用压缩包,解压到D:\dev\maven\apache-maven-3.9.6,然后把MAVEN_HOME加到环境变量、把%MAVEN_HOME%\bin加进 Path。验证方式是mvn -v,输出里会同时打印 Maven 版本和它认到的 Java 版本——这一步非常重要,如果打印出来的 Java 版本不是你期望的那个,说明 Maven 读到了错误的 JAVA_HOME,后面所有编译都有隐患。
接下来是核心:配置文件settings.xml。它在两个位置存在——Maven 安装目录下的conf\settings.xml(全局配置)和用户目录下的.m2\settings.xml(用户配置)。用户配置优先级更高,会覆盖全局配置。这就是为什么很多人改了安装目录下的文件却不生效:他之前某次操作已经在用户目录下生成了一份。建议只维护一份,我习惯改安装目录里的那份并明确指定 IDEA 使用它,同时把用户目录下的那份删掉,避免混淆。
这份文件里我只改三处:
第一处,本地仓库路径。在<settings>下加:
<localRepository>D:\dev\m2repo</localRepository>第二处,镜像仓库。找到<mirrors>节点,加一个镜像:
<mirror> <id>aliyun-public</id> <mirrorOf>*</mirrorOf> <name>aliyun public</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>这里有两个高频坑。一是URL 必须用 https——Maven 3.8.1 之后默认禁止通过 http 访问外部仓库,你写 http 会直接报 "blocked mirror" 或者干脆被忽略;二是<mirrorOf>里不要同时写多个互相冲突的条目,比如又有一条<mirrorOf>*</mirrorOf>又有一条<mirrorOf>central</mirrorOf>,Maven 只会按第一个匹配的处理,你以为配了两份保险,实际只用了其中一条。
第三处,编译级别。在<profiles>里配置一个默认激活的 profile,声明编译插件使用的 JDK 版本。或者更简单,直接在项目 pom 里用属性声明,这两种我会搭配使用,具体在 3.5 节讲。
注意:改了 settings.xml 之后,之前下载失败的依赖垃圾需要清理。本地仓库里会留有大量
.lastUpdated后缀的文件,它们标记了"这次下载失败过",Maven 在一段时间内不会重试。要么手动搜索删除这些文件,要么执行带上-U参数的构建强制更新。
3.3 IDEA 里三处路径必须对齐,否则等于没配
打开 IDEA 的Settings → Build, Execution, Deployment → Build Tools → Maven,这个页面有三个输入框:
- Maven home path:默认值可能是 IDEA 自带的 Bundled (Maven 3),这是个大坑。用 IDEA 自带的 Maven,你的
MAVEN_HOME和手工安装的那份就完全不起作用了。改成你自己的安装目录。 - User settings file:指向你真正在维护的那份 settings.xml,并勾上右边的 Override 复选框。不勾的话,IDEA 会用默认位置。
- Local repository:正常情况下它会自动读取 settings.xml 里的配置,如果这里显示的还是默认的
.m2\repository,说明 settings.xml 没被正确加载,回到上一步检查。
这三个值不一致时,最典型的症状是:命令行mvn clean package一切正常,IDEA 里却依赖全红、提示找不到包。因为 IDEA 用的是自己那套配置,去另一个仓库目录里找,当然找不到。
另外那个JDK for importer也要选成项目使用的 JDK。它的作用是在导入依赖、解析 pom 时用哪个 JVM,选错了会在导入阶段就报错,压根走不到编译。
3.4 依赖拉不下来的几种真实情况和处理顺序
遇到依赖变红,按这个顺序排查效率最高:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 控制台长时间无输出后超时 | 镜像源不通或网络受限 | 换镜像地址,或用浏览器访问仓库地址验证连通性 |
| 报 blocked mirror / 拒绝明文传输 | 镜像 URL 用了 http | 改成 https |
| 报找不到某个具体版本 | 坐标写错或该版本不存在 | 去仓库网站搜一下确认版本号 |
| 报包损坏 zipfile is empty | 下载中断留下半成品文件 | 删除对应目录重新下载 |
| 命令行正常、IDEA 报错 | 三处路径没对齐 | 回到 3.3 逐个核对 |
还有一种情况很隐蔽:依赖其实下下来了,但 IDEA 没刷新索引。习惯做法是点 Maven 面板里的刷新按钮,或者右键 pom 文件选 Maven → Reload project。如果还不行,执行File → Invalidate Caches重建索引。这个操作治百病,但每次都要重建,耗时较长,所以只在确认配置没问题时才用。
3.5 编译级别到底听谁的
pom.xml里的编译级别、IDEA 项目设置里的 Language Level、以及 Maven 编译插件实际用的 JDK,这三者如果打架,会出现"IDEA 里不报错但打包失败"或者反过来。
我现在的做法是在 pom 里统一声明:
<properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>用release而不是老的source/target组合,是因为release参数会同时约束字节码版本和可用的 API 集合,能防止你在 JDK 17 下用了 JDK 21 才有的 API、结果运行时才炸。再加上project.build.sourceEncoding声明 UTF-8,能避免一类很烦人的问题:本地用 UTF-8 写的代码,在默认 GBK 的机器上编译出来中文全变乱码。
4. MySQL:装完能连上只是及格线
4.1 安装版还是解压版
MySQL 在 Windows 上有两种装法:官方安装器(Installer)和免安装压缩包。安装器胜在省事,能一路点下去,还能顺手装 Workbench 可视化客户端;压缩包胜在干净可控,配置全在一个my.ini文件里,出了问题好定位。
我给新手的建议是第一次装用安装器,因为你需要一个能跑起来的环境先找到感觉;等你第二三次重装或者要在服务器上部署时,再转压缩包。安装器里有个环节要注意:在选择安装类型时不要选 "Developer Default",它会顺带装一堆你可能永远用不到的东西,还占服务端口。选 "Custom" 只勾 MySQL Server 和 MySQL Workbench 就够了。
如果用压缩包,流程是:解压到D:\dev\mysql\mysql-8.0.x,手动创建my.ini,用管理员身份执行mysqld --initialize-insecure(不设密码初始化)或--initialize(生成随机密码,在日志里找),然后mysqld --install注册成服务,再net start mysql启动。
4.2 字符集、时区、认证插件三个必改项
字符集:默认字符集务必确认是utf8mb4。老版本里那个utf8其实是"最多三个字节的 UTF-8",存不了 emoji 和部分生僻字,插入时报 "Incorrect string value"。改法是在my.ini的[mysqld]段加上:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci在建库建表时再显式声明一次,双保险:
CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;时区:这是 Java 连接 MySQL 时最常见的报错来源。服务端时区、操作系统时区、JDBC 连接串里声明的时区三者不一致,就会报 "The server time zone value is unrecognized" 或者你存进去的时间跟取出来的差 8 小时。最稳的做法是在连接串里明确写死:
jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueuseSSL=false是为了避免本地开发时反复弹 SSL 警告,allowPublicKeyRetrieval=true是配合下面要说的认证插件用的。
认证插件:MySQL 8.0 默认使用caching_sha2_password,而一些老版本的 JDBC 驱动只认识mysql_native_password,连接时会报 "Unable to load authentication plugin"。两条路:要么升级驱动到 8.0 以后的版本(推荐),要么把账号的认证方式改回去:
ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;我更推荐升级驱动,因为改认证方式属于降级兼容,新装的库没必要为老驱动让步。
4.3 建库建账号:别再到处用 root
开发阶段用 root 图省事,但把 root 密码写进项目配置文件、提交到版本库,是实打实的安全隐患。我现在的习惯是每个项目单独建库、单独建账号,只给这个库需要的权限:
CREATE USER 'demo_app'@'%' IDENTIFIED BY '一个足够长的密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON demo.* TO 'demo_app'@'%'; FLUSH PRIVILEGES;注意这里没有给DROP、ALTER、CREATE权限。开发阶段如果需要频繁改表结构,可以临时加上,上线前一定收回来。这个习惯的价值不在当下,而在于它能防止一类事故:代码里一个手滑的删表操作,因为权限不足直接失败,而不是真的把数据删了。
4.4 连不上数据库时的排查顺序
按这个顺序走,基本五分钟内能定位:
- 服务在不在。Windows 下执行
net start | findstr /i mysql,或者去服务管理器看 MySQL 服务状态。服务没起,后面都别谈。 - 端口通不通。
netstat -ano | findstr 3306。如果 3306 没被监听,回去看服务日志(数据目录下的.err文件),大概率是初始化失败或my.ini里datadir路径写错。 - 端口被谁占了。如果 3306 被别的进程占用、MySQL 起不来,可以改端口,也可以处理掉占用进程。改端口后记得同步改连接串。
- 账号能不能登。用命令行客户端
mysql -u demo_app -p -h 127.0.0.1试一下。命令行能进、Java 连不上,问题就在驱动或连接串。 - 驱动版本对不对。MySQL 8 的服务端配 5.x 的驱动,十有八九出问题。pom 里统一用
mysql-connector-j的新版本。 - 连接串参数全不全。时区、编码、SSL、公钥检索这四项,缺一项就可能有报错。
4.5 忘了 root 密码时的自救流程
这个场景很常见,尤其是隔了几个月回来接着做项目。标准做法是临时跳过权限校验启动:
- 停掉 MySQL 服务;
- 用
mysqld --skip-grant-tables --console方式启动,此时不需要密码即可连接; - 用命令行客户端连进去,执行
FLUSH PRIVILEGES;刷新权限表,然后ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';; - 关掉这个进程,正常启动服务,用新密码登录。
注意:
--skip-grant-tables期间任何本地进程都能无密码连入,操作时确保这台机器没有对外暴露数据库端口,改完立刻恢复正常启动。
5. Tomcat:那道 javax 与 jakarta 的墙
5.1 版本选型比安装重要得多
Tomcat 的安装本身几乎没有技术含量——下载 zip、解压、完事。真正的分水岭在版本选择上:
Tomcat 9.0 及以前,Servlet API 的包名是javax.servlet;从 Tomcat 10.0 开始,换成了jakarta.servlet。
这意味着什么?你手上任何一份基于javax.servlet写的代码——不管是HttpServlet、Filter还是@WebServlet注解——放到 Tomcat 10 里,编译直接过不去,或者编译能过但运行时抛ClassNotFoundException。而网上大量教程、示例项目、甚至一些仍在维护的老框架,用的都是javax那一套。
所以我的建议是:除非你明确知道项目用的是新规范,否则先上 Tomcat 9.0 系列。它能兼容的场景足够多,遇到的坑也最少。等你要学 Jakarta EE 新规范了,再单独装一份 10.1 并注意两个版本不要共用环境变量。
5.2 解压即用与三个必看文件
解压之后的目录结构值得花两分钟看一眼:
| 目录 | 作用 | 你会用到它做什么 |
|---|---|---|
| bin | 启动停止脚本 | startup.bat / shutdown.bat 手动测试 |
| conf | 配置文件 | 改端口、改编码、加数据源 |
| webapps | 应用部署目录 | 手工部署 war 时放这里 |
| logs | 日志 | 启动失败时第一时间看这里 |
| lib | 公共依赖 | 放全局 jar,但一般别动 |
三个必看文件:
conf/server.xml,改端口和编码。找到 Connector 配置:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />URIEncoding="UTF-8"是解决中文参数乱码的关键。老版本默认不是 UTF-8,GET 请求里带中文参数就会乱码,很多人以为是前端编码问题,折腾半天。
conf/logging.properties,改控制台日志编码。Windows 控制台默认 GBK,Tomcat 日志输出 UTF-8,叠加起来就是满屏乱码。可以在文件里把控制台处理器的编码改成 GBK:
java.util.logging.ConsoleHandler.encoding = GBK或者在 IDEA 的启动配置里加 VM 参数-Dfile.encoding=UTF-8。两种方案选一种即可,千万别两边都改,否则可能从一种乱码变成另一种乱码。
bin/catalina.bat,需要调整 JVM 内存时改这里(一般通过设置CATALINA_OPTS环境变量更规范)。
5.3 IDEA 里配 Tomcat 的几种方式,以及社区版的现实
先泼一盆冷水:Tomcat 的集成运行是 IDEA 旗舰版的功能,社区版没有。如果你用的是社区版,在 Run/Debug Configurations 里翻遍也找不到 Tomcat Server 选项,这是正常的,不是你没找到。你有三个选择:
| 方案 | 适用场景 | 代价 |
|---|---|---|
| 换用旗舰版 | 公司项目、长期开发 | 需要授权,学生和开源项目可以申请免费授权 |
| 装 Smart Tomcat 插件 | 社区版临时跑 Web 项目 | 配置项少,功能比原生集成弱 |
| 用 Maven 的 Tomcat 插件 | 只想快速验证 | 插件版本老,对新规范支持有限 |
| 改用 Spring Boot 内嵌容器 | 新项目 | 需要改造项目结构 |
旗舰版的配置路径是Run → Edit Configurations → + → Tomcat Server → Local,然后指定 Tomcat 安装目录,在 Deployment 标签页里添加 Artifact。社区版用户如果只是想验证一个 Servlet,我建议直接用命令行把项目打包成 war 丢进webapps,然后跑startup.bat,虽然笨但一定通。
顺带提一句内嵌容器的思路,这是现在新建项目的主流做法:Spring Boot 项目默认就把 Tomcat 作为内嵌容器打进来了,spring-boot-starter-web里已经包含了它。如果你因为某些原因需要替换内嵌容器,标准做法是在这个依赖里排除掉 Tomcat,再引入其他容器的 starter。代码层面几乎不用改,因为 Servlet API 是统一的——当然,前提是你已经跨过了 5.1 节说的那道包名变更的墙。
5.4 war 与 war exploded 的区别,以及热更新怎么开
在 Deployment 页面添加 Artifact 时,你会看到两种形态:
- war 包:把整个项目打成一个压缩包再部署,改了代码必须重新打包才生效;
- war exploded:把目录结构原样展开部署,Tomcat 直接读目录里的 class 和资源文件,改完可以增量更新。
开发阶段一律选war exploded。然后在旁边的 "On 'Update' action" 和 "On frame deactivation" 两个下拉框里选择更新策略:前者是手动触发更新时的动作(一般选 Update classes and resources),后者是切换到浏览器等窗口时的自动动作(选 Do nothing 避免频繁触发拖慢 IDEA)。
这里有个经典困惑:改了 Java 代码,浏览器刷新了但页面还是老逻辑。原因是 Tomcat 默认只重新加载被修改过的 class,但如果你的修改涉及方法签名变更、新增类、改了注解,热更新往往不到位。这时候不要犹豫,直接重启 Tomcat。我在开发时对 JSP、HTML、CSS 这类资源改动依赖热更新,对 Java 代码改动一律重启,反而更省时间。
5.5 三类高频故障的定位手法
第一类,8080 端口被占用。报错关键字是 "Address already in use"。执行:
netstat -ano | findstr :8080拿到最后一列的 PID,去任务管理器对照结束进程。如果占用者是java.exe,很可能是上一次 IDEA 里的 Tomcat 没完全停掉,或者另一个 IDE 窗口还在跑。
第二类,启动日志乱码。回去看 5.2 节,注意只改一边。
第三类,访问根路径 404。按这个顺序看:
- Application context 是不是设成了
/之外的值。设成/demo的话,你访问根路径当然是 404,要访问http://localhost:8080/demo/; - 项目的 Artifact 有没有正确地包含进 Deployment 列表;
- 是不是把
web.xml里的url-pattern和浏览器地址对不上,或者在 Tomcat 10 里用了javax.servlet的注解(这种情况下类根本不会被扫描到,控制台可能只有一行不起眼的警告)。
第三条我再强调一次,因为它太隐蔽了。注解不被识别时不会报错,只会静静地不注册这个 Servlet,然后在访问时给你一个 404。遇到"代码怎么改都 404",先确认包名。
6. 把四个组件串起来跑通一遍
前面都配好之后,一定要做一次端到端验证。不是为了写业务代码,而是为了确认这条链路上没有暗雷。
6.1 用 Maven 骨架起一个最小 Web 工程
在 IDEA 里新建项目,选 Maven 骨架maven-archetype-webapp。它会生成标准的 webapp 目录结构和一份最简 pom。生成后先做两件事:把Project SDK设为你的 JDK,把 Language Level 对齐(见 2.4 节)。
6.2 pom 里需要补的三样东西
第一,打包方式必须是 war:
<packaging>war</packaging>第二,Servlet API 的依赖。注意作用域必须是 provided,否则它会被打进 war 包,跟 Tomcat 自带的版本冲突:
<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency>用 Tomcat 9 的话,这里要换成javax.servlet:javax.servlet-api,版本 4.0.1。这就是 5.1 节那道墙在具体代码里的样子。
第三,MySQL 驱动和连接池。驱动用mysql-connector-j,连接池用 HikariCP 或 Druid 都行,这里用 Druid 举例。
6.3 写一个同时碰到 Tomcat 和 MySQL 的验证页面
写一个 Servlet,在init或doGet里建一次数据库连接,查一行数据,然后打印出结果。不用写在正式代码里精心设计,一个最朴素的实现就够:
@WebServlet("/ping") public class PingServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String url = "jdbc:mysql://127.0.0.1:3306/demo" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; try (Connection conn = DriverManager.getConnection(url, "demo_app", "你的密码"); Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("select 1")) { rs.next(); resp.setContentType("text/plain;charset=UTF-8"); resp.getWriter().println("db ok: " + rs.getInt(1)); } catch (Exception e) { resp.setContentType("text/plain;charset=UTF-8"); resp.getWriter().println("db fail: " + e.getMessage()); } } }这个页面同时验证了四件事:Tomcat 能加载并注册 Servlet、Servlet 注解被正确扫描、JDBC 驱动能被加载、连到 MySQL 的账号密码和参数都对。任何一项有问题,它都会给你一个明确的失败信息,比写一堆业务代码再慢慢排查高效得多。
6.4 从 IDEA 一键跑起来
配置 Tomcat 运行项,Deployment 里加war exploded,Application context 设成/demo,启动。浏览器访问:
http://localhost:8080/demo/ping看到db ok: 1,整个环境就算真的搭完了。如果显示db fail: ...,看冒号后面的具体异常:Access denied是账号密码或权限问题,Communications link failure是服务或端口问题,Unknown database是库名写错了,The server time zone value是连接串缺时区参数。异常信息本身就是最好的排查指南,别跳过它去瞎猜。
7. 多版本共存与环境迁移的实操经验
7.1 一台机器上跑多个 JDK 和多份 Tomcat
做老项目维护的人几乎都会遇到:手上有个 JDK 8 的老系统和一个 JDK 17 的新系统要同时开发。系统级的 JAVA_HOME 只有一个,来回改太累。我的做法是:
系统 JAVA_HOME 固定指向最常用的那个版本,保证命令行下java -version是日常用的版本;然后在 IDEA 里给每个项目单独设置 Project SDK,指向不同的 JDK 目录。IDEA 是完全按项目隔离的,多个窗口同时运行不同 JDK 的项目也没问题。
命令行需要临时切换时,写两个批处理脚本:
REM use-jdk8.bat set JAVA_HOME=D:\dev\jdk\jdk-8 set PATH=%JAVA_HOME%\bin;%PATH% java -version注意这种set只影响当前窗口,关掉就恢复。不要用setx去改系统变量,那会把全局状态改掉,下次又得改回来。
Tomcat 同理:不同版本解压到不同目录,各自的conf/server.xml里把端口错开(比如 8080、8081、8082),这样几份 Tomcat 可以同时启动,互不干扰。端口冲突是很多人以为"Tomcat 坏了"的真实原因。
7.2 换电脑时怎么把环境搬过去
我整理过一份自己的迁移清单,按这个顺序恢复几乎没有返工:
- 先确认新机器的用户名和安装盘符,如果和旧机器不一致,所有硬编码路径的配置文件都要改,包括 settings.xml、my.ini、IDEA 的 SDK 配置(IDEA 配置在用户目录下,跟着账户走,路径变了要重新指);
- 拷贝
D:\dev整个目录,保持相对结构不变; - 重新配置系统环境变量:JAVA_HOME、MAVEN_HOME、Path 三条;
- 用
mysqld --install在新机器上重新注册 MySQL 服务(服务注册信息存在系统里,不是拷贝目录就能带过去的,这一步最容易漏); - 启动各组件做一次版本验证:
java -version、mvn -v、mysql --version、Tomcat 的 startup; - 最后打开 IDEA,核对 Maven 的三处路径和 Project SDK。
经验:第 4 步是迁移时的高频故障点。很多人拷完目录直接
net start mysql,报"服务名无效",因为服务压根没在新机器上注册过。而且注册时必须用管理员权限的终端,否则会静默失败。
8. 一些我自己踩出来的碎经验
关于环境变量:任何时候改完环境变量,先关掉所有终端再重开,这一条能省掉至少三成的"配了没生效"。
关于版本:能选 LTS 就选 LTS,能选流行组合就选流行组合。环境搭建的目标是让后面的开发顺利,不是给未来的自己增加调试负担。用冷门版本组合省下来的那点新鲜感,远不够填排查问题的时间。
关于配置文件的维护:我会在D:\dev目录下放一个env-notes.md,记录每个组件的版本号、安装路径、端口、账号(密码不写明文,只写存放位置)。下次重装或者换机器,翻这个文件比翻浏览器历史快得多。这个小习惯救过我很多次,尤其是隔了半年再回来维护某个老项目的时候。
关于排查心态:环境类问题的特点是症状五花八门但根因集中。看到报错别急着搜关键词,先问自己两个问题——这个组件的版本是哪个?它和上下游的版本对不对得上?把这两问答清楚,绝大多数问题会自己浮出水面。