我之前带过好几个实习生,发现一个特别有意思的现象:大家学JavaWeb的时候,视频看了、笔记抄了,但真到自己用IDEA新建一个项目、连上MySQL、跑通一个完整案例,往往要折腾好几天。尤其是搜"idea运行javaweb项目配置"、"javaweb项目完整案例mysql"这些关键词的同学,大概率是卡在了某个配置环节,或者照着教程做却始终跑不起来。
这篇文章是我整理的第05期JavaWeb实战笔记,核心就三件事:IDEA里JavaWeb项目的正确运行方式、MySQL接入时那些容易踩的坑、以及一个能完整跑通的用户管理模块案例。适合正在学JavaWeb的初学者,也适合那些看完了黑马视频但动手能力还欠火候的同学。我会把教程里没细讲、但实操中必须知道的细节全部摊开来说,包括为什么要这么配、报错了怎么一步步排查。看完你至少能独立把一个JavaWeb项目从零搭起来跑通。
1. 动手前先理清JavaWeb项目的骨架:目录、依赖和运行链路
很多同学一上来就跟着教程点鼠标,结果项目长什么样、文件为什么放那个位置、Tomcat到底在中间扮演什么角色,完全没有概念。这样一旦脱离教程,换个版本、改个目录,立刻抓瞎。我建议你先把JavaWeb项目的"骨架"搞清楚,再谈运行。
1.1 一个JavaWeb项目由哪些部分组成
标准的JavaWeb项目,从IDEA里看大致分这么几块:
- src目录:存放Java源码,通常按包名分层,比如com.example.dao、com.example.service、com.example.servlet
- web目录(旧版叫WebContent):存放JSP、HTML、CSS、JS等前端资源
- WEB-INF目录:这是核心,web.xml在这里,classes(编译后的class文件)也在这里,lib(第三方jar包)也在这里
- pom.xml或build.gradle(如果用Maven/Gradle管理):声明项目依赖
这里有个新手经常搞混的点:WEB-INF下面的东西是浏览器直接访问不到的。JSP页面如果放在WEB-INF里面,必须通过Servlet转发才能访问,直接输入URL是404。这个设计是为了安全,但很多第一次接触的人会以为是自己路径写错了。
如果你用的是Maven项目,目录结构又会多一层src/main/java、src/main/resources、src/main/webapp。本质上跟上面是一回事,只是Maven帮你自动管理了依赖和构建流程。我强烈建议你从Maven开始学,因为以后工作里基本都是Maven或Gradle,手工导jar包的时代已经过去了。
1.2 从URL到数据库:一次请求的完整旅行
理解一次请求是怎么走的,比背一百个API都有用。假设你在浏览器输入了一个地址,比如http://localhost:8080/user/list,背后发生了什么:
- 浏览器发出HTTP请求到Tomcat(端口8080)
- Tomcat根据请求的路径找到对应的Servlet(通过web.xml或注解映射)
- Servlet调用Service层处理业务逻辑
- Service层调用Dao层访问数据库(通过JDBC或MyBatis)
- Dao层从MySQL查出数据,逐层返回给Servlet
- Servlet把数据放进request域,转发到JSP页面渲染成HTML
- Tomcat把HTML响应给浏览器
整个过程里,Tomcat是Web服务器,也是Servlet容器。它负责监听端口、接收请求、创建Servlet实例、管理Servlet生命周期。很多人把Tomcat当成"部署工具",其实它更准确的定位是"Java Web应用的运行环境"。没有Tomcat,你的Servlet代码就只是一堆.class文件,没法对外提供服务。
1.3 为什么新手总在"跑不起来"上卡住
我总结了一下,卡住的根本原因通常是这三类:
一是环境割裂。IDEA、JDK、Tomcat、MySQL各自安装,但版本之间的兼容性没人告诉你。比如JDK17和Tomcat9、Tomcat10的兼容性就不同,Tomcat10把javax.servlet换成了jakarta.servlet,旧代码复制进去直接报ClassNotFoundException。
二是缺依赖。写Servlet、连数据库都需要第三方jar包。用了Maven没配好依赖,没用Maven又漏了jar包,导致运行时ClassNotFound。
三是不懂报错。看到一屏红色日志就慌,其实很多报错只需要看最上面几行"Caused by"就能定位。
所以接下来的内容,我会围绕这三个痛点,把配置和排查讲透。
2. IDEA运行JavaWeb项目:必踩的配置细节与正确姿势
IDEA里运行JavaWeb项目,本质上是把Web应用部署到Tomcat上。这个操作不算难,但配置项多,而且IDEA的界面版本之间有差异,导致很多人照着截图都找不到按钮。我以IntelliJ IDEA 2023版本为例,讲几个最关键的点。
2.1 Tomcat集成:别把Artifact选错
在IDEA里添加Tomcat运行配置时,有一个叫Deployment的选项卡,新手最爱在这里翻车。你需要先把项目打成Artifact(通常是war exploded格式),然后把这个Artifact加到Tomcat的Deployment列表里。
这里面有两个坑:
第一,war和war exploded的区别。war是把项目打包成一个war文件再部署,适合最终上线;war exploded是把项目解压后的目录直接作为部署目录,IDEA里调试基本都用这个,改了代码后可以热部署,不用反复重启Tomcat。
第二,Application context的路径。你填的/还是/user,直接决定你访问URL的前缀。如果填的是/,访问地址是http://localhost:8080/xxx;如果填/myapp,就要http://localhost:8080/myapp/xxx。这个值会被写进Tomcat的conf目录下的配置文件,如果改了没生效,大概率是IDEA缓存了旧配置,试试clean再rebuild。
完整的操作是这样:打开Run -> Edit Configurations -> 点左上角+号 -> 找到Tomcat Server -> Local -> 在Server选项卡里选好Tomcat安装目录(关键是选到Tomcat的根目录,不是bin目录)-> 切到Deployment选项卡 -> 点+号 -> Artifact -> 选择你项目的war exploded -> Application context填/ -> Apply。
2.2 项目结构里的lib和Facet:IDEA隐藏的坑
很多非Maven项目,jar包都是放在WEB-INF/lib目录下的。但光放进去还不够,IDEA必须知道这些jar包在编译和运行时都能被找到。做法是:File -> Project Structure -> Libraries -> +号 -> Java -> 选中lib目录。这样IDEA就会把整个lib目录下的jar包都加入classpath。
还有一个很多人忽略的东西叫Facet。Project Structure里,Facets面板需要给项目添加Web类型,并且指定Web资源目录(就是webapp或web目录)、Web部署描述符(web.xml)的位置。否则你在web.xml里配置的Servlet映射,IDEA根本不会识别。
我遇到过最典型的情况:代码看着没问题,web.xml也没问题,但一启动Tomcat就报404。最后发现是IDEA的Facet里Web资源目录路径配错了,它指向了一个不存在的目录。所以新建完项目,先检查Project Structure里的两个地方:Modules是否识别了src为源码目录,Facets是否识别了Web资源目录。
2.3 热部署与日志查看技巧
调试的时候我最烦反复重启Tomcat。IDEA里Tomcat配置有两个跟热部署有关的选项:
- On 'Update' action:当代码更新时触发什么操作,建议选Update classes and resources
- On frame deactivation:当IDEA失去焦点(比如你切到浏览器)时自动更新,这个我一般也勾上
这两个配合起来,改一下JSP或者Java方法体,切回浏览器刷新就能看到效果。但要注意,如果改动的是方法签名、新增了Servlet类、改了web.xml,还是需要重启Tomcat的。热部署不是万能的,它只是帮你在不重启的情况下替换掉改动的class文件和静态资源。
日志查看也有讲究。Tomcat的控制台日志在IDEA的Run窗口里能看到,但如果项目部署到独立Tomcat,就需要看Tomcat安装目录下的logs文件夹,重点是catalina.out(或catalina.<日期>.log)和localhost.<日期>.log。前者记录启动和全局异常,后者记录Web应用的访问细节。如果你部署了项目但Tomcat启动后没报错却访问不到,多半要看localhost日志,那里会有"Deployment of web application ... has finished in xxx ms"之类的信息,以及上下文路径的注册记录。
3. 接入MySQL:连接池、JDBC和SQL注入的三个层次
JavaWeb项目十有八九要接数据库。这一块我想分三个层次讲:最基础的JDBC连接、工程上必须用的连接池、以及安全性层面的SQL注入问题。这三个层次代表了三类不同水平的代码,你可以对照一下自己写到了哪一步。
3.1 从DriverManager到连接池:为什么不能用最简写法
教科书里最常见的JDBC写法是:
Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai"; String user = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, user, password); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from t_user");这段代码能跑通,但绝对不能用在真实项目里。原因有两个:
一是每次请求都创建物理连接,而MySQL建立连接需要TCP握手、权限验证、SSL协商,代价很高。高并发场景下,几百个请求就能把数据库连接数打满。
二是根本没有资源释放逻辑。Connection、Statement、ResultSet如果不在finally里关闭,连接会泄漏。MySQL默认的max_connections是151,泄漏几十次就再也连不上了,必须重启数据库才能恢复。
工程上的做法是使用连接池,比如HikariCP或Druid。连接池的思想很朴素:预先创建一批连接放在池子里,用的时候借出来,用完了还回去,而不是扔掉。HikariCP在SpringBoot里是默认选择,性能很好;Druid是阿里开源,自带监控页面,国内中小项目用得很多。
使用Druid时,配置一把梭:
DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(user); dataSource.setPassword(password); dataSource.setInitialSize(5); dataSource.setMinIdle(5); dataSource.setMaxActive(20);关键参数的含义:initialSize是启动时创建多少个连接;maxActive是最大活跃连接数;maxWait是获取连接的超时时间,超过这个时间拿不到连接就报错。如果你发现系统卡在获取连接上,先看看是不是maxActive设小了,或者有连接泄漏没归还。
3.2 表结构设计:一个用户管理模块的最低要求
不管多复杂的案例,只要是"用户管理",核心表基本上就一张用户表。我的设计习惯是:
CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '密码(加密后)', `email` VARCHAR(100) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT '1' COMMENT '1启用 0禁用', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';几个容易被忽视的点:
- 字符集一定要用utf8mb4,不是utf8。utf8在MySQL里是utf8mb3的别名,最多支持3字节,像emoji表情和部分生僻字会存不进去。utf8mb4才是完整的UTF-8。
- password字段不要用char(32)存MD5,现代项目至少用SHA-256加盐,或者直接用BCrypt。
- create_time和update_time写成DEFAULT CURRENT_TIMESTAMP,可以在插入和更新时自动维护时间,少写不少Java代码。
- 逻辑删除(deleted字段)的话,查询条件里别忘带
where deleted = 0,否则你会查出"已经删除"的数据。
3.3 PreparedStatement为什么是底线
很多人写SQL拼接是这么写的:
String sql = "select * from t_user where username = '" + username + "' and password = '" + password + "'";这段代码如果被用户输入admin' or '1'='1,拼接出来的SQL就变成了:
select * from t_user where username = 'admin' or '1'='1' and password = 'xxx'因为or '1'='1'恒成立,整条查询无条件通过,这叫万能密码注入。更严重的是,攻击者可以用'; drop table t_user; --来删表。
解决方案是PreparedStatement,预编译占位符:
String sql = "select * from t_user where username = ? and password = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery();PreparedStatement为什么能防注入?因为参数在预编译阶段就被当作纯数据传给数据库,数据库只把它当成字符串值,永远不会被解析成SQL关键字或表达式。这也是为什么我在代码审查时,只要看到字符串拼接SQL,几乎都会打回去。
4. 完整案例复盘:用户管理模块从建表到上线验证
前面讲了很多理论,这一节我带你完整走一个案例。这个案例很典型,就是用户管理模块的增删改查,加一个登录验证。它是JavaWeb项目完整案例里出现频率最高的场景,你在搜"javaweb项目完整案例mysql"时看到的十有八九是这个。
4.1 需求与页面原型
我们的需求很简单:
- 用户输入用户名和密码,点击登录,验证通过后跳转到用户列表页
- 用户列表页展示所有用户,支持按用户名模糊搜索、删除用户、跳转到编辑页
- 新增/编辑用户共用同一个表单页,提交后保存到数据库
- 退出登录
为什么选这个案例?因为它麻雀虽小五脏俱全:Servlet映射、请求转发、重定向、JDBC增删改查、列表循环渲染、条件查询,JavaWeb的核心知识点全用上了,而且逻辑不绕,适合用来打通"前端 -> Servlet -> Service -> Dao -> MySQL"的完整链路。
页面我直接用JSP+JSTL+EL来写,不用前端框架。原因很现实:JavaWeb阶段重点在后端,JSP烂熟于心,后面学SpringMVC和Vue时会轻松很多。JSP里如果对EL表达式和C标签不熟,很容易在渲染时踩坑。
4.2 后端分层:Servlet、Service、Dao怎么划分
很多同学写代码喜欢把逻辑全塞在Servlet里,一个方法搞定查询和数据库操作,页面上确实能跑,但做完这个案例你就会感觉越写越乱。我建议从第一次写就养成三层结构。
先建包:
com.example.user.entity -- 实体类 User com.example.user.dao -- 数据库访问层:UserDao com.example.user.service -- 业务逻辑层:UserService com.example.user.servlet -- 控制层:LoginServlet、UserListServlet、UserEditServlet、UserDeleteServlet分层有个关键原则在Service层体现得最清楚:事务控制的边界。比如删除用户的同时需要写入日志表,如果其中一步失败,应该整体回滚。事务必须放在Service层统一管理,放在Dao层的话,每个Dao方法各开各的事务,无法保证原子性。
以查询用户列表为例,UserDao里写一个方法:
public List<User> findByUsername(String username) { String sql = "select id, username, email, phone, status, create_time from t_user where username like ?"; try (Connection conn = DbUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, "%" + username + "%"); try (ResultSet rs = pstmt.executeQuery()) { List<User> list = new ArrayList<>(); while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); // ... list.add(user); } return list; } } catch (SQLException e) { throw new RuntimeException("查询用户失败", e); } }注意这里的try-with-resources写法,Java7及以上版本可以用,它会在代码块结束后自动关闭Connection、Statement、ResultSet,比在finally里手动关简洁多了,而且不会漏。
Servlet这边只负责三件事:接收请求参数、调用Service、决定跳转到哪个页面。不要写SQL,不要写业务判断。
@WebServlet("/user/list") public class UserListServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); if (username == null) { username = ""; } List<User> userList = userService.findByUsername(username); req.setAttribute("userList", userList); req.setAttribute("searchUsername", username); req.getRequestDispatcher("/WEB-INF/jsp/userList.jsp").forward(req, resp); } }这里有个细节:搜不到结果时username可能是null,如果直接把null传给Service,SQL会变成where username like %null%,查不出来任何东西。所以先做空值处理。类似这种小坑,你多写几个案例就记住了。
4.3 联调测试:使用Postman和浏览器验证接口
项目跑起来之后,不要一上来就点页面。我习惯分三步验证:
第一步,验证数据库连接。写一个最简单的Servlet,或者直接跑一段JDBC代码,能查出数据说明环境和连接串没问题。
第二步,用Postman直接测Servlet接口。比如访问http://localhost:8080/user/list?username=admin,服务端日志里能看到SQL执行,浏览器或Postman里能看到JSP渲染后的HTML。这一步能帮你把控制层和数据层的问题分开:如果Postman返回200而且内容正确,说明后端逻辑OK,剩下的只是页面展示的问题。
第三步,在浏览器里完整走一遍用户流程:新增 -> 列表看到新用户 -> 搜索 -> 编辑 -> 删除。这一步主要检验Cookie/Session和跳转逻辑。
还有一个联调时特别容易踩的坑:表单提交方式。我见过很多人把新增功能的form设成method="post",Servlet里却只实现了doGet方法,结果提交报405。要记住:URL直接访问、超链接跳转是GET;表单提交、Ajax的POST是POST。Servlet的doGet和doPost都要想清楚用哪个,或者统一在service方法里处理。
我常用的做法是在Servlet里这样写:
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String method = req.getMethod(); if ("POST".equalsIgnoreCase(method)) { doPost(req, resp); } else if ("GET".equalsIgnoreCase(method)) { doGet(req, resp); } }这样无论表单GET还是POST,都能进到统一的处理逻辑里。当然,生产环境建议还是严格区分,因为GET请求的参数会被记录在日志和浏览器历史里,不适合传敏感信息。
5. 运行中的异常排查:黑马笔记里没细说的实战经验
项目能跑通不算完,真正考验你水平的是出问题了怎么排查。这一节我把JavaWeb最常见的三类异常,按完整排查链路写出来,你看看有没有命中过。
5.1 404、500、ClassNotFoundException的排查顺序
Web应用报错,先分大类:
404:请求的地址在服务器上不存在。按顺序排查:URL路径里的Application context有没有写对(比如之前配了/myapp但URL忘记带);web.xml或@WebServlet里映射的路径是否和URL一致;Servlet类是否编译后被放到了WEB-INF/classes对应的包目录下;项目是否成功部署到了Tomcat。
500:服务端代码运行时出错了。此时IDEA控制台或Tomcat日志里一定有堆栈信息。先看最底下的"Caused by",那是根本原因。常见的有:NullPointerException(某个对象是null,常见于参数没传到Servlet)、ClassNotFoundException(jar包缺失,检查依赖)、NoSuchMethodError(jar包版本冲突)。
启动时就报错:那就不是你的代码问题了,大概率是Tomcat配置坏了,或者项目里的web.xml写错。看启动日志里第一次出现ERROR的位置。
我处理项目报错有一个固定的排查顺序:先看部署有没有成功,再看URL映射对不对,最后才看代码逻辑。很多时候部署这一步就错了,后面全白搭。
怎么确认部署成功?看Tomcat启动日志,找一行Deployment of web application archive [xxx.war] has finished。如果没看到,看error级别日志,比如Failed to deploy。
5.2 数据库连接失败:从时区到权限一网打尽
数据库连不上,报错通常是Communications link failure或Access denied for user。前者是网络/连接层面的问题,后者是账号权限问题。
先看URL写法。MySQL 8.x要求驱动是com.mysql.cj.jdbc.Driver,URL里必须带时区参数,比如serverTimezone=Asia/Shanghai。如果你代码里写的是com.mysql.jdbc.Driver,在MySQL 8.x下会直接报错,因为那个旧驱动已经移除了。
再看MySQL服务有没有启动。Windows下你在任务管理器看看MySQL服务;Linux下systemctl status mysqld。最朴实的方法是命令行直接连一下mysql -u root -p,能连上说明数据库正常,问题出在Java代码里。
Access denied for user的话,按这个思路查:
-- 查看所有用户和host SELECT user, host, authentication_string FROM mysql.user; -- 给指定host授权('%'代表所有主机) GRANT ALL PRIVILEGES ON javaweb_demo.* TO 'root'@'%' IDENTIFIED BY '123456'; FLUSH PRIVILEGES;还有一个很隐蔽的坑:数据库连接串里的端口写错了。MySQL默认3306,但你在服务器上可能装了Docker版MySQL,端口映射成了33061,这时候Java代码连3306自然连不上。
5.3 字符编码问题:一个看不见的坑
页面显示乱码、插入数据库中文变问号,这两类问题折磨了无数新手。根源是编码在四个环节里不一致:客户端页面 -> HTTP请求 -> Java字符串 -> 数据库存储。
解决方案要四管齐下:
- JSP页面顶部加
<%@ page contentType="text/html;charset=UTF-8" language="java" %> - Servlet里在读取参数前执行
req.setCharacterEncoding("UTF-8"),设置响应编码用resp.setContentType("text/html;charset=UTF-8") - 数据库连接URL加
useUnicode=true&characterEncoding=utf8 - 建表时指定CHARSET=utf8mb4
这四个环节只要有一个不一致,就可能出乱码。我之前的经验:页面乱码,多半是JSP或Servlet响应编码问题;数据库里变问号,多半是连接串或建表字符集问题;提交的参数在Servlet里就是乱码,那大概率是req.setCharacterEncoding("UTF-8")这行没写,或者顺序错了(必须在getParameter之前调用)。
顺便提一句,Tomcat 8及以后版本,POST请求的默认编码才是UTF-8,GET请求的编码取决于Tomcat的URIEncoding配置。如果GET请求中文乱码,可以在Tomcat的conf/server.xml里给Connector加上URIEncoding="UTF-8"。这是很多人查了半天查不出来的原因。
6. 我踩过几次坑之后的几点体会
文章写到这里,该讲的都讲了。最后分享几个我个人在反复练习JavaWeb项目时总结的体会,不一定都在书本上,但确实实用。
第一,做项目别追求"大而全"。网上随便搜一下就有号称几十万行代码的案例,但那种项目你根本看不完,也消化不了。我建议就做用户管理这种经典模块,做完一个,再把分页、过滤器、文件上传、AJAX、Redis会话共享一个个往里加,每加一个功能,你的知识树就长出一根新枝。
第二,遇到报错先读日志,不要急着问人。很多人一看到Exception就复制粘贴到搜索引擎,但你自己能读日志的话,往往花两分钟就能定位。记住一个原则:堆栈信息从下往上读,先看Caused by,再看是本项目的哪一行代码抛出来的。排查过程本身就是最有价值的练习。
第三,配置类的坑,解决一次就够了。像IDEA运行配置、Tomcat乱码、MySQL时区,这些属于一次配置永久受益的内容。建议把你遇到的所有坑记录下来,包括报错信息、原因、解决办法。下次再遇到类似问题,翻自己的笔记比重新搜索快得多。我电脑里就有一份自己积累的JavaWeb踩坑记录.md,已经写了几百条,现在遇到多数问题都能在五分钟内解决。
第四,别忽略基础工具。有些同学连curl、Postman都不太用,全程靠浏览器点。其实这些工具在排查问题时非常好用——用curl可以模拟任意请求头,用Postman可以保存各种接口测试。花一个小时学会它们,后面能省几十个小时。
JavaWeb是整个Java后端开发的地基,地基不牢,后面学Spring、SpringBoot、微服务都会很虚。希望这篇笔记能帮你把"IDEA运行配置+MySQL接入+完整案例"这条线打通。等你把这个用户管理模块跑通了,再回头看那些热搜词里的问题,你会发现其实没当初想的那么难。