Java Web数据库系统开发实战:从JDBC连接到事务控制的完整方案
简介这份资源面向Java Web与数据库开发的初学者及进阶学习者围绕Servlet、JSP、JDBC及MVC设计模式等核心技术结合SQL语言、事务处理与ORM框架讲解如何构建数据库驱动的Web应用并配有源码与教学视频供动手实践。压缩包共884个文件约222.5MB以java源码、jsp页面、class字节码、js脚本、jar依赖包、xml配置和sql脚本为主另含avi操作演示视频、gif与jpg素材及properties等配置文件覆盖从编码到部署运行的完整环节。已有471人学习下载。读者可通过源码对照视频理解Servlet生命周期、JSP内置对象、数据库连接与增删改查实现并借助系统演示与报表打印等录屏掌握项目部署与调试思路适合作为课程配套练习与项目参考。1. 从“能跑起来”到“能交出去”一套 Java Web 数据库系统到底要补哪些课很多同学做 Java Web 数据库课设卡住的地方不是不会写 Servlet而是把“能跑起来”当成了终点本地 Tomcat 一启动、页面能查出几条数据就以为完事了。真到验收或者答辩现场被问一句“你的连接池怎么配的、事务边界在哪、SQL 注入怎么防”立刻露馅。这套 Java Web 数据库系统应用开发与实例解决的正是从“跑通 Demo”到“交得出完整方案”之间的落差它把 JDBC 连接管理、DAO 分层、事务控制、分页查询、前后端数据流转这些必考模块串成一条完整链路配套源码和视频演示适合正在做数据库课设、Web 应用毕设或者需要给团队交付一套可维护 CRUD 系统的开发者。下面按需求、设计、实现、避坑、验收的顺序把这份资源拆成一份能照着走的方案。2. 需求与选型先把边界划清楚再谈技术栈2.1 这套系统要覆盖的真实业务范围做数据库系统课设最常见的翻车不是技术难而是范围失控。一开始想着“做个管理系统”结果用户、角色、权限、订单、报表全想塞进去最后每个模块都半成品。我一般会先把需求压到一条主线一个核心业务实体 一套完整的增删改查 一个关联查询 一个统计视图。比如图书管理里的“图书”是核心实体借阅记录是关联借阅统计是视图。这条主线跑通数据库设计、DAO 分层、事务、分页就全都有了落点答辩时也有话讲。这套实例的价值在于它没有停在单表 CRUD。单表增删改查谁都会写但真正体现数据库系统能力的是三件事多表关联查询怎么写才不产生笛卡尔积、分页在数据量上来后怎么不拖垮页面、事务在“借书扣库存 写借阅记录”这种组合操作里怎么保证一致性。需求边界就围绕这三点划至少两张有关联的表、至少一个跨表查询页面、至少一个涉及两条以上写操作的事务场景。低于这个标准方案撑不起“数据库系统”四个字高于这个标准课设周期内做不完。适用人群也要说清楚。学生拿它交课设或毕设重点看分层结构和事务处理这是老师必问的点从业者拿它做内部小工具或者给新人做练手项目重点看连接管理和 SQL 组织方式这套东西改改表名就能用。两类人关注点不同但底层那套 JDBC Servlet JSP 的骨架是共通的。2.2 为什么是 JDBC 直连而不是上来就上框架现在一提 Java Web很多人第一反应是 Spring Boot MyBatis。但数据库课设的评分点往往在“你懂不懂数据库访问的底层机制”而不是“你会不会配 starter”。这套实例走的是原生 JDBC Servlet JSP 路线不是技术落后而是教学场景下的合理选择连接怎么建、Statement 和 PreparedStatement 差在哪、事务的 commit 和 rollback 什么时候调、结果集怎么映射成对象这些在框架里被封装掉的东西恰恰是答辩时最能体现功底的部分。技术选型上JDK 8 及以上、Servlet 3.1、Tomcat 8/9、MySQL 5.7/8.0这套组合在大多数机房环境和学生笔记本上都能跑不需要额外装容器。前端用 JSP JSTL 而不是前后端分离理由是课设周期短JSP 直接渲染省掉一层接口联调把精力留给数据库设计。如果后续要升级成前后端分离DAO 层不用动只换控制层和视图层这也是分层带来的好处。提示选型不是越新越好而是和验收标准对齐。课设看的是数据库访问的规范性框架反而会掩盖细节。2.3 数据库设计先落表别急着写代码表结构没定就写 Java 代码是血泪经验里排前三的坑。字段类型反复改DAO 里的映射跟着改改到最后自己都不知道哪个版本是对的。正确顺序是先出 E-R 图再落物理表字段名、类型、约束一次定死后面代码只做映射不做设计。以图书借阅场景为例核心两张表加一张关联表-- 图书表核心业务实体 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, -- 唯一约束防重复录入 title VARCHAR(100) NOT NULL, author VARCHAR(50), stock INT NOT NULL DEFAULT 0, -- 库存借阅事务要扣减 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 借阅记录表关联查询和事务的落点 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, borrower VARCHAR(50) NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0, -- 0借出 1已还 CONSTRAINT fk_book FOREIGN KEY (book_id) REFERENCES book(id) );字段设计上有几个细节值得说。isbn加唯一约束是在数据库层兜住重复录入而不是靠 Java 代码先查再插——后者在并发下会失效。stock用整型而不是布尔“是否有货”因为借阅数量可能大于一扣减逻辑要能表达。borrow_record上的外键约束保证不会出现指向不存在图书的脏记录这在答辩时是个加分点说明你考虑了参照完整性。status用 TINYINT 而不是字符串查询和索引效率更好也避免“已还/已归还/returned”这种值不统一的问题。索引方面borrow_record.book_id上建索引因为按图书查借阅记录是高频操作borrow_record.borrower如果要做“某人借了哪些书”的查询也建议加。索引不是越多越好写操作多的表加太多索引会拖慢插入这个权衡在方案里要能讲出来。3. 总体架构设计三层结构与四个模块的职责划分3.1 分层不是形式是让每层只操心一件事这套系统的骨架是经典三层表现层JSP Servlet、业务层Service、数据访问层DAO底下是 MySQL。分层的目的不是画图好看而是让改动有边界。换数据库只动 DAO改业务规则只动 Service调页面只动 JSP互不牵连。课设里最常见的反模式是把 SQL 直接写在 JSP 里页面一复杂就没法维护答辩时被问“你的业务逻辑在哪”直接答不上来。四个模块的职责这样划图书管理模块负责图书的增删改查和库存维护借阅管理模块负责借书、还书和借阅记录查询是事务的主要发生地用户/借阅人模块做简单的信息维护统计模块出一个“按图书分类统计借阅次数”或者“库存预警列表”的视图。四个模块里借阅模块是重点因为它同时涉及多表操作和事务其他三个模块相对独立可以并行开发。数据流向上请求从浏览器到 ServletServlet 做参数校验和流程编排调 ServiceService 组织业务规则并控制事务调 DAODAO 用 JDBC 执行 SQL 并映射结果原路返回。这条链路要能在答辩时口述出来比背概念有用得多。3.2 包结构与关键类清单包结构按层划分一眼能看出职责com.example.web ├── controller // Servlet接收请求、参数校验、跳转 │ ├── BookServlet │ └── BorrowServlet ├── service // 业务规则、事务边界 │ ├── BookService │ └── BorrowService ├── dao // SQL 执行、结果映射 │ ├── BookDao │ └── BorrowDao ├── model // 实体类与表字段对应 │ ├── Book │ └── BorrowRecord └── util // 连接管理、工具类 └── DBUtil关键类里DBUtil负责连接的获取和释放是整个系统稳定性的地基BorrowService里的borrowBook方法是事务的核心要在一个连接上完成“扣库存”和“写记录”两步BookDao里的分页查询方法体现 SQL 组织能力。这几个类是答辩时最容易被追问的代码要能讲清楚每一行为什么这么写。3.3 连接管理别让每次请求都新建连接数据库连接是稀缺资源每次请求都DriverManager.getConnection再关掉在并发稍高时会把数据库连接数打满页面直接卡死。这套实例的做法是把连接获取收敛到DBUtil用连接池管理。课设环境用 Druid 或者简单的池化实现都行关键是不要在 Servlet 里裸写连接创建。public class DBUtil { private static DataSource dataSource; static { // 初始化连接池参数从配置文件读取避免硬编码 DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大连接数按并发量调 ds.setMaxWait(3000); // 获取连接超时防止无限等待 dataSource ds; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源顺序与打开相反 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { /* 记录日志 */ } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { /* 记录日志 */ } try { if (conn ! null) conn.close(); } catch (SQLException e) { /* 记录日志 */ } } }initialSize和maxActive是最常被问的参数。初始连接数太小冷启动时第一批请求会等最大连接数超过数据库的max_connections连接池会报获取超时。一般课设环境maxActive设 20 足够生产环境要按实际并发和数据库承载能力算。maxWait设 3 秒是为了在连接耗尽时快速失败而不是无限挂起这个参数在排查“页面一直转圈”时特别有用。注意连接池的关闭方法里conn.close()在池化环境下是归还连接而不是真正关闭这个语义差异要理解清楚否则会误以为连接泄漏。4. 核心模块实现从 DAO 到事务的完整链路4.1 DAO 层PreparedStatement 与结果映射DAO 层的两个硬要求一律用PreparedStatement一律做结果集到对象的映射。前者防 SQL 注入后者让上层不碰ResultSet。拼接 SQL 字符串是课设里最容易被扣分的写法答辩时老师随手输个 OR 11就能演示注入场面很难看。public class BookDao { // 分页查询用 LIMIT 做物理分页避免全表查出来再截取 public ListBook findByPage(int page, int pageSize) throws SQLException { String sql SELECT id, isbn, title, author, stock FROM book ORDER BY id LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); // 偏移量 ps.setInt(2, pageSize); // 每页条数 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setIsbn(rs.getString(isbn)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setStock(rs.getInt(stock)); list.add(b); } } } return list; } // 扣减库存带条件更新库存不足时影响行数为 0 public int decreaseStock(Connection conn, int bookId) throws SQLException { String sql UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bookId); return ps.executeUpdate(); // 返回受影响行数用于判断是否成功 } } }分页这里用LIMIT ?, ?而不是在 Java 里subList是因为数据量大了以后全表查出来再截取会吃掉大量内存物理分页让数据库只返回当前页。decreaseStock的 SQL 里带AND stock 0是防超卖的关键库存为 0 时更新影响行数为 0业务层据此判断借阅失败而不是先查库存再更新——后者在并发下会有竞态。这个方法接收外部传入的Connection是为了让它能参与上层的事务这个设计在下一节会用到。4.2 事务控制一个连接两步操作要么全成要么全败借书这个动作包含两步写操作扣库存、写借阅记录。如果扣了库存但写记录失败库存就白白少了如果写了记录但扣库存失败记录就是脏的。事务就是解决这个的。JDBC 里事务的边界在Connection上所以两步操作必须用同一个连接且关闭自动提交。public class BorrowService { public boolean borrowBook(int bookId, String borrower) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务关闭自动提交 BookDao bookDao new BookDao(); int affected bookDao.decreaseStock(conn, bookId); if (affected 0) { conn.rollback(); // 库存不足回滚 return false; } BorrowDao borrowDao new BorrowDao(); borrowDao.insertRecord(conn, bookId, borrower); conn.commit(); // 两步都成功才提交 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { /* 记录日志 */ } } return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { /* 记录日志 */ } } } } }这段代码有几个必须讲清楚的点。setAutoCommit(false)是事务的开关忘了这行后面的commit和rollback都是摆设。rollback在库存不足和异常两个分支都要调前者是业务回滚后者是故障回滚。finally里恢复setAutoCommit(true)再归还连接是因为连接池里的连接会被复用不恢复自动提交状态会污染下一个使用者——这个坑很隐蔽现象是“偶尔有请求的事务不生效”排查起来很费劲。提示事务里的所有 DAO 方法都要接收同一个Connection参数不能各自去DBUtil.getConnection()否则就是多个连接、多个事务回滚只回滚了一个。4.3 控制层与视图参数校验和分页展示Servlet 的职责是薄薄一层收参数、做基础校验、调 Service、把结果放进 request 域、转发到 JSP。业务判断不要写在 Servlet 里否则分层就白做了。WebServlet(/book/list) public class BookListServlet extends HttpServlet { private final BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int pageSize 10; try { String p req.getParameter(page); if (p ! null !p.isEmpty()) page Integer.parseInt(p); if (page 1) page 1; // 防非法页码 } catch (NumberFormatException e) { page 1; // 参数非法时回退默认值不抛 500 } PageResultBook result bookService.queryByPage(page, pageSize); req.setAttribute(pageResult, result); req.getRequestDispatcher(/WEB-INF/views/book/list.jsp).forward(req, resp); } }参数校验的重点是“非法输入不崩”。page传个abc或者负数如果直接parseInt不捕获页面就是 500 错误验收时演示到这个就尴尬了。回退默认值是课设场景下最务实的处理。JSP 放在WEB-INF下是为了禁止浏览器直接访问视图强制走 Servlet这也是个规范细节。分页展示时 JSP 里用 JSTL 遍历pageResult的列表页码导航根据总页数生成总页数由count(*)除以pageSize向上取整得到这个计算放在 Service 里别放 JSP。5. 避坑与排查五个最容易翻车的点5.1 中文乱码现象是页面问号根因在三个地方现象表单提交的中文存进数据库变成???或者查询出来的中文显示乱码。原因通常有三处没统一数据库表的字符集、JDBC 连接串的编码参数、请求和响应的编码。解决方法是三层一起查建库建表时指定utf8mb4连接串带characterEncodingutf8Servlet 里在读取参数前设置request.setCharacterEncoding(UTF-8)响应设置response.setContentType(text/html;charsetUTF-8)。只改一处往往不够因为数据在“浏览器→请求→JDBC→数据库”这条链上每一环都可能转码。5.2 连接泄漏现象是跑一阵就卡死根因是异常路径没关资源现象系统刚启动正常点几十次之后页面越来越慢最后报“获取连接超时”。原因是某个 DAO 方法在异常时跳过了close连接没归还池子。解决办法是用 try-with-resources 管理Connection、PreparedStatement、ResultSet或者在finally里按相反顺序关闭。排查时可以临时把连接池的maxActive调小问题会更快复现再用日志打出每次获取和归还定位到具体方法。5.3 事务不生效现象是回滚了但数据还在根因是连接不是同一个现象借书时故意让第二步抛异常结果库存还是被扣了。原因是两个 DAO 方法各自getConnection跑在两个连接上rollback只回滚了其中一个。解决办法是把Connection作为参数从 Service 传到 DAO全程一个连接。这个坑的隐蔽之处在于代码看起来“有事务”但实际没生效排查时在commit和rollback处打日志确认是不是同一个连接对象。5.4 分页总数算错现象是最后一页多出空白或漏数据现象总共 25 条数据每页 10 条页面显示 3 页但第 3 页只有 5 条却渲染了 10 个空行或者显示 2 页漏了 5 条。原因是总页数用了整数除法25/102没向上取整。解决办法是(total pageSize - 1) / pageSize或者用Math.ceil。这个错误不影响功能但很影响观感验收时一眼就能看出来。5.5 SQL 注入演示翻车现象是登录框输特殊字符就绕过现象登录或者查询框输入 OR 11直接返回全部数据。原因是用了字符串拼接 SQL。解决办法是全部换成PreparedStatement的占位符参数用setXxx传入。这个点在答辩时经常被主动测试提前自查一遍所有 DAO 方法确认没有SELECT ... WHERE name name 这种写法。6. 验收与答辩怎么把这份方案讲成自己的验收不是把系统跑一遍就完事而是要让评审相信你理解每个设计决策。我一般会准备一条“演示主线”从图书列表分页开始展示分页参数怎么传、总数怎么算然后借一本书展示库存扣减和借阅记录同时出现再故意借一本库存为 0 的书展示事务回滚、库存不变最后在查询框输入注入字符串展示PreparedStatement挡住了。这条主线五分钟走完覆盖了分页、事务、安全三个核心点比漫无目的地翻页面有效得多。答辩提问高频集中在几个方向提前准备好答案可能的提问回答要点为什么用 JDBC 不用框架课设考察数据库访问底层机制连接、事务、结果映射要能讲清楚事务边界怎么定的在 Service 层一个业务动作一个事务连接从 Service 传到 DAO连接池参数怎么定的按并发量和数据库承载能力maxActive不超过数据库max_connections分页为什么用 LIMIT物理分页减少数据传输和内存占用数据量大时优势明显怎么防 SQL 注入全部用 PreparedStatement 占位符不拼接用户输入代码讲解时挑BorrowService.borrowBook这一个方法讲透比每个类都念一遍强。讲的时候按“为什么关自动提交→为什么判断影响行数→为什么两个分支都回滚→为什么 finally 恢复状态”的顺序每一句都对应一个具体问题评审能听出你是真写过而不是背的。还有个小技巧提前把数据库脚本、建表语句、初始化数据整理成一个init.sql验收时现场重新建库跑一遍证明系统不依赖你本地那套“玄学环境”。这一步能挡掉“在我机器上是好的”这类质疑。从那以后我每次带人做这类课设都强制要求先写init.sql再写一行 Java 代码环境可复现是交付的底线。希望这套拆解能帮到你把手里那份源码真正变成能讲、能改、能验收的方案。本文还有配套的精品资源点击获取