每次带新人,只要涉及 IDEA 新建项目这件事,问题就特别集中:代码一个字没改,浏览器一打开就是 404;依赖明明写在 pom 里,编译却报“找不到符号”;JSP 改完刷新还是老样子。这些毛病九成不是业务代码写错,而是“新建项目”那一步埋下的伏笔。IDEA 新建项目(Java 项目、JavaWeb 项目)看着只是点几下 Next,实际上它在后台同时干了四件事:确定项目 SDK(就是项目使用的 JDK)、生成目录结构、建立模块与源码根目录的映射、声明构建方式与部署产物。这四件事里任何一件选偏,后面都要花几个小时甚至几天去补。这篇东西就是把这四件事拆开讲透,从环境检查、普通 Java 项目搭建,到 Maven 版 JavaWeb 项目的目录、依赖、Tomcat 部署配置,再到我这些年被坑出来的排查套路,全部铺开写,新手照着做能跑通,有基础的人也能在里面对照出自己一直没搞明白的那个环节。
1. 先把地基摸清:IDEA 里的项目模型到底是什么
1.1 Project、Module、Facet、Artifact 四层结构
很多人用 IDEA 很久,还是把 Project 当成“一个文件夹”,这个理解偏差是后面所有困惑的源头。IDEA 的模型是分层的,从上到下依次是 Project、Module、Facet、Artifact,四层各有各的职责,一层断链,运行阶段就会以 404 或者 ClassNotFound 的形式把问题甩回给你。
Project 是最外层的工作空间,对应你打开的那个窗口,它在磁盘上体现为一个根目录加上一个隐藏的.idea配置目录。.idea里存的不是代码,而是这个工作空间的元信息:用了哪个 JDK、包含哪些模块、编码设置、运行配置等等。你把这个目录删掉,项目结构信息基本就丢了,只剩下源码文件;反过来,如果你换台机器只拷了.idea没拷源码,打开就是一堆红。
Module 是真正的代码单元和构建单元。一个 Project 下面可以有多个 Module,每个 Module 有独立的源码根目录、独立的依赖列表、独立的编译输出路径。日常说的“这个项目拆成几个模块”,说的就是这一层。Module 的配置文件在旧版本里是一个.iml文件,新版本收进了.idea/modules目录里,但本质没变,它记录的是“哪些目录算源码、哪些算资源、依赖从哪来、编译产物扔哪去”。
Facet 是 IDEA 给模块贴的“身份标签”。一个模块如果被标记了 Web Facet,IDEA 才知道这个模块里有 Web 资源目录、有部署描述符web.xml,才知道打包的时候该去哪找WEB-INF。这个标签是纯 IDE 层面的概念,跟代码本身没关系,所以它最容易被忽略,也最容易出问题——你明明建了webapp目录,也放了index.jsp,但模块上没有 Web Facet,IDEA 就当作没看见,部署下去自然什么都没有。
Artifact 是打包产物的描述,回答的是“部署的时候到底把哪个东西扔进容器”。它有两种常见形态:war exploded指的是一个展开的目录,容器直接读这个目录;war指的是压缩好的归档包。Artifact 的配置里会明确写出输出目录、包含哪些模块的编译结果、Web 资源根目录在哪。Tomcat 的运行配置里挂的就是 Artifact,这一层要是挂错了或者没挂,服务器能起来,但页面就是打不开。
我把这四层的关系画成一句话:Project 是壳,Module 是肉,Facet 是标签,Artifact 是快递包裹。壳对了肉不对,编译不过;肉对了标签没贴,Web 资源不被识别;标签贴了包裹没装,服务器跑起来也是空的。
1.2 为什么“新建项目”这一步最容易翻车
翻车集中在三个地方。
第一个是版本链条没对齐。JDK 版本、语言级别(Language Level)、编译目标版本(target)、Maven 编译插件的 source/target、Servlet 规范版本,这几项必须是同一条链上的。我见过太多次这样的组合:项目 SDK 选的 17,语言级别是 8,pom 里写的是 1.8,然后用了某个只在高版本才有的语法,IDEA 编辑器不报错,一编译就炸。或者反过来,环境全按 8 配,依赖里拉进来的某个包要求 11 以上,运行期直接UnsupportedClassVersionError。
第二个是“模板默认值”陷阱。IDEA 的向导每个版本都在变,默认勾选项也在变。老版本新建 Java 项目时有个 “Java Web” 的附加选项可以勾,新版本改成了独立的 Jakarta EE 模板;老版本是先建 Project 再建 Module,新版本向导里直接就把 Module 建好了。你照着两年前的教程点,会发现界面对不上,然后凭感觉乱点,最后建出来的结构跟教程里完全不一样。这不是你的问题,是教程过期了,但代价要你自己承担。
第三个是目录标记。源码目录没标成 Sources Root(蓝色),IDEA 就不会编译里面的类;资源目录没标成 Resources Root,配置文件就不会进 classpath;webapp目录没在 Facet 里指认,静态资源就不会被部署。这三个标记全是“右键菜单里的一个选项”,新手根本想不到要去点,结果就是代码看着没问题,运行死活不对。
提示:新建项目之前,先花三十秒确认三件事——本机装了几个 JDK、IDEA 里配的是哪个、命令行
java -version输出的是哪个。很多奇怪的编译错误,起因就是这三者不一致。
2. 环境前置准备:新手最容易忽略的检查清单
2.1 JDK 的选择与配置
JDK 这件事,我给的建议很直接:本机保留两到三个版本就够了,别再多了。常见组合是一个 8(维护老项目)、一个 11 或 17(新项目)。装太多版本的直接后果是,你在 IDEA 里选 SDK 的时候根本分不清哪个是哪个,路径还都长得差不多。
装完之后要在 IDEA 里登记一次。入口在 Project Structure 里的 SDKs 面板,点加号,选 JDK 的安装根目录,IDEA 会自动识别出版本号并给它起个名字。这里有个小技巧:给 SDK 命名的时候带上完整版本号,比如“JDK 1.8.0_301”,别用默认的“1.8”,因为同一个大版本可能有多个小版本,出问题时你连自己用的是哪个都不知道。
项目级的 SDK 在 Project 面板里设置,同时下面还有一个 Language Level 下拉框。这两项的关系是:SDK 决定“用哪个编译器”,Language Level 决定“允许用什么语法”。两者不一致时,编辑器会容忍你的写法,但编译阶段会报错。我的做法是让 Language Level 永远跟着 SDK 走,除非是老项目必须降级,那就明确写出来,别让它悬着。
再往下是模块级的 SDK。Project Structure 里 Modules 面板,每个模块可以单独指定 SDK。多模块项目里经常出现某个模块被误设成了别的版本,编译时提示“无效的目标发行版”,找半天找不到原因。这种情况我一般直接全局搜*.iml文件和.idea/modules目录,把版本号统一掉。
2.2 Maven 的本地仓库与配置
JavaWeb 项目基本都要用 Maven,所以本地仓库这件事绕不开。默认的本地仓库在用户目录下的.m2/repository,这个位置本身没问题,但如果你的系统盘空间紧张,或者习惯把开发环境放在非系统盘,就得改。
改的方式是编辑 Maven 安装目录下conf/settings.xml,找到<localRepository>节点,把注释打开写上自己的路径。这里有个坑:settings.xml有两份,一份在 Maven 安装目录的conf下(全局配置),一份在用户目录的.m2下(用户配置),IDEA 默认读的是用户配置。你在全局配置里改了仓库路径,IDEA 里没生效,就是因为这个。要么两份都改,要么在 IDEA 的 Maven 设置里手动把 User settings file 指过去。
仓库源也建议换一下。默认的中央仓库在国内下载速度很不稳定,经常下到一半卡住,然后留下一个.lastUpdated后缀的残留文件,之后无论怎么重试都失败,因为 Maven 认为这个依赖已经试过了。遇到这种情况,最干脆的做法是去本地仓库对应目录下,把所有.lastUpdated文件删掉,再重新拉一次。
# 在本地仓库根目录执行,清掉所有失败标记 find ~/.m2/repository -name "*.lastUpdated" -delete至于具体换哪个镜像地址,各家云厂商都提供,选一个响应快的填进去就行。填的位置在settings.xml的<mirrors>节点里,注意<mirrorOf>一般写central,别写成*,否则会把你私有仓库的请求也一起劫持掉,到时候自己发布的包拉不到,又是一轮排查。
2.3 容器版本的对应关系
服务端容器的版本与 Servlet 规范版本是绑死的,这一条必须记牢,因为它直接决定了你 import 的包名是javax.servlet还是jakarta.servlet。这两个包名不通用,写错了编译就过不去,而且改了包名之后还要对齐依赖版本,链条很长。
大致的对应关系是这样:Servlet 3.1 对应容器 8.0/8.5,Servlet 4.0 对应容器 9.0,Servlet 5.0 起包名从javax换成jakarta,对应容器 10.0 及以上。这个分界线是从 9 到 10 的那一步,也是目前最容易踩的一步——很多教程还在用 9,你本地装的是 10,照着抄就报“找不到符号”。
我个人的建议是:如果是跟着教程学,容器版本和教程保持一致,别自己升级;如果是新起项目,直接用新版本,从一开始就别碰旧包名。最怕的是学着学着中途换版本,前面写的代码全部要改 import。
另外还要确认容器的 JRE 指向。容器运行需要 JDK,在 IDEA 的运行配置里会有一个 JRE 下拉框,如果这一项是空的或者指向了一个不存在的路径,服务器启动时会直接报错退出,错误信息通常很含糊,只说“无法启动”,不说原因。
3. 新建普通 Java 项目:从空白到跑通第一个类
3.1 向导面板逐项拆解
从欢迎界面点新建项目,进入向导。左侧是语言列表,选 Java;右侧从上往下依次是名称、存储位置、构建系统、JDK、以及是否添加示例代码。这几项看着简单,其实每一项都影响后面的走向。
名称和存储位置:名称会同时作为目录名和模块名。我的习惯是名称用小写加短横线,跟包名解耦,因为目录名一旦用了中文或者空格,某些构建工具会直接给你上一课。存储位置尽量选一个短路径,别放在桌面或者深层目录里,Windows 上路径过长导致编译失败的情况真的存在。
构建系统这一项是新手最容易忽略的。可选项通常是 IntelliJ(内置构建)、Maven、Gradle,还有一栏是添加示例代码的勾选框。选“IntelliJ”意味着不生成任何构建描述文件,全靠 IDE 自己的编译机制,适合练语法、写单文件工具类;选 Maven 会生成 pom,适合后面要引第三方库、要打成可分发产物的场景。
JDK 这一项,如果你前面已经登记过 SDK,下拉里会直接列出来。没登记过就点右边的添加按钮现场指路径。
注意:勾选“添加示例代码”会帮你生成一个带
main方法的类,省事但容易让人误以为项目已经配好了。实际上它只证明“能编译能跑”,不证明目录标记、依赖管理这些关键项是对的。我建议新手第一次故意不勾,自己从空目录开始建包、建类,把这个过程走一遍,理解会深很多。
3.2 项目建好之后的目录长什么样
一个干净的 Java 项目落地后,磁盘上大概是这个结构:
demo-java/ ├── .idea/ # IDEA 的工作空间配置,不要提交到版本库 ├── src/ # 源码根目录,蓝色标记 │ └── com/example/demo/ │ └── Main.java └── out/ # 编译输出,橙色标记,自动生成 └── production/demo-java/src目录是蓝色的,这个颜色不是装饰,是 IDEA 告诉编译器“这个目录下的.java文件需要参与编译”。如果哪天你发现新建的类前面没有那个小图标,或者代码里全是红线提示“找不到符号”,先去 Project Structure 的 Modules 面板看看 Sources 标签页里,src是不是被标成了 Sources。有时候导入别人的项目,目录结构是对的,但标记丢了,表现就跟代码写错一模一样。
out目录是编译输出,里面放的是.class文件。这个目录不需要你手动维护,也不需要提交。它被标成橙色(Excluded),意思是“里面的内容不参与索引和搜索”,把它排除掉可以显著减少 IDE 的索引负担,尤其是编译产物多的时候。
.idea目录要单独说一下。这个目录里存的是你个人的工作空间状态,包括窗口布局、打开的标签页、运行配置。多人协作时它通常被加入忽略列表,因为每个人的配置不一样,提交上去只会制造冲突。但有一种情况例外:项目的 JDK 版本、编码设置这些“团队应该统一”的东西,有的团队会把它提交上去。这件事没有绝对对错,关键是团队内部要统一口径,别一半人提交一半人不提交。
3.3 建包、建类与运行配置
在src上右键新建包,包名按反域名习惯写,比如com.example.demo。一定不要图省事直接把类建在src根下,那样类就落进了默认包,默认包里的类不能被别的包 import,后面想重构都难。
包建好之后在里面新建 Java 类,填类名,回车。IDEA 会生成一个空类骨架。输入psvm回车生成main方法,输入sout回车生成输出语句,这两个是最高频的代码模板,值得一开始就记住。
package com.example.demo; public class Main { public static void main(String[] args) { System.out.println("hello"); } }写完之后点类名左边那个绿色三角就能跑。第一次运行时 IDEA 会自动创建一个运行配置,类型是 Application,里面记录了主类全名、使用哪个模块的 classpath、工作目录。这个配置可以在运行下拉框里点“编辑配置”查看和修改。
关于运行配置,有三项值得留意。Main class必须是带main方法的类全名,写错了会提示“找不到或无法加载主类”。Use classpath of module决定了运行时能加载哪些类,多模块项目里如果选错了模块,会出现“代码明明在,运行就是找不到”的情况。Working directory是相对路径的基准,读文件写文件时用的相对路径都以它为起点,默认是项目根目录,但你可以改,改完之后代码里那些相对路径的含义就跟着变了,这一点在读写配置文件时特别容易出问题。
还有一种情况:右键菜单里压根没有“运行”选项。这通常意味着 IDEA 没把这个文件识别成可编译的源码,回到上一节检查 Sources 标记,八成就解决了。
4. 新建 JavaWeb 项目:三条路线的取舍
4.1 路线一:用 Maven 骨架一键生成
这是最省事的一条路。新建项目时左侧选 Maven,勾上“从骨架创建”,在骨架列表里搜webapp,选中那个maven-archetype-webapp,填好 GroupId 和 ArtifactId,一路下一步。
生成出来的结构是这样:
web-demo/ ├── pom.xml └── src/ └── main/ └── webapp/ ├── index.jsp └── WEB-INF/ └── web.xml这个骨架的好处是pom.xml里已经把packaging写成了war,webapp目录和web.xml也给你备好了,符合 Web 项目的标准目录约定。缺点是它不生成src/main/java目录,你得自己建,建完之后还要去 Project Structure 里把它标记成 Sources Root。这一步漏掉,你写的 Servlet 类根本不会参与编译,部署上去当然找不到。
骨架生成时还有一个坑:它默认的web.xml版本可能很老,声明的 schema 是 2.3 或者 2.4 那一档。如果你的容器是新版本,用老 schema 启动会有一堆警告,某些特性也用不了。判断方法很简单,打开web.xml看version属性的值,如果是 2.x,手动改成 4.0 并把xmlns那一串换成对应版本的地址就行。另一种更省事的做法是干脆不要web.xml,改用注解方式注册 Servlet,这个在下一节展开。
4.2 路线二:手工建结构,掌控每一处细节
我个人的主力做法是这条。先建一个空的 Maven 项目,不勾骨架,然后把目录结构一层层手工建出来:
web-demo/ ├── pom.xml └── src/ ├── main/ │ ├── java/ # 标记为 Sources Root │ ├── resources/ # 标记为 Resources Root │ └── webapp/ # 标记为 Web 资源目录 │ └── WEB-INF/ └── test/ └── java/ # 标记为 Test Sources Root建完之后必须做三件事,缺一不可。第一,src/main/java右键标记为 Sources Root;第二,src/main/resources标记为 Resources Root;第三,去 Project Structure 的 Facets 面板添加一个 Web Facet,把它挂到当前模块上,然后在 Web Resource Directory 里把src/main/webapp指过去。
第三条是新手最容易漏的。前面说过,Facet 是 IDEA 层面的身份标签,没有它,webapp目录在 IDEA 眼里就是个普通的文件夹,打包的时候不会被当成 Web 根目录,部署下去自然空空如也。
这条路线的价值在于:你对每一处配置都心里有数。哪一步对应什么效果,出问题时该往哪查,全在掌控里。而且这套结构跟大多数构建工具的默认约定一致,换个 IDE 或者上流水线,都不需要额外适配。
4.3 路线三:Jakarta EE 模板,慎用但可以了解
新版本 IDEA 里有一个专门的企业级模板,选中之后会一次性把 Web 模块、依赖、目录、运行配置全给你配好,甚至连容器的运行配置都能生成。听起来很美,实际用起来有几个顾虑。
一是它默认的版本组合通常比较新,包名可能是jakarta.*,你本地的容器如果不是对应版本,跑不起来。二是它生成的配置项多,对新手来说是个黑盒,出了问题不知道从哪一层开始查。三是各种教程和现成代码大多还是旧包名体系,直接往上贴会冲突。
我的建议是:如果你本地环境本来就是新版本,而且打算从头学新的规范,用这个模板没问题;如果你是跟着某份材料学,或者要维护既有项目,老老实实走路线一或者路线二,把控制权握在自己手里。
| 路线 | 上手成本 | 目录完整度 | 掌控度 | 适用场景 |
|---|---|---|---|---|
| Maven 骨架 | 低 | 缺java目录 | 中 | 快速验证、跟着教程走 |
| 手工搭建 | 中 | 完整 | 高 | 正式项目、需要长期维护 |
| 企业级模板 | 低 | 完整 | 低 | 新版本环境从头起步 |
5. 让页面真正跑起来:JavaWeb 的关键配置
5.1 pom.xml 里的三处关键写法
pom.xml是 Maven 项目的总纲,Web 项目里必须把三件事写对:打包方式、依赖范围、编码与编译版本。
<packaging>war</packaging>这一行决定了产物形态。写成jar,打包出来就是一个普通归档,容器不认;写成war,才会按 Web 应用的目录约定组织,容器才能部署。骨架生成的项目一般已经是war,手工建的要自己加。
依赖这块,Servlet 相关的包必须设成provided:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>provided的含义是“编译时需要,运行时不打包”。为什么这么做?因为容器自己就带着这套 API。你如果按默认的compile范围把它打进WEB-INF/lib里,容器启动时 classpath 上就有两份同名的类,轻则警告,重则类加载冲突,报出一堆莫名其妙的LinkageError。这个坑极其常见,因为 IDEA 的自动导入补全默认就是compile,不手动改就等着踩。
编译版本和编码也要显式写死,别指望默认值:
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>source和target必须跟你项目 SDK 的语言级别一致。三处不一致的后果前面说过,症状是“编辑器不报错、编译报错”,最难查。编码这项不写,某些平台上默认编码不是 UTF-8,源码里的中文注释和字符串会变成乱码,而且是编译之后就坏掉,回过头看源码又是好的,非常迷惑。
还有一个可选但很有用的配置:
<build> <finalName>demo</finalName> </build>finalName决定了打出来的包名,而这个包名在很多容器配置里会直接影响访问路径。默认的包名是artifactId-version,带版本号和横线,访问路径就变成/web-demo-1.0-SNAPSHOT/hello这种又长又难看的地址。设成demo之后路径就清爽了。这件事看似小事,但在配访问路径和排查 404 的时候,能省掉不少来回。
5.2 部署描述符与注解方式的选择
老的写法是在web.xml里注册 Servlet:
<servlet> <servlet-name>hello</servlet-name> <servlet-class>com.example.demo.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>新的写法是直接在类上打注解:
package com.example.demo; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>hello servlet</h1>"); } }注解方式要求 Servlet 规范在 3.0 以上,现在基本都是了。它的好处是不用维护两份配置,改了路径直接改注解,不会出现“类改了名字但web.xml没同步”的低级错误。
但注解方式有一个前提容易被忽略:web.xml里的metadata-complete属性不能设成true。这个属性为true时,容器会跳过注解扫描,只认web.xml。有些老项目的模板里默认写的是true,你加了注解怎么都不生效,查半天查不出原因,就是卡在这。
我的习惯是:新项目直接不要web.xml,全用注解;老项目保持原样,别混着改。混用是最麻烦的,一部分 Servlet 在配置里,一部分在注解里,出问题时要在两个地方来回找。
5.3 容器运行配置的完整过程
这是整个流程里最需要细节的一段。打开运行配置面板,点加号,找到容器类型下的本地实例,新建一个配置。
第一块,服务器本体。在 Server 面板里指定容器安装目录、访问端口、以及运行时使用的 JRE。端口默认是 8080,如果被别的程序占了,启动时会报“地址已被占用”,改成 8081 之类的即可。JRE 那一项千万别留空,留空的表现是点启动之后一闪而过,日志里只有一行含糊的报错。
第二块,部署产物。切到 Deployment 面板,点加号添加工件(Artifact)。这里通常会看到两个候选:一个是war exploded,一个是war。前者是展开目录,后者是压缩包。开发阶段一定选前者,因为它支持热更新,改了类或者页面之后不用重启;后者每次改动都得重新打包部署,循环太慢。
添加工件之后,下面有一个 Application context 输入框,这个值决定了访问路径的前缀。默认可能是/或者/项目名_war_exploded,前者意味着根路径直接访问,后者意味着要带上那一长串。我一般手动改成/demo这种短名字,然后在浏览器里访问http://localhost:8080/demo/hello。这里填的值和finalName没关系,两者是独立的,这也是为什么很多人误以为改了finalName就能改访问路径,结果没生效。
第三块,更新策略。在 Server 面板里有个“On frame deactivation”或者“On update action”的下拉框,可选“更新类和资源”“重启服务器”“不操作”。开发阶段选“更新类和资源”最舒服,切回浏览器时自动把改动同步过去。但要注意,这个策略只对war exploded有效,对压缩包无效。而且它同步的是资源和编译后的类,不包含依赖变动和配置变动,改了pom.xml还是得重启。
配好之后点运行,控制台会打印一串启动日志。看到“服务器在 X 毫秒内启动”之类的字样就说明起来了。这时候别急着写业务,先访问一下根路径,如果能看到index.jsp的默认内容,说明整条链路是通的:编译、打包、部署、访问,四个环节都没问题。这一步验证过了,再往上加代码,出问题就只可能是代码本身的问题,排查范围缩小一大截。
5.4 编译输出路径与工件的对应关系
这一段讲的是“为什么我改了代码但部署上去的还是旧版本”这个问题。
IDEA 的编译输出路径可以在 Project Structure 的 Modules 面板里看到,每个模块有一个输出目录。较新的版本把编译输出统一放到了项目的target或out目录下,具体位置取决于构建方式。用 Maven 构建的,输出在target/classes;用内置构建的,输出在out/production/模块名。
工件(Artifact)的定义里,会明确写出“从哪个模块的输出目录取类文件”。这两者的关系是一条链:源码 → 编译输出目录 → 工件定义 → 容器部署目录。链上任何一环指错,你改的代码就进不了部署目录。
排查方法很直接:改一行代码,看编译输出目录里对应的.class文件修改时间有没有变。没变说明没编译,检查自动编译是不是关了;变了但部署目录里没变,说明工件定义指错了地方,去 Artifact 配置里核对输出路径。这个思路一旦建立起来,这类问题基本几分钟就能定位。
提示:自动编译这个开关在设置里的编译相关面板中,默认状态在不同版本里不一样。如果发现改代码之后必须手动点一下“构建”才生效,先去把这个开关打开。
6. 常见问题与排查实录
6.1 三个高频错误的定位顺序
404,页面打不开。这个错误的排查有固定顺序,别乱试。第一步,看容器日志里应用有没有成功部署,如果有“部署成功”之类的记录,说明打包没问题,是路径问题。第二步,核对访问地址的 context path,就是部署配置里那个输入框的值,多一个斜杠少一个斜杠都算错。第三步,确认webapp目录下有没有默认首页,或者你访问的路径在代码里有没有对应的映射。第四步,回到 Facet 配置看 Web 资源目录指对了没有。这四步走完,百分之九十五的 404 都能定位。
ClassNotFound,类找不到。先分清是编译期还是运行期。编译期报错,检查依赖有没有正确引入、版本有没有冲突、源码目录标记对不对。运行期报错,重点看部署产物的WEB-INF/lib目录里到底有没有那个 jar。缺的话,可能是依赖范围写成了provided而它并不是容器提供的包,也可能是打包插件配置里排除了某些依赖。
端口占用,启动失败。报错信息里会明确写出被占用的端口号。改端口是最省事的做法。如果一定要用原端口,就去查是哪个进程占着。Windows 上用netstat -ano | findstr 8080拿到进程号再查,类 Unix 系统上用lsof -i :8080。很多时候占用端口的不是什么可疑程序,就是你上一次没关干净的容器实例。
6.2 中文乱码的整条链路
乱码这件事,出问题的环节比想象的多,得按链路一段段查:源码文件编码、编译器读取编码、输出资源编码、容器解析请求编码、浏览器渲染编码。五个环节里有任意一个不是 UTF-8,就可能出现乱码。
具体做法:设置里把全局编码、项目编码、属性文件编码全部设成 UTF-8;pom.xml里写死project.build.sourceEncoding;JSP 页面顶部声明pageEncoding="UTF-8";Servlet 里输出前设resp.setContentType("text/html;charset=UTF-8");容器配置文件里的连接器节点加上URIEncoding="UTF-8"。
还有一个很容易漏的点:控制台输出乱码。这个跟应用本身没关系,是运行配置里的虚拟机参数没指定文件编码导致的。在运行配置的虚拟机参数里加上编码设置,控制台的中文就正常了。这个不影响功能,但影响你调试时看日志的心情,值得顺手解决。
6.3 常见问题速查表
| 现象 | 最可能的原因 | 快速验证方式 |
|---|---|---|
| 页面 404 | 访问路径没带 context path | 看部署配置里的上下文值,手工补全 |
| 页面 404 | 工件没添加到部署列表 | 打开部署面板看列表是否为空 |
| 编译报“找不到符号” | 源码目录未标记 | 看目录颜色是否为蓝色 |
| 运行报类找不到 | 依赖范围写错 | 检查部署目录下WEB-INF/lib |
| 改动不生效 | 更新策略为“不操作” | 改策略为更新类和资源 |
| 中文乱码 | 编码链路某处不是 UTF-8 | 逐环节核对编码设置 |
| 启动一闪而过 | 运行配置的 JRE 为空 | 打开配置补上 JRE |
| 依赖下载失败 | 本地仓库残留失败标记 | 删掉.lastUpdated文件重试 |
| 注解不生效 | 配置里metadata-complete为 true | 改成 false 或删掉该属性 |
| 包名报错识别不了 | 规范版本与容器不匹配 | 核对包名是javax还是jakarta |
7. 我踩过之后固化下来的几条习惯
第一条,一个业务系统一个 Project,但内部按职责拆模块。早期我图省事所有东西塞一个模块里,结果 Web 层依赖了数据层的实现细节,测试代码跟业务代码混在一起,改一处牵连一片。拆开之后,模块之间的依赖方向变得可见,谁依赖谁一眼就能看出来,编译期的约束帮你挡住了一部分低级错误。
第二条,版本号集中声明,别散落在各处。pom.xml里的依赖版本统一放进properties里,改的时候只改一处。散着写的直接后果是某天升级一个包,忘了另一个,两边版本冲突,运行时报出谁也看不懂的错误。
第三条,每次新建项目之后,先走一遍最小验证。不要一上来就写业务,先跑通一个最简单的输出,确认编译、部署、访问三个环节都通,再往上叠。这个习惯帮我省掉了无数次“到底是代码错了还是配置错了”的纠结。
第四条,把常用配置固化成模板。IDEA 支持把当前的工程配置存成模板复用,把 JDK 版本、编码、常用依赖、运行配置这些每次都一样的部分固化下来,下次新建直接套。这件事的价值随项目数量线性增长,做过三次以上同类项目就该动手了。
最后再分享一个我自己用得最多的小技巧:任何“明明代码没问题但跑不起来”的情况,先去看编译输出目录里的.class文件时间戳。这个动作只需要两秒钟,却能立刻把问题二分——时间戳没更新,问题在构建链路;时间戳更新了,问题在部署或者运行配置。我靠这一招把排查时间从半小时压缩到几分钟,屡试不爽。