我第一次用IDEA 2019搭Java Web项目时,犯了一个特别基础的错误:直接建了一个普通Java工程,然后手动把Tomcat自带的servlet-api.jar复制进项目里。折腾了一天,项目确实跑通了,但换目录、换电脑、换版本的时候问题全冒出来,依赖乱成一团。后来老老实实改用Maven建Web工程,才发现“看着多绕了一步”的做法,反而能让你把第一个Servlet跑得明明白白。
这篇文章要解决的问题很聚焦:在IDEA 2019环境下,怎么用Maven从零搭建一个Web工程,手写第一个Servlet,再用Tomcat把它跑起来供浏览器访问。适合刚接触Java Web、还没把Maven和Servlet这两个概念串起来的同学,也适合照着视频敲完却不知道“每一步到底在做什么”的人。我会把创建项目、补全目录、写Servlet、配Tomcat以及最常见的404和依赖报错,全部按实际操作顺序走一遍。
1. 开工前先说清楚:为什么绕道Maven反而最快
1.1 两种常见的起步方式,差在哪里
很多新手在IDEA里新建项目时,都会在“普通Java工程”和“Maven工程”之间犹豫。我最早用的是普通Java工程,做法是:File → New → Java,然后右键项目 → Open Module Settings,把Tomcat安装目录下的servlet-api.jar加到Libraries里。这种做法的好处是临时能跑,坏处也很明显:
- jar包是手工引进去的,项目一旦换到其他电脑,依赖信息不会自动恢复。
- 你根本不知道自己的项目依赖了哪些版本,也没法管理。
- 打包成war部署到Tomcat时,还得手动处理一堆琐事。
Maven的作用,简单说就是三件事:告诉你依赖从哪来、项目打成什么格式、目录结构按什么规则摆放。它像工地上的物料清单和施工规范——哪些材料要买、放在哪个房间、最后建成的结构长什么样,全部有标准。你用pom.xml描述需求,Maven去仓库拉取,再帮你编译、打包。刚开始会觉得多了一步,但这一步省掉的是后面无数次手工折腾。
1.2 Servlet到底是个什么角色
Servlet不是Spring MVC那种大型框架,它本质上就是一个运行在服务器里的Java类,负责接收HTTP请求并返回响应。你可以把它类比成餐厅门口的服务员:客人(浏览器)递过来一张点菜单(HTTP请求),服务员转给后厨(业务代码),做好菜之后再由服务员端回去(HTTP响应)。
但Servlet不会自己凭空运行。它需要Tomcat这类Web容器来创建、调用和销毁。Tomcat负责监听端口、接收请求、找到对应的Servlet类、调用对应的方法。你作为开发者要做的,只是告诉Tomcat:当访问某个地址时,该调用哪个类。这个“告诉”的过程,在Servlet 3.0之后的版本里,最简单的方式就是@WebServlet注解。
1.3 项目需要的最简依赖和范围
一个最基础的Maven Web项目,其实只需要一个Servlet依赖。
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>这里的scope=provided值得画个重点。意思是:编译代码时我需要这个jar包,但最终打出war包的时候,不要把它打包进去。为什么?因为Tomcat的lib目录里已经自带了一份servlet-api.jar。如果你再塞一遍,两个同名的Servlet类同时存在,轻则找不到类,重则版本冲突直接启动报错。
很多初学教程把这行配置直接复制过去,但从不解释provided的含义,结果有人把它删掉,又有人把它改成compile,改完各种诡异问题。这里先说透:provided是Servlet开发的标准姿势。
2. 在IDEA 2019里新建带Maven骨架的Web项目
2.1 新建项目的完整选择路径
IDEA 2019新建项目时,有两条路径可以达到类似目的,我这里用的是最标准的一条:
File → New → Project → 左侧选Maven → 勾选Create from archetype → 在列表里找到org.apache.maven.archetypes:maven-archetype-webapp→ Next。
这里有个容易卡住的地方:IDEA需要联网拉取archetype列表,第一次打开可能会加载半天,甚至列表是空的。我当时的处理是等两分钟,再打开一次就出来了。如果你实在等了很久还是空的,可以检查一下Maven的settings.xml是否指向了能正常下载依赖的仓库地址。这一步不涉及什么复杂原理,就是让Maven能正常访问中央仓库而已。
还有一点必须提前告诉你:IDEA Community社区版不支持Java Web和Tomcat的完整集成,工具栏里找不到Tomcat的运行入口。如果你手头是社区版,建议先用Ultimate旗舰版来完成这个实验,否则后面会卡在配置Tomcat那一环。
2.2 坐标和项目名怎么填
点击Next之后,需要填写Maven坐标。我填的是:
- GroupId:
com.example - ArtifactId:
servlet-demo - Version:默认
1.0-SNAPSHOT
GroupId一般写反向域名,作用像包名,用来区分组织或公司。ArtifactId是项目的实际名字,对应的是你的war包最终叫什么。这里有个小细节:IDEA默认会把ArtifactId当项目名,Module路径也用这个,所以尽量取一个能看懂的名字,建议用纯小写字母和连字符,不要用中文和空格。
2.3 创建完成后先看一眼目录结构
Finish之后,IDEA会帮你生成一个最简骨架。它的目录大概长这样:
servlet-demo ├─ pom.xml └─ src └─ main ├─ resources └─ webapp ├─ WEB-INF │ └─ web.xml └─ index.jsp注意了:这个骨架里没有我们最想要的src/main/java目录。很多新手在这里直接懵了,“我明明建的是Java项目,为什么不给我建Java目录?”原因是webapp这个archetype默认只生成Web资源目录,源码目录需要你自己创建。不要慌,这是正常现象,我会在下一步带你补上。
如果你用Maven面板查看Dependencies,可能会看到一堆文件,但里面并没有javax.servlet-api的依赖。别急,这也正常,初始pom.xml几乎是空的,依赖本来就需要我们自己加。
3. 补全工程结构,把骨架改造成可用的Web工程
3.1 手动创建src/main/java并标记为源码目录
回到项目里,右键src/main目录 → New → Directory,输入名字java。建好之后,它现在只是个普通目录,IDEA不会主动把它当成源码目录来看待,所以还要做一次关键操作:
右键java目录 → Mark Directory as → Sources Root。
这一步的含义是告诉IDEA:这个目录里的.java文件要参与编译,编译结果会输出到target/classes。如果你跳过标记,后面写完Servlet类,IDEA不会报红,但编译时它会假装这个类不存在。很多人最后404找不到Servlet类,原因往往就出在这里。
同样的道理,src/main/resources如果没被标记,也需要右键 → Mark Directory as → Resources Root。这个目录以后用来放一些配置文件,现在空着也没关系。
3.2 在pom.xml里补齐依赖和打包配置
打开项目根目录的pom.xml,你会看到IDEA自动生成的坐标信息。这里要注意,maven-archetype-webapp生成的pom.xml默认的packaging标签通常是war,不过有些版本也可能是jar,务必确认一下:
- packaging为
war:Web项目,最终打包成可部署到Tomcat的war文件。 - packaging为
jar:普通Java类库,这不是我们要的。
我的做法是直接把pom.xml替换成下面这个最简版本,同时加上了编译插件,避免IDEA默认用奇怪的编译器版本导致JDK不匹配。
<?xml version="1.0" encoding="UTF-8"?> <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>servlet-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>servlet-demo</finalName> </build> </project>这里我特意把JDK编译版本锁成了1.8,因为IDEA 2019配合Java 8几乎是当年的黄金组合。如果你本机只装了更高版本的JDK,把maven.compiler.source和maven.compiler.target改成对应的版本也行。
放心,上面的配置完全兼容Tomcat 8和Tomcat 9。Tomcat 8对应Servlet 3.1,Tomcat 9对应Servlet 4.0,但servlet-api 3.1的写法在Tomcat 9里同样能跑,基础接口没变。
3.3 resources目录为什么值得留一个习惯
在刚开始的这个Demo里,src/main/resources里什么都不用放。但请你养成习惯:以后数据库连接配置、日志配置、MyBatis映射文件这些,都会放在这个目录下,而且经过Maven构建后,它们会被原样复制到target/classes里。这比把配置文件放在其他角落要规范得多。现在空着,但目录结构保留在那里,后面学习框架时你会感谢这个习惯。
4. 写第一个Servlet:从代码层面理解一次HTTP响应
4.1 Servlet类的骨架和关键导入
现在真正的重头戏来了。在src/main/java下新建包com.example,再新建Class,类名叫HelloServlet,然后让它继承HttpServlet。
package com.example; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println("<html>"); writer.println("<head><title>Hello Servlet</title></head>"); writer.println("<body>"); writer.println("<h1>第一个 Servlet 跑通了</h1>"); writer.println("</body>"); writer.println("</html>"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }为什么必须继承HttpServlet,而不是随便写个普通类?因为Tomcat在运行时只认继承了HttpServlet的类,它会利用Java反射机制创建实例,然后根据HTTP请求的类型自动调用doGet或者doPost。你不需要自己写main方法,生命周期由容器代管。
这里还有一个知识点:HttpServlet的父类GenericServlet提供了一套完整的Servlet生命周期方法,比如init()、service()、destroy(),只不过我们平时只关心doGet和doPost这两个钩子方法。
4.2 doGet和doPost,以及响应的中文编码
doGet处理浏览器通过地址栏直接访问的GET请求;doPost处理表单POST提交或者Ajax POST请求。初学者最容易犯的问题是把代码写在doGet里,然后表单用method="POST"提交,结果发现Servlet永远不执行。为了偷懒,很多Demo会在doPost里直接调doGet,我这里也这么做了,至少保证逻辑一致。
response.setContentType("text/html;charset=UTF-8")这行千万不能漏。它的意思是:告诉浏览器,我返回的内容是HTML,而且使用UTF-8编码。如果没有指定字符集,Tomcat默认按ISO-8859-1输出,中文显示出来就是乱码。你可以把这行理解成“上菜前给客人说清楚这是中餐还是西餐,并且用中文菜单点单”。
4.3 @WebServlet注解与web.xml的取舍
注解里的/hello就是访问路径。路径必须以斜杠开头,写成hello是不对的,Tomcat会直接报错。用注解方式配置Servlet,意味着你不用再去web.xml里手动注册,这种写法是Servlet 3.0以后的主流。
当然,maven-archetype-webapp默认生成的web.xml还是会存在的,这个没关系,留着不影响。旧项目需要用XML方式配置Servlet时,一般在web.xml里这样写:
<servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>两套配置的最终效果完全一样,但注解方式明显更简洁。我把两者的对比列一下,方便你理解为什么不用写web.xml:
| 对比维度 | @WebServlet注解 | web.xml配置 |
|---|---|---|
| 配置位置 | 写在类上 | 写到XML文件里 |
| Servlet容器支持 | 需要3.0及以上版本 | 所有版本都支持 |
| 阅读性 | 类名与路径直接对应 | 需要来回翻文件 |
| 维护成本 | 改类时路径跟着改 | 改了XML还要重启生效 |
如果你以后接手老项目,看到web.xml里到处是servlet-class和servlet-mapping,不用慌,那只是历史版本留下的风格,原理是一样的。
4.4 提前踩一下编码的坑
在实际的实验里,我第一次访问Servlet时返回的内容一切正常,但一旦表单里的中文经过POST请求传到Servlet再echo回页面,就出现了乱码。Tomcat 8开始,POST请求体的默认编码就是UTF-8,所以大多数情况下没问题。但老版本的Tomcat并不保证这一点,所以保险起见,在doPost里读取参数之前加一行request.setCharacterEncoding("UTF-8"),能省去很多玄学问题。
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); doGet(request, response); }5. 让项目跑起来:Tomcat配置与第一次访问
5.1 准备一个本机Tomcat
Servlet自己不会跑,你得先准备好一个Web容器。我用的Tomcat 9.0.x,解压到一个不含中文和空格的路径,比如D:\apache-tomcat-9.0.xx。解压出来的目录里,几个关键目录要认一下:
bin:启动和关闭脚本都在这里。conf:server.xml等全局配置在在这里。lib:Tomcat自带的jar包,servlet-api.jar就在这。webapps:放war包或部署文件夹的地方。
初学者不建议去记命令行启动方式,因为IDEA会帮你完成Tomcat的呼起,但你至少要知道Tomcat长什么样,不然出了端口错误都不知道去哪儿查。
5.2 在IDEA 2019里配置Tomcat
点击IDEA右上角的运行下拉框 → Edit Configurations → 左上角加号 → 找到Tomcat Server → Local。
注意:如果在加号列表里没有Tomcat Server这一个选项,几乎可以断定你使用的是IDEA Community社区版。这不是项目配置的问题,是版本功能限制。需要切换到Ultimate版才能做这一步。
在弹出的页面里,Name随便填,比如Tomcat9;然后点击Application server右边的Configure,在弹出的对话框里选择你解压好的Tomcat目录,IDEA会自动识别版本号。这一步完成后,IDEA就能直接和Tomcat通信了。第一次操作时我还遇到过一个问题:IDEA提示需要选择JRE,确保你项目用的JDK和Tomcat支持的是同一套Java环境,用JDK 8最省事。
5.3 让Tomcat知道该运行哪个项目
配好Tomcat之后,还不能直接点启动,因为你还没告诉Tomcat“要部署哪个项目”。切到Deployment选项卡,点加号 → Artifact → 选择带war exploded字样的选项。
这里我想多说一句war和war exploded的区别。war exploded可以理解成“已解压的war”,IDEA会把target/classes、webapp等资源拼成一个目录结构直接给Tomcat加载,好处是改代码之后可以支持热更新,调试非常方便。完整war则是把一个压缩包丢给Tomcat,适合最终交付部署。开发阶段,选war exploded就对了。
选好Artifact之后,还要设置Application context。我建议直接把/servlet_demo_war_exploded这种长后缀改成/,或者也填/hello,取决于你想用什么根路径访问。如果你设成/,那么最终的访问地址就是http://localhost:8080/hello,省去一层context的干扰,对初学者最友好。
5.4 第一次启动:看到什么才算成功
点右上角绿色的运行按钮,IDEA下方会弹出Run窗口,里面会唰唰刷出一片Tomcat启动日志。看到类似“Server startup”字样,就说明Tomcat已经起来了。
这时候打开浏览器,访问http://localhost:8080/hello,如果一切正常,页面会显示“第一个 Servlet 跑通了”。如果页面直接404,先别急着查代码,大概率是下面这三件事之一:
- 启动时IDEA报错,端口8080被其他程序占用,换到8081之后你却还在访问8080。
- Deployment里没有添加Artifact,Tomcat根本没有加载你的项目。
java目录没有标记为Sources Root,Servlet类没有被编译出来。
我会在后面的排查部分再展开细讲。第一次跑通的时候,建议多花两分钟点击IDEA运行窗口里的日志,找到类似Deployment of web application这样的行,能让自己直观地感受到“项目是被Tomcat加载的”。
6. 从浏览器到Servlet:一次请求的完整旅程
6.1 做一个最简单的表单页面
光能访问Servlet还不够,我们来做一次真实的数据交互。把maven-archetype-webapp自动生成的index.jsp删掉,在src/main/webapp下新建一个index.html,内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>第一个Servlet表单</title> </head> <body> <form action="hello" method="post"> <label for="username">用户名</label> <input type="text" id="username" name="username" placeholder="请输入用户名"> <input type="submit" value="提交"> </form> </body> </html>注意表单里的action="hello",当浏览器地址是http://localhost:8080/时,它会相对拼接成http://localhost:8080/hello。这是一种相对路径写法,简单但依赖当前请求地址。如果你想绝对稳妥,直接写action="http://localhost:8080/hello"或action="/hello"也行。
6.2 在Servlet里接收参数并回显
修改HelloServlet的doPost,让它读取表单携带的参数:
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println("<html>"); writer.println("<body>"); writer.println("<h1>收到参数:" + username + "</h1>"); writer.println("</body>"); writer.println("</html>"); }现在启动Tomcat,先访问http://localhost:8080/看看表单,再提交。这样一来,从浏览器发起HTTP请求、表单数据以POST方式传输、Servlet通过getParameter拿到参数、再拼接成HTML返回,整条链路就被打通了。
6.3 后台日志帮你看到请求来了
再往源码里加一行:
System.out.println("收到提交的用户名: " + username);重启Tomcat后再提交一次表单,IDEA的Run窗口里会打印出这一行。这一步对排查问题特别重要:如果你在页面看到了内容,说明请求已经到达Servlet;如果你什么都没看到,就说明请求可能就没有转发到这个类,排查方向会完全不同。
7. 新手最容易踩的坑:404、依赖失效、结构错乱
7.1 “程序包javax.servlet不存在”的完整排查链路
这个报错太经典了,几乎每个新手都见过。我先描述一下你遇到的场景:写完Servlet类,IDEA的编辑器可能不报错,但一编译就冒出“程序包javax.servlet不存在”。遇到这个提示,按下面这条链路走,很快能找到原因。
- 打开项目根目录的pom.xml,确认里面有没有我上面写的Servlet依赖。如果没有,复制进去。
- 看IDEA右侧的Maven面板,展开Dependencies,检查
javax.servlet-api:jar:3.1.0是否出现在列表里。如果没有,点面板顶部的刷新图标,让IDEA重新读pom.xml并下载依赖。 - 如果出现了依赖但编译仍然报错,到File → Project Structure → Libraries里检查有没有Maven引入的jar包。
- 确认pom.xml里
scope是provided而不是系统缺失或空值。空值默认是compile,虽然也能编译,但和打包部署有关的语义会不太一样,建议按标准写provided。 - 实在不行,File → Invalidate Caches / Restart,清掉IDEA的索引缓存。这个操作经常解决一些“明明什么都没改,突然就编译失败”的玄学问题。
这一步要特别提醒:刷新Maven依赖和清缓存是两个不同层面的操作。前者是做增量更新,后者是推倒缓存重建。遇到提示“Cannot resolve javax.servlet-api:3.1.0”,大概率是Maven仓库没有这个包,而不是IDEA的问题。
7.2 页面404的三种情况,以及对症处理
Tomcat启动成功,但浏览器访问/hello返回404,大部分原因可以归到这三类:
| 现象 | 原因 | 处理 |
|---|---|---|
| 地址访问后出现Tomcat默认404页 | Servlet路径没注册上 | 检查@WebServlet注解路径是否以/开头 |
| 访问根路径没反应 | 没有部署Artifact或context没有设置对 | Edit Configurations → Deployment → 添加war exploded |
| 提交表单后404 | 表单action路径与Servlet映射不匹配 | 检查index.html里的action是否写成hello还是/hello |
其中最容易忽略的就是目录标记问题:如果你src/main/java没有Mark Directory as Sources Root,IDEA编译时不会把HelloServlet.class放进target/classes,Tomcat运行时自然找不到这个类。表现出来就是Tomcat启动正常,但访问路径404。我在前面强调过这一步,这里再说一遍,因为实际遇到的人真的不少。
7.3 war exploded和war,开发阶段别选错
很多教程会直接让你在Deployment里选war,我建议你在IDEA中选war exploded。两者的区别,我整理成一张表:
| 对比维度 | war | war exploded |
|---|---|---|
| 部署方式 | 压缩包 | 解压文件目录 |
| 修改后生效 | 需要重新打包 | 配合IDEA设置可热更新 |
| 调试体验 | 较差 | 较好 |
| 最终交付 | 适合 | 不适合 |
开发阶段选war exploded,然后还可以在Edit Configurations中把On frame deactivation设置为Update resources,这样你的HTML、图片等静态资源改完后IDEA会自动同步,省去手动重启的工夫。如果想偷懒更彻底,还可以在Update action下拉框里选择Redeploy或Update classes and resources,但这部分设置和IDEA版本有关,如果你找得到就顺手配上,找不到也不影响核心实验。
7.4 我的一个小技巧:用Maven打包来验证一切
当你觉得项目配置乱成一团,或者搞不清楚到底是IDEA自动编译出了问题,还是Maven配置出了问题,可以直接在IDEA右侧的Maven面板里,双击package生命周期,或者到项目根目录执行命令行:
mvn clean package执行完之后,去target目录看看有没有生成servlet-demo.war。如果war包能正常打出来,说明你的pom.xml、目录结构、依赖声明都没有大问题,剩下就是IDEA运行配置层面的问题;如果打包过程报错,比如提示找不到javax.servlet,那问题一定出在依赖或者目录标记这一块。用这个办法,能把“项目有问题”和“IDEA配置有问题”这两件事一刀切开。
我在实际开发中还有一个体会:以后你学Spring Boot时,会发现Servlet这个概念被Spring的DispatcherServlet包在后面了,但如果你当初手写过第一个Servlet,理解了doGet和doPost、URL映射、HTTP请求和响应的交互,那么看Spring MVC的源码时,会轻松很多。基础这一步绕不过去,也别嫌慢,慢走一遍比快抄十遍有价值。