每次带刚入行的朋友,最先被问住的往往不是什么疑难杂症,而是“IDEA新建项目到底该点哪个”。这个问题看着不起眼,真能把人卡半天:欢迎界面上一排入口,进到向导里又让你选Java、Maven、Gradle、Jakarta EE、Spring Boot,选完发现目录是空的,写个main方法还满屏报红。JavaWeb项目更麻烦,不少人新建完发现没有webapp目录、没有web.xml,Tomcat挂上去跑出来就是个404。这篇就把IDEA新建项目这件事从头到尾捋一遍,Java项目和JavaWeb项目分开说,每个参数填什么、目录该长什么样、为什么要这么选、报错了往哪儿查,都按我平时带人的顺序来。不论你是刚装好IDEA第一次建工程,还是从别的编辑器转过来对Module概念一直没转过弯,照着走一遍都能跑出一个能启动、能部署的工程。
1. 动手之前先把IDEA的项目模型搞清楚
1.1 Project和Module不是一个东西,这个坎几乎人人要过
从Eclipse转过来的朋友,最大的认知落差就在这儿。Eclipse里workspace是工作空间,project是工程,一个workspace下可以铺一堆平级的project,互相之间靠Build Path串起来。IDEA是另一套逻辑:Project是顶层容器,它本身不编译任何东西,真正干活的是Module。一个Project下可以有N个Module,每个Module有自己的SDK、自己的语言级别、自己的依赖、自己的输出目录。你新建一个普通Java项目,其实IDEA在背后帮你做了两件事——建了一个Project,又在里面建了一个同名Module,只是界面上没强调这个区别。
这个设定带来的第一个坑是:很多人建JavaWeb项目时,直接把整个Project当成Web模块来配,Web Facet挂在根上,源码目录也放在根上。等过两天想加一个公共工具模块,或者想拆一个后台管理模块出来,就完全不知道怎么下手了,因为根上那层已经被Web Facet和Artifact绑死了。我现在带人的做法是:从第一个项目开始就养成“Project当壳子,Module当实体”的习惯,哪怕只有一个模块,也心里清楚这层结构。后面加了第二个模块,你只要File → New → Module,勾选继承父工程的SDK,路径放在Project下面,它就是一个平级的新编译单元,依赖各管各的,互不干扰。
第二个坑是路径规划。新建向导里的Location填的是Project根目录,Module如果默认建在根下,它的路径就是根目录本身,源码在src里。这种情况在单模块工程里没问题,但一旦变成多模块,你就得把每个Module放到子目录里,比如demo-web、demo-common。我一般建议新手在新建时就手动把Location写成D:/work/demo,Module名和Project名保持一致,简单直接;等真要拆模块了,新建Module时路径填D:/work/demo/demo-web,父工程当聚合器,逻辑上不容易乱。
1.2 SDK、语言级别、编译输出:三个容易被混为一谈的版本号
这三个东西是新建项目时最该盯紧的,但界面上它们分散在三个不同的地方,特别容易配串。
SDK指的是JDK的安装路径,它决定了你能用什么,比如JDK 17里有record、有sealed,JDK 8里就没有。Language Level是语法特性上限,它只影响IDEA的代码检查和补全,不影响实际编译产物——你把Language Level设成8,但SDK是17,用var照样报红提示不支持。Target bytecode version才是最终class文件的版本号,这个设错了,运行时会直接抛UnsupportedClassVersionError。
我见过的典型事故是这样的:项目SDK选的JDK 17,Language Level忘了改,默认跟SDK走,结果公司服务器上跑的是JDK 8,一部署就炸。正确做法是三个值对齐:Project Structure → Project里把SDK和Language Level设成同一个版本,然后Project Structure → Modules里检查每个模块的Language Level,最后Settings → Build, Execution, Deployment → Compiler → Java Compiler里把Target bytecode version也设成同一个数字。如果用了Maven,还要在pom里写maven.compiler.source和maven.compiler.target,因为Maven编译走的是自己的插件,不认IDEA的界面设置——这一点太多人栽过跟头,界面上改得好好的,命令行mvn package打出来的包还是低版本。
提示:判断版本有没有配串,最快的办法是编译完去
target/classes或者out/production目录下看class文件,用十六进制工具看开头几个字节,CAFEBABE后面那两位就是版本号,52对应JDK 8,61对应JDK 17。懒得看字节就去终端跑javap -verbose 类名 | findstr major。
1.3 三种新建入口分别适合什么场景
IDEA的版本迭代让新建入口变过好几次。2020版之前是New Project左边一竖排:Java、Maven、Gradle、Spring、Java Enterprise、Kotlin之类。2022.3之后改成了先填Name和Location,再在Language里选Java,然后Build system里选IntelliJ、Maven还是Gradle。JavaWeb这边,老版叫Java Enterprise,新版改名叫Jakarta EE。界面变了但底层逻辑没变,选哪条路取决于你要做什么:
| 入口 | 适用场景 | 优点 | 后续代价 |
|---|---|---|---|
| Java(Build system选IntelliJ) | 练语法、刷算法、写小工具、临时验证一段代码 | 秒建,无外部依赖,目录最干净 | 要引第三方jar只能手动Add as Library,不规范 |
| Maven | 绝大多数正式项目,包括JavaWeb | 依赖声明式管理,目录结构约定统一,别人拉下来就能跑 | 要等依赖下载,网络不好时首建会卡 |
| Gradle | 安卓、需要灵活构建脚本的项目 | 脚本灵活,增量构建快 | 学习成本高,国内镜像配置略麻烦 |
| Jakarta EE(Java Enterprise) | 传统Servlet/JSP项目,想直接用IDE模板 | 自动带webapp目录和web.xml,Facet自动挂好 | 社区版没有这个入口,且模板版本偏老 |
新手我一般直接推Maven。理由很实在:JavaWeb项目离了依赖管理寸步难行,你总得引Servlet API,总得引数据库驱动,总得引JSON库,手动扔jar包早晚要乱。而Maven的目录约定(src/main/java、src/main/resources、src/main/webapp)恰好也是JavaWeb的标准结构,一开始就按这个来,后面换机器、换同事、上CI都省事。
顺带说一句版本选择的事。社区版(Community Edition)没有Java EE/Jakarta EE的完整支持,也没有内置的Tomcat集成,做Web开发会比较别扭;旗舰版(Ultimate)才有完整的Web Facet、应用服务器集成和Spring支持。如果你手上只有社区版,也不是不能做,用Maven建webapp项目,再装个第三方的Tomcat运行插件,一样能跑起来,只是配置过程会多几步手动操作。这一点在建项目之前心里要有数,别建到一半才发现功能是灰的。
2. 新建普通Java项目:从空白工程到点下绿色的运行键
2.1 创建向导里每个输入框到底影响什么
走一遍最基础的路径。File → New → Project,Language选Java,Name填demo-java,Location选一个纯英文、无空格的路径,比如D:/work/demo-java。JDK那一栏点开下拉,如果没有你想要的版本,点Add JDK手动指到安装目录。Build system这里选IntelliJ,下面的Add sample code勾不勾都行,勾了会给你生成一个带main方法的Main类,新手拿来验证环境挺方便。
几个输入框的实际含义值得说清楚。Name不只是显示名,它默认决定了pom.xml里的artifactId(如果你选了Maven),也决定了编译输出的目录名。Location是工程根目录,IDEA会在里面生成.idea隐藏目录,放工程级配置,包括模块列表、SDK绑定、代码风格、运行配置。这个.idea目录该不该进版本库,团队里吵了很多年,我的做法是:.idea整体忽略,只保留codeStyles和inspectionProfiles两个子目录,因为这两个直接影响代码格式和检查规则,统一了能省掉很多无谓的diff。.iml文件也是一样,模块级配置,随人走,不进库。
JDK那一栏下面还有个容易被忽略的下拉,就是Language level的默认值。新建时就把这个值确认一遍,别等写完代码报红了再回头找。如果公司统一用JDK 8,而你自己电脑上装的是17,最简单的方式是在向导里直接选8——前提是你本机确实装了8;没装的话选17,但要在后面把Language Level和编译版本手动压到8,否则写出来的代码在服务器上跑不了。
2.2 项目建好之后的目录识别与源根设置
工程建完,左边Project面板里应该是一个src目录加一个Main.java。这里有个隐藏机制:IDEA靠目录的颜色标记来判断哪些是源码、哪些是测试、哪些是资源。src显示成蓝色,说明被识别为Sources Root,里面的.java文件会被编译。如果哪天你从别处拷了个目录进来,发现文件图标是灰的、代码里全是红的、import什么都找不到,八成就是没标源根。
处理办法是右键那个目录 → Mark Directory as → Sources Root。测试代码标Test Sources Root(绿色),配置文件标Resources Root。这个操作看着土,但极其常用,尤其是导入别人的非Maven工程时,基本每次都要手动标一遍。Maven工程不用管,src/main/java和src/test/java是约定好的,IDEA打开pom后会自动识别。
再说编译输出目录。Project Structure → Project → Compiler output,默认是工程根下的out目录,里面按模块分production和test两个子目录。这个路径一般不用改,但如果你的工程放在网络盘或者同步盘上,建议改到本地磁盘,不然编译过程会被同步软件拖得很慢。另外输出目录千万别手滑设成src,否则编译出来的class文件和源码混在一起,清理起来非常难受。
2.3 运行配置的生成逻辑与手动调整
写完main方法,代码左边行号旁边会出现一个绿色三角,点它选Run,IDEA会自动创建一个Application类型的运行配置,名字默认取类名。这个自动创建的配置存在.idea/runConfigurations下面(老版本是workspace.xml里),跟着工程走。
自动生成够用,但真正做项目还是得会手动配。Run → Edit Configurations → 左上角加号 → Application,几个关键项:Main class填全限定类名,Program arguments是传给main(String[] args)的参数,多个用空格分隔,含空格的参数要加引号;VM options是给JVM的,比如-Xmx512m -Dfile.encoding=UTF-8;Working directory是进程的工作目录,这个值影响相对路径的解析,很多人读配置文件读不到,就是因为工作目录默认是模块根,而文件实际在别处。
还有一个坑值得单独提:Use classpath of module。一个工程里有多个模块时,运行配置必须指定用哪个模块的classpath,选错了就会出现“类明明写了却报ClassNotFoundException”。尤其是从别人那儿拷了运行配置过来,模块名对不上,IDEA会显示成红色,得手动重选一次。
2.4 多模块工程怎么加第二个Module
单模块跑通之后,加模块的操作是File → New → Module。向导里会让你选Module SDK,默认继承Project的,也可以单独指定。路径这里要特别注意:如果你希望它作为子目录存在,Location要写成父工程路径/模块名,否则IDEA会把它建到工程外面去,变成一个游离的目录,虽然也能用,但管理起来别扭。
加完模块后,父工程的pom.xml(如果是Maven)里会自动多出一个<module>节点。如果不是Maven工程,那就是纯IDEA的模块关系,靠.idea/modules.xml维护。这时候模块之间的依赖需要手动加:Project Structure → Modules → 选中模块 → Dependencies → 加号 → Module Dependency,勾上你要依赖的模块。Maven工程则是在pom里写<dependency>,groupId和artifactId对应另一个模块的坐标,配合<version>和<scope>使用。两种方式效果一样,但Maven的方式可移植,换台机器不用重新配。
2.5 编码、编译版本、输出路径的统一配置
这三样东西建议在新建项目后立刻统一设一遍,别等出问题。
编码在Settings → Editor → File Encodings,三个下拉全部选UTF-8:Global Encoding、Project Encoding、Default encoding for properties files。最后那个properties编码特别关键,Java的Properties.load()默认按ISO-8859-1读,如果你的配置文件里写了中文,不改这个选项读出来就是乱码。新版IDEA在properties文件里会提示你转成\uXXXX转义,或者在Settings → Editor → File Encodings里勾上Transparent native-to-ascii conversion,本质上也是转义。
编译版本在Settings → Build, Execution, Deployment → Compiler → Java Compiler,把Project bytecode version和目标模块的Target bytecode version设成一致。如果用Maven,还得在pom里配:
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>或者更省事,直接锁插件版本:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>8</source> <target>8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>输出路径在Project Structure → Project的Compiler output统一设一次,各模块默认继承。如果某个模块要特殊处理,在Modules → Paths里单独改,但一般没必要,统一反而好排查。
3. 新建JavaWeb项目:两条主流路线和它们的坑
3.1 路线一:用Maven的webapp骨架快速起盘
这是目前最主流也最省事的做法。File → New → Project,Language选Java,Build system选Maven,勾上Create from archetype,点Add Archetype手动加一条,或者直接在列表里找org.apache.maven.archetypes:maven-archetype-webapp。填好groupId(比如com.example)、artifactId(比如demo-web)、version,一路Next。
建完的目录结构是这样的:src/main/webapp下有一个index.jsp和一个WEB-INF/web.xml,src/main/java和src/main/resources这两个目录不会自动生成,得自己右键New → Directory建出来,建完IDEA通常会自动识别成源根和资源根,没识别就手动Mark Directory as。
这个骨架最大的价值在于它替你定好了packaging是war,定好了webapp目录位置,定好了war插件的默认行为。剩下的就是补依赖、补代码。但骨架也有明显的坑,下面单独说。
3.2 骨架自动生成的web.xml版本问题
maven-archetype-webapp这个骨架年头太久了,生成出来的web.xml默认是2.3版本,头部是这样的:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> </web-app>2.3版本意味着什么?意味着不支持注解。你在Servlet上写@WebServlet("/hello"),容器压根不认,必须去web.xml里手写<servlet>和<servlet-mapping>。很多人第一次建Web项目就卡在这,代码写得没问题,访问就是404,翻半天找不到原因。
处理办法是把头部换成对应版本。如果部署在Tomcat 9上,用Servlet 4.0:
<?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_4_0.xsd" version="4.0"> </web-app>如果部署在Tomcat 10及以上,命名空间要换成jakarta:
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0"> </web-app>这里必须提醒一句:Tomcat 10是个分水岭。从10开始,Servlet API的包名从javax.servlet整体迁到了jakarta.servlet,你代码里的import、pom里的依赖坐标、web.xml的命名空间,三处必须同时改,只改一处就是编译不过或者运行时报ClassNotFoundException。这个坑每年都要埋一批人,建项目之前先确认服务器版本,能省掉一整天的排查。
3.3 路线二:普通Java模块手动挂Web Facet
有些场景不适合用Maven骨架,比如公司老项目就要求特定的目录布局,或者你只是想在现有工程里加一个Web模块。这时候走手动路线:先File → New → Module建一个普通Java模块,然后在模块上右键 → Add Framework Support → 勾选Web Application,版本选4.0,如果想让IDE生成web.xml就勾上Create web.xml。
生成完,模块下会多出一个web目录(不是webapp,这是IDEA自己的默认命名),里面是WEB-INF/web.xml。同时在Project Structure → Facets里会多一个Web Facet,里面配置了Web Resource Directory和部署描述符的位置。这个Facet是IDEA用来理解“这个模块是个Web应用”的关键,Artifact就是根据Facet生成的。
手动路线的好处是目录位置完全可控,你可以把Web Resource Directory指到src/main/webapp,也可以指到任意路径。坏处是所有东西都得自己配,包括依赖、包括打包方式,一旦哪个环节漏了,部署时就可能缺文件。我现在除非维护老项目,一般不走这条路。
3.4 Tomcat服务器接入与Artifact部署
项目建好、代码写完,下一步是让它跑起来。Run → Edit Configurations → 加号 → Tomcat Server → Local(新版可能显示成Application Server → Tomcat Server)。第一次需要点Configure指定Tomcat的安装目录,IDEA会自动识别版本。
关键的配置有三块。Server面板里是端口,默认8080,如果本机已经被别的服务占了,改成8081之类,点一下旁边的检查能看出端口是否被占用;还有JRE的选项,要和项目用的JDK匹配。Deployment面板是最容易出错的地方,点加号 → Artifact,选demo-web:war exploded,下面的Application context填你想要的路由前缀,比如/demo,那么访问地址就是http://localhost:8080/demo/hello。On 'Update' action建议设成Update classes and resources,这样改完代码按Ctrl+F11就能热更,不用每次重启。
3.5 war与war exploded该怎么选
Deployment里加Artifact时会看到两个选项,很多人随手选一个,出问题了也不知道区别在哪。简单说,war是打成一个完整的压缩包再扔给Tomcat解压,war exploded是直接把编译输出目录映射成Web应用根目录,不打压缩包。
| 对比项 | war | war exploded |
|---|---|---|
| 开发期热更新 | 不支持,改代码要重新打包部署 | 支持,改完直接生效 |
| 静态资源修改 | 需要重新部署 | 立即生效 |
| 构建耗时 | 多一步打包,稍慢 | 几乎无耗时 |
| 适用阶段 | 交付、验证最终产物 | 日常开发调试 |
开发阶段一律用war exploded,这条几乎没有争议。真正要验证打包结果的时候,再单独跑一次mvn clean package,去target目录下拿war包,手动扔进Tomcat的webapps里跑,确认没问题再交付。
3.6 Servlet API依赖的scope为什么必须是provided
pom里加Servlet依赖时,scope写错是另一个高频事故:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency>provided的意思是“编译期需要,运行期由容器提供”。为什么不写compile?因为Tomcat自己的lib目录下已经有这两个jar了,你的war包里再塞一份,就会形成两个同名类的加载冲突,典型表现是启动时报ClassCastException,说某个类是javax.servlet.http.HttpServlet但不能转成javax.servlet.http.HttpServlet——同一个类名,两个不同的类加载器加载,互不认账。这个报错看着像灵异事件,本质就是依赖重复。
同理,jsp-api、jstl在处理时也要分清哪些是容器自带、哪些需要打进包里。JSTL的实现jar要打进去,API通常容器也带,具体看Tomcat版本和目录。
4. 目录规范与pom配置:决定后面好不好维护
4.1 Maven Web工程的标准目录长什么样
一个规规矩矩的Maven Web工程,目录应该是这样:
demo-web ├── pom.xml └── src ├── main │ ├── java <- 源码,包里放 Servlet、Service │ ├── resources <- 配置文件,jdbc.properties、logback.xml │ └── webapp <- Web 根目录 │ ├── index.jsp │ ├── static <- css、js、img,自己建 │ └── WEB-INF │ ├── web.xml │ └── views <- JSP 放这里,避免被直接访问 └── test └── java <- 单元测试WEB-INF目录下的东西浏览器无法直接访问,这是Servlet规范定的,所以把JSP放WEB-INF/views下面,只能通过Controller转发进去,安全性更好。static目录放静态资源,从webapp根开始算路径,引用时写/static/css/app.css就行,注意别写成相对路径,不然换个路由层级就全断了。
资源目录src/main/resources里的文件,经过Maven打包会进到WEB-INF/classes下,读的时候用getClass().getResourceAsStream("/jdbc.properties"),前面那个斜杠表示从classpath根开始找,漏了斜杠就是相对当前类包路径找,这是另一个经典小坑。
4.2 pom.xml里几个必须写对的节点
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-web</artifactId> <version>1.0.0</version> <packaging>war</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <build> <finalName>demo-web</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>packaging必须是war,这是Web工程的身份证,写错成jar的话打包出来没有webapp目录,部署上去必然404。finalName决定打出来的包名,也影响部署路径,建议固定成模块名,别用默认的artifactId-version形式,否则访问路径里会多个版本号,很别扭。failOnMissingWebXml设成false是因为Servlet 3.0以后注解完全可以取代web.xml,如果你的项目不用web.xml,这个开关关掉才不会打包失败。
4.3 中文乱码的三个源头与对应处理
Web项目里的乱码基本集中在三个地方,各自处理方式不同。
第一处是请求参数。GET请求的中文取决于Tomcat的server.xml里Connector的URIEncoding,Tomcat 8之后默认就是UTF-8,一般不用动;POST请求取决于request.setCharacterEncoding("UTF-8"),得在读取参数之前调用,或者干脆在web.xml里配一个CharacterEncodingFilter统一处理。第二处是响应内容。response.setContentType("text/html;charset=UTF-8")这一句必须在拿到Writer之前写,写晚了不生效。第三处是控制台日志。Tomcat自己的日志乱码,去Tomcat安装目录的conf/logging.properties,把里面的java.util.logging.ConsoleHandler.encoding改成UTF-8;如果只是IDEA控制台乱码,去Help → Edit Custom VM Options,加一行-Dfile.encoding=UTF-8,重启IDE。
三个位置看起来分散,但排查有固定顺序:先看是参数错了还是响应错了,参数错就是请求侧编码,响应错就是ContentType,都不错那就只是控制台显示问题,不影响实际数据,改日志配置就行。
5. 新建项目最容易踩的坑与排查实录
5.1 问题速查表
把这些年被问到最多的问题整理成表,遇到现象先在这里对一下,能省不少时间。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 新建完目录是空的,没有src | 向导里勾了不生成模板,或者目录被标错 | 手动建src并Mark Directory as Sources Root |
| 代码里所有类都报红,提示找不到JDK | Project SDK未指定或指向失效路径 | Project Structure → Project 重新选SDK |
| 运行按钮是灰的 | 没有main方法,或者当前文件不在源根下 | 检查类结构和源根标记 |
| 访问路径返回404 | Artifact没配,或者Application context写错 | Deployment里确认已添加artifact,核对访问URL前缀 |
启动报ClassNotFoundException: javax.servlet.http.HttpServlet | Servlet依赖scope写成了compile,和容器jar冲突;或者Tomcat版本与jakarta/javax不匹配 | 检查pom的scope和服务器大版本 |
@WebServlet不生效 | web.xml还是2.3版本 | 升级web.xml的schema和version |
| 页面中文乱码 | 响应未设charset,或JSP页面pageEncoding没写 | 加ContentType设置和<%@ page pageEncoding="UTF-8" %> |
编译报UnsupportedClassVersionError | 编译版本高于运行环境JDK | 对齐SDK、Language Level、target bytecode |
| 改了代码重启还是不生效 | 用的是war而不是war exploded,或者没触发update | Deployment换成war exploded,On Update设为Update classes |
| Maven依赖一直转圈下不下来 | 中央仓库网络慢 | 配国内镜像到settings.xml的mirrors节点 |
5.2 三个典型案例的排查过程
案例一:新建的Web项目访问全是404。有个朋友跟我说他新建完项目,Tomcat启动日志一切正常,就是访问什么都是404。我先让他看Deployment面板,发现artifact那一栏是空的——他新建完项目直接点了Run,IDEA让他配Tomcat,他配完Server就点了OK,没加artifact,等于Tomcat启动了一个空壳。加上war exploded之后,又出现访问根路径404,检查Application context填的是/demo-web,但他访问的是http://localhost:8080/,路径对不上。改成访问/demo-web/就正常了。这个链条说明一件事:Tomcat启动成功和你的应用部署成功是两回事,日志里出现Deployed application才算真的部署上去。
案例二:Servlet写了注解,就是不生效。代码是这样的:
@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=UTF-8"); try (PrintWriter out = resp.getWriter()) { out.println("<h1>hello, web</h1>"); } } }代码没问题,pom也没问题,最后发现web.xml是骨架默认生成的2.3版本,注解扫描直接不启动。把web.xml换成4.0的schema,重启后就好了。这个坑的隐蔽之处在于:项目完全能跑,只是注解被无声忽略,日志里一点提示都没有。
案例三:本地一切正常,打成war包部署到服务器就报类转换异常。报错信息大意是javax.servlet.http.HttpServlet cannot be cast to javax.servlet.http.HttpServlet。这个“同一个类不能转成自己”的报错,99%是类加载器冲突。查pom,果然Servlet API写的是默认scope(也就是compile),打war时被塞进了WEB-INF/lib,服务器上容器自身又有一份,两个类加载器各加载一份,互不认识。改成provided重新打包,问题消失。这类问题本地跑war exploded时不会暴露,因为IDEA部署时不会把provided的jar放进lib,只有真打war包才现原形,所以交付前一定要用真实war包在独立Tomcat上验证一次。
5.3 几条带人时长出来的经验
第一条,新建项目的第一件事不是写代码,是把SDK、语言级别、编译版本、编码这四项固定下来。这四项配错,后面所有的红都是它引起的,而新手往往从代码层面去找原因,越找越迷茫。我一般会让新人建完项目后主动去三个设置页各看一眼,形成肌肉记忆。
第二条,Maven工程不要依赖IDEA界面的设置,一切以pom为准。IDEA里有大量“看起来改了其实没改”的地方,比如Project Structure里改的语言级别,如果pom里没同步,命令行构建的时候还是老样子,CI上就会炸。我现在判断一个工程配置对不对,标准流程是关掉IDEA,命令行跑一遍mvn clean package,能过才算数。
第三条,Servlet容器版本和jakarta命名空间要一开始就确定。这个决定影响依赖坐标、import包名、web.xml命名空间,中途换等于重写。项目启动前先问清楚服务器是Tomcat 9还是10,是8.5还是9,把版本号记在文档里,比事后排查省太多事。
6. 一套从新建到跑通的自查清单
每次建完Web项目,我都会照着下面这套顺序过一遍,基本能在五分钟内确认工程是健康的。
- SDK与版本:Project Structure → Project,SDK、Language Level 是否一致;Modules里每个模块的Language Level是否也跟着;Compiler里Target bytecode是否同步;pom里
maven.compiler.source/target是否写死。 - 目录结构:
src/main/java是蓝色,src/main/resources是资源根,src/main/webapp存在且是Web Resource Directory,WEB-INF/web.xml在正确位置。 - 依赖:pom里Servlet和JSP的scope是
provided,数据库驱动、连接池、JSON库这些是compile。改完pom后一定要点一下Maven面板的Reload,别只保存不刷新。 - Facet与Artifact:Project Structure → Facets里有Web Facet,Artifacts里有
xxx:war exploded,两者指向的目录一致。 - 运行配置:Deployment里已添加artifact,Application context确认无误,On Update选的是Update classes and resources,端口没有被占用。
- 一次真实验证:启动Tomcat,看日志里有没有
Deployment of web application archive ... has finished;浏览器访问一个静态页和一个Servlet,确认中文不乱码;最后mvn clean package打一次war,扔到独立的Tomcat上再访问一遍。
这套流程走下来,项目基本不会在部署环节出岔子。我在实际带人的过程中发现,新手出问题的地方高度集中在前三步,也就是版本、目录、依赖scope,而这三样恰好都是新建项目时一次配好、后面几个月都不用再碰的东西。所以花十分钟在建项目的时候把这些确认清楚,比事后花两小时排查划算得多。最后一个建议:把你配好的pom.xml和web.xml存成模板,下次新建同类项目直接复制这两个文件,能省掉绝大部分重复劳动和重复踩坑。