带新人的时候我经常发现一个奇怪的现象:明明代码逻辑都写对了,数据库也建好了,但项目到了“跑起来”这一步就卡壳。点开运行按钮,浏览器一片404;或者Tomcat启动了,页面却报500;再不然就是项目能开,数据库连接直接超时。这些问题说难不难,但排查起来特别绕,原因往往不是某一个环节错,而是从环境版本、项目结构、部署方式到数据库驱动这一整条链路里,有一个环节没对齐。
这篇文章我就用最笨的办法,把JavaWeb项目从创建、配置Servlet、连数据库到扔进Tomcat部署的完整过程拆开讲一遍。默认你用的是IntelliJ IDEA + Maven + MySQL + Tomcat这套最常见组合。适不适合你?只要你是刚学完Servlet、准备做第一个能增删改查的Web项目,或者被部署问题折磨了一下午还没搞定的人,这文章就是给你写的。
1. 版本组合选不对,后面全是坑:JDK、Tomcat、IDEA的配比逻辑
1.1 Tomcat 10之后Servlet命名空间发生了分水岭
很多人在网上找Servlet教程,复制了一段import javax.servlet.http.HttpServlet,结果在Tomcat 10里编译直接报错。这不是你代码的问题,是Tomcat版本导致的API迁移。
Tomcat 9及之前版本,Servlet规范里所有类的包名是javax.servlet开头;但从Tomcat 10开始,整个Servlet和WebSocket规范从Java EE迁移到了Jakarta EE,包名变成了jakarta.servlet。也就是说你把Tomcat 10下载回来,项目里所有import javax.servlet.*的地方都要改成jakarta.servlet.*,否则部署后就是ClassNotFoundException。
1.2 我建议的环境组合
如果你刚开始学习,不必追新版本,选一套社区验证最充分的组合,比用最新版本踩坑强得多。我目前最常给新人推荐的是下面这套:
| 组件 | 推荐版本 | 理由 |
|---|---|---|
| JDK | JDK 8 或 JDK 11 | 生态最成熟,IDE和插件兼容性几乎没有坑 |
| Tomcat | 8.5 或 9.0 | 包名还是javax,网上大量教程直接可用 |
| IDEA | 2021及以上 | 社区版也支持JavaWeb基础开发 |
| MySQL | 5.7 或 8.0 | 8.0用得最多,注意驱动类名差异 |
| Maven | 3.6以上 | 依赖管理必须 |
有些资料让你用Tomcat 10.1配JDK 17,这组合本身没问题,但一旦遇到老项目的Servlet代码,你要同时处理包名迁移和模块化带来的访问权限问题,学习成本翻倍。等你把javax这套链路走通了,再切到jakarta其实就是全局替换的事。
还有一个容易忽略的点:IDEA的Tomcat配置面板里有个JRE选项,它决定Tomcat启动时用哪个JDK。如果你的项目编译版本是JDK 17,但这里选了JDK 8,启动时会报UnsupportedClassVersionError。这个选项通常藏在Run Configuration的Environment里,报这个错时先看它。
1.3 环境变量的正确设置方式
Windows下要让Tomcat正确启动,JAVA_HOME必须精确指向JDK的安装根目录,而不是C:\Program Files\Java\jdk-17\bin这种带bin的路径。JRE_HOME不需要单独设置,Tomcat会自动找JAVA_HOME。
设置完后,验证方式是在命令行执行:
echo %JAVA_HOME% java -version我见过有人配好了PATH里的java命令能识别,但JAVA_HOME是乱的,结果双击Tomcat的startup.bat窗口一闪而过。排查方法很简单,在startup.bat启动的黑色窗口消失前,右键编辑catalina.bat,在里面加一行pause命令,窗口就会停在报错信息上。
2. 新建JavaWeb项目的两条路线:IDEA原生工程与Maven war工程
2.1 路线一:IDEA原生JavaWeb工程添加Web骨架
如果你只是练手,不打算后续接Spring全家桶,可以直接在IDEA里新建一个普通Java项目,然后右键项目名,选择Add Framework Support,在弹出的窗口里勾选Web Application。IDEA会自动为你创建web目录,里面放着一个WEB-INF文件夹和web.xml配置文件。
这个方式最大的问题是依赖管理全靠手动。你下载了MySQL驱动jar包,必须右键jar包才能Add as Library,每次换机器都要重新来一遍,非常痛苦。我建议除非只是做五分钟的测试,否则不要用这种方式。
2.2 路线二:Maven构建war包工程(推荐)
Maven方式的标准项目结构长这样:
myweb/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ # 存放Servlet类、工具类 │ │ ├── resources/ # 存放配置文件 │ │ └── webapp/ # 存放HTML、JSP、web.xml创建时在IDEA的New Project里选择Maven,勾选Create from archetype,选择maven-archetype-webapp。这个原型会自动生成一个最简单的index.jsp和web.xml,打包方式是war。
pom.xml中最核心的依赖就三个:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> </dependencies>注意servlet-api的scope是provided。这意味着它只参与编译,不被打进war包。因为Tomcat自带了Servlet API,如果你强行把这个jar打进去,反而可能出现多个Servlet实现类冲突,报出一堆莫名其妙的NoSuchMethodError。这个细节我见过不少人栽过。
2.3 两种方式到底怎么选
我的判断标准很简单:只要你的项目以后可能会接数据库连接池、加过滤器、做登录验证,就直接Maven。Maven不仅帮你管理jar包,更重要的是它能统一项目的目录约定,让任何人在任何机器上打开项目都知道代码放哪、配置放哪、页面放哪。Maven默认这套目录结构,就是行业内最有共识的规矩。
还有一个好处:等你想学Spring Boot时,Maven的这些基础概念——依赖坐标、本地仓库、打包命令——全都能复用。你用IDEA原生Web项目学的那些,切换到Spring Boot后基本作废。
3. Servlet从写出来到跑起来:注解配置与URL映射
3.1 用@WebServlet还是web.xml?
有两种方式把Servlet暴露给Tomcat:一种是在类上标注@WebServlet("/hello"),另一种是在web.xml里配置servlet-mapping。
用注解的方式最简洁:
@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"); resp.getWriter().println("<h1>Hello JavaWeb</h1>"); } }访问URL就是http://localhost:8080/项目名/hello。注意,这个URL里的项目名是Tomcat部署后的上下文路径,后面部署部分我会专门说明。
那web.xml还需要吗?如果你用的是Servlet 3.0以上规范(Tomcat 8.5就是),可以不写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_4_0.xsd" version="4.0"> </web-app>但我要提醒你,很多老项目和网上资料还是xml配置方式,你在web.xml里看到一个servlet-name标签,要能看懂它和url-pattern是怎么对应的。即使平时用注解,这个阅读能力也要有。
3.2 URL映射:404心结的根源
代理配置中一个常见错误是访问路径写错。假设你的上下文路径是/myweb,一个@WebServlet("/hello")的Servlet完整路径应该是:
http://localhost:8080/myweb/hello少了myweb,或者多了一个斜杠,都会变成404。新手往往把IDEA运行窗口里的控制台输出当成参考,但Tomcat实际部署后的路径要以部署配置里Application context为准。
另一个容易忽略的坑是Servlet的URL映射不支持按后缀匹配多个斜杠。你写@WebServlet("/api/*")表示/api/下的所有请求都进这个Servlet;写@WebServlet("*.do")表示所有.do结尾的请求都进这个Servlet。这两种写法混在一起时,优先级规则是精确匹配优先于路径匹配,路径匹配优先于后缀匹配。这一块如果记不牢,调试的时候会走很多弯路。
4. 数据库接入:驱动、连接串、连接池的完整链路
4.1 驱动版本和类名是第一道坑
JavaWeb项目连MySQL,最土的写法就是JDBC。但JDBC的第一步Class.forName("com.mysql.jdbc.Driver")就埋了一个雷。
MySQL 5.x时代,驱动类名是com.mysql.jdbc.Driver;MySQL 8.0之后,驱动类名变成了com.mysql.cj.jdbc.Driver。你如果拿着MySQL 8的驱动,想着沿用旧习惯写老类名,运行时会直接报ClassNotFoundException: com.mysql.jdbc.Driver。
我建议的做法是:新项目统一用com.mysql.cj.jdbc.Driver。同时注意驱动版本的坐标,如果Maven仓库拉到的是mysql-connector-java8.0.x,对应的类名和URL格式都是新版风格。
4.2 连接串的每个参数都在解决一个具体问题
完整的MySQL 8连接串通常长这样:
String url = "jdbc:mysql://localhost:3306/testdb" + "?useUnicode=true" + "&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; String user = "root"; String password = "123456";拆开解释一下每个参数:
useUnicode=true&characterEncoding=utf8:保证中文能正常存进数据库。不设置的话,可能出现?问号乱码或者Incorrect string value报错。useSSL=false:本地开发环境不需要SSL加密,设置成true反而可能因为证书问题导致连接失败。serverTimezone=Asia/Shanghai:MySQL 8和驱动之间对时区的判断容易产生偏差,如果不设置,驱动会用JVM默认时区,经常报出The server time zone value 'Öйú±ê׼ʱ¼ä'的错误,这串乱码本质就是GBK和UTF-8解码错位显示的“中国标准时间”。allowPublicKeyRetrieval=true:MySQL 8默认身份认证插件是caching_sha2_password,JDBC首次连接时需要从服务器获取公钥。不开启这个参数会看到Public Key Retrieval is not allowed的报错。
4.3 代码里该写裸JDBC还是连接池?
初学阶段,你确实可以用DriverManager.getConnection()一条连接走天下。但一旦项目里要多表查询、要在一个请求里连续做几次数据库操作,每次都新建和销毁连接,性能会明显劣化,因为TCP握手和MySQL认证的消耗是实打实的。
我建议哪怕只是一个学习项目,也把连接池引进来。这里不推荐你手写连接池,直接用Druid或者HikariCP。以Druid为例,Maven依赖坐标是com.alibaba:druid:1.2.20,使用方式非常直接:
public class DbUtil { private static DruidDataSource dataSource; static { dataSource = new DruidDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxActive(10); } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } }注意:连接池的testOnBorrow参数和validationQuery是配套的。如果你只设置maxActive就以为万事大吉,数据库重启后连接池里的旧连接会全部失效,项目会间歇性报CommunicationsException。正确的做法是加上一行dataSource.setTestWhileIdle(true)和dataSource.setValidationQuery("SELECT 1"),让连接池定期探测连接有效性。这个经验是我在项目偶发断连时排查了很久才确定的。
4.4 配置文件与编码问题
把数据库连接信息硬编码在Java类里,不属于正常的工程实践。时间久了,换数据库IP、换密码都要重新编译。我用db.properties文件来解决:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456然后在代码里加载:
Properties props = new Properties(); try (InputStream in = DbUtil.class.getClassLoader().getResourceAsStream("db.properties")) { props.load(in); }这里有一个非常隐蔽的坑:getResourceAsStream返回的InputStream有可能是null,但不会立刻报NullPointerException,而是等到调用props.load(in)时才会炸。你排查的时候一看是NullPointerException,往往想不到是配置文件路径问题。所以加载配置时最好加一个判空:
if (in == null) { throw new RuntimeException("db.properties not found in classpath"); }配置文件编码也是问题多发区。IDEA的默认全局编码建议统一设成UTF-8,否则properties文件里写中文密码或注释时,运行时会读出一堆乱码,连接失败的原因让你无从查起。
5. 部署到独立Tomcat:war包、运行配置与目录权限
5.1 两种部署姿态:war包和war exploded
Tomcat部署JavaWeb项目有几种方式,但对IDEA用户来说,最常见的是在Artifacts里配置一个Web Application: Exploded(解压目录),然后通过Run Configuration的Tomcat Server指向这个Artifacts。
这和你手动把war包丢进Tomcat的webapps目录有什么区别?IDEA的本地Tomcat运行配置,会把项目编译后的class文件、webapp下的静态资源和依赖jar自动组装好,然后以Tomcat标准目录结构加载它。改完代码后,IDE能智能地把变化的class热替换进去,开发效率高很多。
但有个问题:很多人首次配置时,Artifacts的Output Layout里没有WEB-INF/lib这个目录,结果代码明明引用了其他jar,运行时就是报ClassNotFoundException。手动解决的方法是,在Available Elements里找到你要的jar,右键选择Put into WEB-INF/lib。这个动作非常容易漏,我每次带人都要重复一遍。
5.2 IDEA里配置外部Tomcat的完整步骤
假设你的Tomcat安装在D:\apache-tomcat-9.0.xxx(Windows)或/usr/local/apache-tomcat-9.0.xxx(macOS/Linux)。配置路径是:
- 打开
Run / Edit Configurations,左上角加号,选Tomcat Server -> Local。 Server选项卡里,Application server右侧的Configure按钮,指向你Tomcat的安装目录。- 切到
Deployment选项卡,点加号选Artifact,选中你的war exploded包,设置Application context为/myweb。 - 回到
Server选项卡,确认HTTP port是8080(或者你改过的端口)。
启动后IDEA会自动打开浏览器,你访问的URL末尾会自动拼上/myweb。
5.3 外部Tomcat的webapps部署方式
到了真正的部署环境(比如你买了一台云服务器,或者要在测试环境让同事访问),IDEA这种开发态部署就不适用了。此时的标准操作是:
在pom.xml里配置好<packaging>war</packaging>,执行Maven的package命令,在target目录下产生myweb.war,拷贝到Tomcat安装目录的webapps文件夹下,启动Tomcat。
Tomcat启动时自动完成解压,上下文路径就是war包文件名。如果你担心之前残留旧的解压目录,删除掉Webapps下同名目录再重启,避免增量残留。
cp target/myweb.war /path/to/tomcat/webapps/ bin/startup.sh访问http://服务器IP:8080/myweb/hello,如果服务器开放了对应端口和防火墙规则,外网就能打到了。这一步最常见的问题是:你在本地跑通了,但服务器上端口没开放,或Tomcat绑定了127.0.0.1而不是0.0.0.0。检查conf/server.xml里的Connector标签,address属性没注掉的话,外部怎么都访问不了。
5.4 一个被很多人忽略的目录权限细节
Tomcat安装在Windows的Program Files目录下,或者Linux的/opt目录下,启动脚本可能因为权限不足没法创建日志文件和临时目录。表现现象是:Tomcat进程起来了,但日志目录空空的,部署进去的项目也不解压。
排查思路很简单:确认当前运行Tomcat的用户对安装目录有读写权限,尤其是logs、work、temp三个目录。Linux下直接给Tomcat目录授权到位比纠结组权限配置更省时间:
chown -R 当前用户:当前用户组 /path/to/tomcat6. 部署后的高频报错:按这个顺序排查一次
6.1 端口被占用导致Tomcat起不来
双击startup.bat或点击IDEA运行,Tomcat窗口闪退,十有八九是8080端口被占用了。Windows下的排查命令:
netstat -ano | findstr 8080查到占用进程的PID后,再去任务管理器里结束它。如果你不想结束,就改Tomcat端口。在conf/server.xml里找到Connector节点,把port="8080"改成8081。这一步改完后,IDEA的HTTP port也要同步改,否则两边不一致。
6.2 HTTP 404还是500,分别怎么查
404是“资源没找到”,500是“服务器内部错误”。两个状态码指向的方向完全不同。
404的来源通常是:
- 上下文路径写错。你部署的是
/myweb,访问却写/myweb2/hello。 - Servlet映射写错。
@WebServlet里的值少了一个斜杠。 - war包没有解压成功。去Tomcat的
webapps目录看一眼,有没有对应的文件夹。 - 没部署任何artifact。IDEA的Deployment选项卡是空的,Tomcat起来后当然找不到项目。
500的来源通常是:
- 代码里抛了异常。去Tomcat的
logs目录翻catalina.out或localhost.log,按住Ctrl+F搜Exception。 - 依赖jar缺失。启动时加载类失败,最佳排查方式是看异常栈的
ClassNotFound和NoClassDefFoundError。
6.3 数据库连接失败的四大罪魁祸首
我整理一下最常遇到的数据库连接报错表格,方便你快速定位:
| 报错关键字 | 根因 | 解决方案 |
|---|---|---|
| Cannot create JDBC driver of class | 驱动类名配错 | 确认用com.mysql.cj.jdbc.Driver还是com.mysql.jdbc.Driver |
| Communications link failure | MySQL没启动,或主机名/端口不通 | systemctl status mysql,或在代码里把localhost暂时改成127.0.0.1验证 |
| Access denied for user | 用户名或密码错,或账号没有远程权限 | 在MySQL客户端里重新授权对应host |
| Public Key Retrieval is not allowed | MySQL 8的认证插件问题 | 在URL参数里加allowPublicKeyRetrieval=true |
6.4 中文乱码:从请求到响应的每一层都要统一
JSP页面、Servlet响应、数据库存储、Tomcat日志,每一层都可能出现中文乱码,但根源几乎都是编码不一致。我的建议是:
- IDEA全局编码设为UTF-8。
- 每个Servlet里设置
resp.setContentType("text/html;charset=utf-8")。 - JSP文件顶部写
<%@ page contentType="text/html;charset=UTF-8" language="java" %>。 - Tomcat的
conf/server.xml给Connector加上URIEncoding="UTF-8",这样GET请求的URL中文参数不会乱。 - MySQL表结构的
CHARACTER SET统一用utf8mb4,比utf8支持更完整的字符集(比如表情符号)。
到这里,一个JavaWeb项目从创建到跑的完整链路就通了。我自己带人的时候,最后总让他们把整个流程不带笔记地重做一遍,目的不是考验记忆力,而是要让“配置环境、建Maven工程、写Servlet、配数据库、部署war包”这一串动作变成肌肉记忆。项目不是你用它的时候才去查一遍,而是你要能在十分钟内把它从一个空目录变成能跑的web应用。这套基础链路打扎实了,后面学SSM、Spring Boot,你会发现在这些框架的自动化配置之下运行的,还是你今天亲手搭起来的这套规则。