第一次搭Java Web项目,你大概率照着教程点过一遍,最后看到localhost:8080弹出页面,以为万事大吉。等你需要自己从零配置JDK8、Tomcat8、Maven3、IDEA2020这一整套环境的时候,才会发现那些教程里被跳过的步骤,每一项都藏着不少细节。这套组合放在今天算不上新,但它恰好卡在Java Web技术演变的交接点上:往前看,老一套配置重、写法繁琐;往后看,新框架又帮你把底层逻辑藏得太深。用这组环境入门,你能亲手把Servlet、JSP、web.xml、Maven依赖、Tomcat部署这些基本功全部过一遍,之后再转Spring Boot或者深入微服务,理解速度完全不一样。
这篇内容不适合只想“复制粘贴点下一步”的朋友,更适合想弄明白“为什么要这样配置”的读者。我会把从环境安装到第一个Servlet运行的全过程拆开讲,包括版本选择的逻辑、IDEA建工程的两条路径、Tomcat部署背后的原理,以及我实际踩过的404、乱码和热部署不生效的问题。看完之后,你可以照着完整搭出一个能跑、能改、能部署的最小Java Web工程。
1. 环境准备与版本选型:为什么是这四件套
1.1 版本搭配的逻辑与取舍
先说清楚一件事:JDK8、Tomcat8、Maven3、IDEA2020这套组合,不是随手选的,它对应着Java Web生态里一个非常适合入门的稳定交集。
- JDK8:无论你现在看到多少招聘要求写了JDK17或者JDK21,JDK8依然是国内大量老项目和教材默认使用的主力版本。它对Servlet/JSP的支持最成熟,学习资料最多,碰到报错时在网上几乎都能搜到直接可用的答案。而且JDK8里的Stream、Lambda这些语法,不夹杂太多新特性干扰,刚好够用。等基础稳定了再往上跳,心理负担小很多。
- Tomcat8:从Servlet规范的角度看,Tomcat8对应Servlet 3.1。这个版本把注解式开发做得比较顺手,你可以在Servlet类上用一个
@WebServlet注解就完成路由映射,不用在web.xml里写一大段配置。但它又保留了web.xml这种传统配置方式,能让你理解老项目里常见的一大堆XML到底在干什么。相比Tomcat7,Tomcat8的类加载机制和并发性能也更稳,对新手来说踩坑概率低。 - Maven3:Maven是Java项目构建的事实标准。Gradle也很好,但基础阶段我更推荐先掌握Maven,因为它的“约定优于配置”更直白:目录结构固定、依赖声明集中在pom.xml、构建生命周期命令清晰。这些概念一旦形成,后面几乎每个项目都能复用。
- IDEA2020:这个版本对Java Web工程的支持已经很成熟,内置了Tomcat集成、Maven面板、热部署等能力。不选更新版本,是因为基础阶段用到的功能在这里全都有,没必要为了用不上的新特性去折腾更高的硬件要求或版本兼容问题。
打个比方:JDK是发动机,提供Java语言编译和运行能力;Maven是流水线,负责把源码和依赖按固定流程组装成半成品;Tomcat是展柜,专门承接Web应用的产物,把HTTP请求翻译成Servlet调用;IDEA则是操作台,把这前面三样整合到一个界面上。这四样东西各管一段,缺一个你都会发现在某个环节上怎么都跑不顺。
1.2 安装与环境变量配置
安装顺序很重要,我建议严格按这个顺序来,因为Tomcat和Maven启动时都要依赖JAVA_HOME,JDK没配好,后面全是白搭。
第一步,安装JDK8。选默认步骤安装就行,但安装路径里最好不要有中文和空格,比如C:\Program Files\Java这类路径,部分情况下脚本对空格处理会出现奇怪问题。个人建议装在D:\dev\jdk8这种干净目录,方便后面查路径。
第二步,配环境变量。新增JAVA_HOME指向JDK安装根目录;在Path里加%JAVA_HOME%\bin。然后打开新命令行窗口,输入java -version,看到1.8.0_xxx的输出,说明JDK这一步通过了。
这里有个经验:配置完环境变量后,一定要新开一个命令行窗口再验证。旧窗口缓存了之前的PATH,你验证半天以为配错了,其实只是没刷新。
第三步,下载Maven3,解压到某个目录。同样配置MAVEN_HOME和Path里的%MAVEN_HOME%\bin,新开命令行验证mvn -v。Maven本质是一个带脚本的程序,解压即用,不需要安装。但它依赖JAVA_HOME,所以一定是在第二步之后再做。
第四步,下载Tomcat8,解压到一个目录即可。先不用启动,等IDEA那边配置好再统一操作,避免端口提前被占。
第五步,安装IDEA2020。第一次打开会提示导入配置,直接选默认就好。有一点要注意:IDEA需要带Java Web集成能力的版本,社区版默认没有完整的Tomcat运行配置入口,如果安装的是精简版,后面会找不到Tomcat Server选项。
还有一个基础知识点要提前讲:Maven默认会把依赖下载到用户目录的.m2文件夹下,第一次构建时下载量非常大。如果下载速度特别慢,可以在settings.xml里配置一个镜像源加速,后面我会在工程习惯部分再提一次。
2. 用IDEA创建第一个Java Web工程
2.1 两种建项目方式:骨架项目 vs 手工搭工程
IDEA2020里新建Java Web工程有两条路径,我建议你两条都试一遍,理解会更透彻。
第一条路径是使用Maven的webapp骨架。操作方式是:File -> New -> Project -> Maven -> Create from archetype -> maven-archetype-webapp。这个骨架会直接生成一个最简web工程,包含src/main/webapp目录和index.jsp,连web.xml都帮你占好位了。但它有一个典型问题:骨架生成的目录里没有标准的src/main/java和src/test/java,你需要自己创建目录,而且IDEA不会自动把它们标记为源代码根目录,要手动右键目录选择Mark Directory as Sources Root。这一步如果漏掉,后面写Servlet类时经常会出现编译不过或者类不识别的问题,对新手来说非常容易让人抓狂。
第二条路径是手工搭工程,也是我个人更推荐的方式。操作方式是新建一个Maven项目,不选任何骨架,生成空工程之后自己补全目录结构:在src/main下建java和resources,在src下建webapp/WEB-INF,再手动创建web.xml和pom.xml。看起来比骨架路径多几步,但你清楚地知道每个目录的职责,出了问题也更容易定位。
补充一点,IDEA2020其实还有Java Enterprise模板可以直接创建Web Application。但那个方式默认不依赖Maven,而是用IDEA自己的构建机制,跑通很快,后续依赖管理会比较麻烦。基础阶段我强烈建议用Maven管理依赖,否则后面加Spring或其他框架时会非常别扭,不用一开始就养这种手工作业的习惯。
2.2 工程结构、pom.xml与web.xml的职责
手工搭好的工程结构大概长这样:
project-root ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ ├── resources │ │ └── webapp │ │ └── WEB-INF │ │ └── web.xml │ └── test │ └── java这个结构不是随便定的,是Maven约定的标准布局。src/main/java放业务代码,src/main/resources放配置文件和资源,src/main/webapp是Web应用的根目录,WEB-INF下放部署描述文件web.xml,而WEB-INF/lib会自动加载项目依赖的jar包,WEB-INF/classes放编译后的class文件。理解这个约定,你就能明白Tomcat为什么能自动找到Servlet,为什么依赖jar要放在lib下面。
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.demo</groupId> <artifactId>first-web</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> </dependencies> </project>这里有三个重点。第一,packaging一定要写成war,因为Web应用的最终产物是war包,要交给Tomcat部署。如果保留默认的jar,IDEA在部署配置时会找不到可用的Web Attributes,这一步经常影响后续的Tomcat运行配置。第二,javax.servlet-api和jsp-api的scope要写成provided,意思是在编译时使用,但打包时不用把这两个依赖塞进war包,因为Tomcat自己已经带了Servlet和JSP的实现。如果忘了写provided,打包后的war包里会出现两组一样的类库,运行时反而容易冲突。第三,maven.compiler.source和target直接指定为1.8,能避免IDEA设置的JDK版本和Maven编译版本不一致导致的怪问题。
web.xml在Servlet 3.1环境下其实可以删掉,因为已经有注解可用。但这一阶段我不建议删,它仍然是理解URL映射的好工具。后面写Servlet时,我会同时演示注解和web.xml两种方式,对比着看最清楚。
3. 从代码到页面:Servlet + JSP 的最小闭环
3.1 手写一个Servlet:注解与web.xml两种映射方式
在src/main/java下新建一个包,比如com.demo.servlet,然后创建HelloServlet类:
package com.demo.servlet; 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 req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<html><body>"); out.println("<h1>Hello Java Web</h1>"); out.println("</body></html>"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }解释一下为什么代码要这么写。@WebServlet("/hello")是Servlet 3.1提供的注解式映射,告诉Tomcat当访问/hello这个路径时,由这个类来处理。HttpServlet里最常用的两个方法doGet和doPost,分别对应HTTP的GET和POST请求。开发中如果不重写doPost,表单提交过来的时候很可能会返回405错误,所以一般会让doPost调用doGet,避免踩到这个空实现。
这里必须强调一个初学者容易误解的点:Servlet不是自己new出来的。Servlet由Web容器也就是Tomcat负责实例化、初始化、调用和销毁,这个过程叫Servlet生命周期。容器启动时会扫描带@WebServlet注解的类,把URL路径和Servlet实例的映射关系登记到内部。你写的代码只是一个“被调用者”,不能也不需要自己创建实例。理解这一点,后面学Spring MVC的DispatcherServlet时会顺利很多。
如果走web.xml的映射方式,也很简单,在web.xml里添加:
<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"> <display-name>first-web</display-name> <servlet> <servlet-name>hello</servlet-name> <servlet-class>com.demo.servlet.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>web.xml做的事情,是把“类名”和“URL路径”的对应关系显式写出来。好处是老项目里配置一目了然,坏处是Servlet一多,XML会膨胀得很严重,所以现代新项目普遍改用注解。但你必须能看懂老项目里的servlet-mapping,因为不少企业的历史项目代码还是这种写法。
3.2 JSP页面、Tomcat运行配置与首次启动
再写一个最简JSP页面,放在src/main/webapp/index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Java Web Demo</title> </head> <body> <h2>这是第一个Java Web项目</h2> <a href="${pageContext.request.contextPath}/hello">访问Servlet</a> </body> </html>这里用了一个EL表达式${pageContext.request.contextPath},作用是自动获取当前项目的上下文路径。这是新手非常容易踩的坑:很多教程直接写href="/hello",但项目部署时的上下文路径往往不是根路径,比如可能是/first-web_war_exploded,这时候绝对路径就直接404了。用EL表达式拼上下文路径,能保证不管部署名怎么变,链接都能正确访问。
接下来配置Tomcat并启动,操作步骤是这样的:
- 打开IDEA右上角的
Add Configuration,点加号,选择Tomcat Server -> Local。 - 在
Application Server区域点Configure,选到你本地Tomcat8的解压目录。 - 切到
Deployment选项卡,点加号选择Artifact,通常会看到一个类似first-web:war exploded的选项。选中后,IDEA会自动在URL里加上一个上下文路径。 - 回到
Server选项卡,把URL改成你希望最终访问的地址,比如http://localhost:8080/first-web/,这样访问起来更清爽、路径和工程名能对上。 - 勾选
Update resources和Update classes,这样改代码后不用反复重启Tomcat,按相应快捷键或切换窗口失焦时就能自动更新。
启动成功后,浏览器里访问http://localhost:8080/first-web/,能看到index.jsp的页面,点击链接跳转到/hello,页面显示Hello Java Web,整个最小闭环就算跑通了。
这里顺便解释一下war exploded的意思。它指的是“展开后的war包”,也就是把编译好的classes和资源按目录结构直接交给Tomcat,而不是先压缩成war再让Tomcat解压。开发阶段用exploded模式,好处是更新资源时不需要重新打包,整个迭代速度会快很多。等真正发布的时候,再用mvn clean package生成一个完整的war包,复制到Tomcat的webapps目录下部署。
4. 常见坑与排查技巧实录
4.1 高频问题速查表
我把自己实际遇到、以及身边学基础的同学反复踩的坑整理成一个速查表,建议收藏起来对照排查:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| 访问页面404 | 上下文路径写错、应用没部署成功、URL路径与映射不一致 | 核对IDEA Deployment里的Application context和URL,检查@WebServlet的路径与访问路径是否完全一致 |
| 启动时报端口被占用 | 本机已有Tomcat或其他服务占用8080 | 找到占用进程并结束,或者修改Tomcat的conf/server.xml里Connector端口为8081 |
编译报invalid source release: 11 | Project SDK和Language Level不一致,或者pom里没指定编译版本 | 在Project Structure里统一切到8,并在pom中加上maven.compiler.source和target为1.8 |
ClassNotFoundException | 依赖jar没有拷贝到WEB-INF/lib下 | 在Project Structure的Artifacts里把Maven依赖加入lib目录,或者用mvn package重新打包 |
| 中文乱码 | request和response的编码与页面编码不一致 | Tomcat的Connector设置URIEncoding="UTF-8",Servlet里统一setCharacterEncoding,JSP页面头部加charset=UTF-8 |
| 修改代码后不生效 | 没开热部署,或者Tomcat安装配置用的是旧目录 | 检查部署配置是否勾选了Update resources,必要时手动Build -> Rebuild |
4.2 三个经典场景的排障过程
光看速查表还是有点抽象,我挑三个典型场景展开讲讲。
第一个场景,点击首页链接404。当时有个同学跑来问我,说首页正常,一点Servlet链接就404。我先让他直接访问http://localhost:8080/first-web/hello,结果直接访问也404,说明问题不在index.jsp,而在Servlet映射或部署上下文。我再检查代码,发现@WebServlet写的是/hello,访问路径也是/hello,按理说没问题,最后发现他把类放到了default包下面,没有建包结构。Tomcat对默认包里的Servlet类支持不完整,导致映射扫描失败。建包之后重新编译,问题立刻消失。这个小问题也说明,包名结构必须从一开始就规范。
第二个场景,中文乱码。最常见的是JSP页面显示正常,但是Servlet输出中文全乱。原因往往是IDEA的文件编码默认不是UTF-8,JSP和Servlet文件里的中文已经被保存成GBK了。解决方法是统一工程编码:在IDEA的Settings里把Global Encoding、Project Encoding、Properties Files三个地方都设置为UTF-8,同时把之前已经乱码的文件复制内容出来、删掉重建一次,编码就统一了。如果只是运行时的乱码,还要注意Tomcat连接参数,最好在server.xml里加上URIEncoding="UTF-8"。
第三个场景,热部署不生效。很多人配置完Tomcat后,改了代码重启发现没变化。其实IDEA的热部署不是无条件的,需要你在Run Configuration里勾选对应的更新策略。最常见的做法是把On frame deactivation更新选项设置为Update resources and classes。但要注意,如果修改了web.xml或者加了新的依赖,热部署有时候不会完整生效,这时候还是手动重启一次最稳。热部署是为了小步快跑,不是替你处理所有重启需求。
4.3 一些工程习惯和维护建议
跑通第一个项目之后,我建议你马上做三件事,别急着写下一个功能。
第一,约定好包名结构。不管项目多小,都按com.xxx.servlet、com.xxx.filter、com.xxx.entity这样拆分包。见过太多基础教程里的Servlet全部丢在一个包里,代码少的时候没事,一旦后面加Filter、Listener、Service,不分包就是灾难,找类全靠眼睛扫。
第二,固定Maven依赖版本。Servlet和JSP相关依赖的版本不要随便升。Java Web涉及容器、Servlet API、JSP API三者之间的版本兼容,改一个版本,很可能就会遇到奇怪的运行时异常。如果想加速Maven下载,可以在settings.xml里配置镜像源,这在基础阶段就能省下大量等待时间。但配置完镜像源后,记得把本地仓库目录也确认一下,别把依赖下到系统盘不知道的位置,后面清理起来麻烦。
第三,学会看Tomcat启动日志。这听起来像套话,但实战里真的能救命。Tomcat启动时,控制台会出现Server startup in xxx ms这一行,如果到最后都没看到这行,说明容器没有启动成功。常见原因包括web.xml写错、Servlet类没加载、端口被占用等。先判断问题出在“容器层”还是“应用层”,排查范围一下子就能缩小。
再补充一个实用小套路:首页正常但点击链接404的时候,先用二分法定位。直接访问http://localhost:8080/first-web/hello,如果直接访问没问题,说明是页面里的路径写死导致404;如果直接访问也404,说明Servlet映射或部署上下文有问题,基本和前端页面无关。新手靠这个办法,能把排查时间从半小时压缩到几分钟。
5. 基础过后的三个扩展方向
5.1 写一个Filter统一处理编码
第一个值得马上动手的扩展,是自己写一个过滤器Filter,统一处理请求和响应的编码。虽然现在的Servlet代码里都设置了UTF-8,但以后项目里会有大量Servlet,每个都写一遍显然太低效,Filter正好解决这个横切问题。
代码很简单,新建com.demo.filter包,创建EncodingFilter:
package com.demo.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } }@WebFilter("/*")表示拦截所有请求,chain.doFilter的作用是把请求继续向后传递,最终到达Servlet。你可以把Filter理解成一道关卡,所有请求到了之后先过这里,做完统一处理再放行。这也是后面学Spring MVC拦截器的基础,原理一致的。
5.2 验证一次Servlet生命周期
第二个扩展,是花十分钟验证Servlet生命周期。在HelloServlet里重写init和destroy方法,往控制台打几行日志:
@Override public void init() throws ServletException { System.out.println("HelloServlet init ..."); } @Override public void destroy() { System.out.println("HelloServlet destroy ..."); }然后启动Tomcat,你会看到init只执行一次,但每次刷新页面,doGet都会执行一次。关掉Tomcat时,destroy执行一次。这个过程能让你直观理解Servlet是单实例多线程的,容器启动时就创建好实例,而不是每次请求都新建对象。搞懂这一个生命周期,后面学连接池、ThreadLocal这些概念,就不会觉得它们在凭空冒出来了。
5.3 把项目打包部署到独立Tomcat
第三个扩展,脱离IDEA,手动部署一次。
在项目根目录打开命令行,执行:
mvn clean package构建成功后,target目录下会生成一个first-web-1.0-SNAPSHOT.war文件。把这个war包复制到Tomcat的webapps目录下,再启动Tomcat,Tomcat会自动解压和部署。访问地址是http://localhost:8080/first-web-1.0-SNAPSHOT/。
如果你想用根路径访问,比如直接通过http://localhost:8080/进入项目,就把war包重命名为ROOT.war再放到webapps下,Tomcat会把它当成默认应用。这个方法在生产环境很常用,理解了它,你就明白了IDEA里的Tomcat配置只是替你把这一步自动化了而已。
做到这里,基础阶段的最后一个闭环已经完整了:从源码到war包,从IDEA内嵌部署到独立Tomcat部署。代码层面的变化其实不大,但对环境、构建、部署的掌控感会完全不同。
我个人带项目的一个体会是,基础阶段慢一点反而学得更快。很多同学急着一下跳到Spring Boot,结果遇到一个404查半天,连是Tomcat的问题还是代码的问题都分不清。这个最小Java Web项目虽然不起眼,但你如果把每个文件为什么存在、每次启动控制台输出了什么都顺一遍,后面学任何框架都会轻松很多。至少我自己后来看Spring MVC源码时,心里始终有Servlet容器这张底图,很多抽象概念就自动落到地面了。