ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零构建JavaWeb图书管理系统:Servlet+JSP+JDBC全链路实战解析

从零构建JavaWeb图书管理系统:Servlet+JSP+JDBC全链路实战解析 简介一份基于Java与MySQL的图书管理系统项目面向初学Java Web开发的读者适合作为课程设计或自学实战的完整参考。系统采用Servlet、JSP配合MVC设计模式构建后端通过JDBC与mysql-connector-java驱动连接MySQL数据库覆盖图书录入、条件检索、借阅归还、读者管理、数据统计等常用模块代码按模型、视图、控制器清晰分层便于理解业务逻辑与数据库交互。压缩包共222个文件主体为160个Java源文件另含12个jar依赖包、class字节码、properties配置、SQL数据库脚本、说明文档等资源中提供了建表脚本可快速初始化表结构并搭建演示环境整体压缩后仅8.58MB下载与部署都很轻量。目前已有1464人学习/下载。借助该项目可以掌握MySQL表结构设计思路、JDBC连接参数配置方法学习通过Connection及Statement/PreparedStatement执行增删改查同时熟悉Java Web项目的src、webapp、lib等目录的划分方式与依赖管理习惯无论用于毕业设计还是Java后端入门这套源码都能提供不错的范本。 在Java后端开发这条路上只要你去搜“Java练手项目”第一个蹦出来的十有八九就是“图书管理系统”。这玩意儿从十年前的SSH时代火到现在的Spring Boot时代生命力堪比Java本身。很多人觉得它太老套但我个人看法恰恰相反——图书管理系统几乎是教科书级别的“全链路入门样本”它覆盖了JavaWeb开发最核心的环节前端页面交互、Servlet控制层、业务逻辑封装、JDBC数据库操作、表结构设计、事务控制。把这套流程彻底搞懂后面学什么框架都觉得理所当然因为那些框架不过是把这些环节的重复劳动给优化掉了而已。我开发的这个版本是基于纯粹的JavaServlet JSP MySQL 8.0 构建的。在写这篇文章之前我已经用这套代码跑通了一个可以实际使用的系统借书、还书、查书、用户管理几个核心功能都能正常运转。下面我把完整的思路、数据库设计和代码关键实现拆开揉碎了讲清楚顺便把那些只有在实际动手时才会踩到的坑也一并列出来。1. 项目整体设计思路拆解1.1 为什么选 Servlet JSP 而不是直接用 Spring Boot现在出去面试张口闭口都是Spring Boot很多新手直接跳过了Servlet和JSP上来就啃Spring框架。我不太建议这样。Servlet是JavaWeb的“地基”Spring MVC本质还是围绕Servlet那一套请求分发机制做的封装。你把Servlet和JDBC真正跑熟了后面理解Spring IOC、理解MyBatis的事务管理、理解过滤器拦截器都是水到渠成的事因为你知道它们到底在解决什么样的问题。所以在图书管理系统这个项目上我刻意不用任何框架就用最原始的Servlet JSP JDBC来搭建。效果其实不差请求进来Servlet 通过 Mapping 找到对应的逻辑处理类。业务层调用 DAO 层DAO 层用 JDBC 操作 MySQL。数据拿回来塞进 request 域由 JSP 渲染成页面。整个过程没有任何黑盒每一步你都能调试、都能看到数据是怎么流动的。这对打基本功来说实在是太重要了。1.2 功能模块怎么规划才算合理图书管理系统如果只做“增删改查”确实没什么含金量但你要是把模块划分清楚、把边界做好就完全不一样了。我这边分了五个核心模块用户登录与注册区分管理员和普通读者管理员有借阅和图书管理权限。图书管理新增图书、编辑信息、下架删除。借阅管理借书、还书、查询借阅记录。读者管理注册读者、禁用/启用账号。数据统计做一个简单的借阅排行拿Top 10既是练手也能展示SQL聚合查询的能力。这几个模块横向覆盖了业务系统的典型闭环纵向深挖了表关联和事务处理。很多类似的项目把“用户管理”和“读者管理”混在一起这其实是不对的因为管理员和读者的字段都不一样权限也不同勉强塞在同一个表里只会让后续的权限判断变得非常别扭。1.3 技术栈版本选择的一个小忠告我用的组合是JDK 8等保要求不是是因为8仍然是最稳的LTS而且很多老项目还在用面试也最常问。MySQL 8.0.308.0 相比5.7在窗口函数、json支持、性能上有明显提升。MySQL Connector/J 8.0.30 驱动注意驱动版本必须跟数据库版本大版本一致不然会报时区或认证协议错误。Tomcat 9.xServlet 4.0规范支持。新手最容易踩的坑是JDK版本过高比如用了JDK 17然后Tomcat 9兼容性没问题但某些老教程配套的Tomcat 8就报错了。我建议直接用JDK 8 Tomcat 9最稳。2. 核心细节解析与实操要点2.1 数据库表设计五张表怎么互相衔接表设计是整个系统的灵魂我的具体设计如下第一张表用户表(user)字段包括id主键自增、username、password、role用字符串区分也是可以的但建议用int因为后续扩展角色会更方便比如1表示普通读者、2表示管理员、3表示图书管理员、status0禁用、1启用。第二张表图书表(book)id、book_name、author、publisher、isbn国际标准书号最好加唯一索引我实测图书馆里同一本书不同批次的就是靠isbn区分的、category分类、total_count总数量、current_count当前可借数量。第三张表借阅记录表(borrow_record)id、user_id、book_id、borrow_time、return_time、status0借出中、1已归还。这套设计是经典的“中间表”它把“谁借了哪本书”这样一个多对多关系拆成了两张一对多关系这是管理类系统的核心思想。第四张表分类表(category)为了规范化也可以拆出来但初期用字符串也能跑。如果后续要做分类统计报表还是拆出来更合理。另外还有一个登录日志表(login_log)用来记录用户每次登录的时间和IP地址这既是安全层面的考量也是后面做数据分析的基础数据。SQL脚本我会在后面直接贴出来可以直接拿去执行每个字段我尽量加上COMMENT养成好习惯。2.2 JDBC连接池不能用 DriverManager 硬刚刚学JDBC时大家都是用DriverManager.getConnection()来获取连接用完了conn.close()。这在单机练手时没问题但一旦涉及到并发访问比如几十个读者同时登录查书数据库连接就会反复创建、销毁性能惨不忍睹。正规项目必须用连接池。我用的方案是Apache DBCP2这是最经典的老牌连接池之一。考虑到项目是原生Servlet不引入Spring所以我直接在applicationContext.xml其实更准确的名字是 db.properties 一个连接池工具类里配置。核心配置参数就三组初始连接数initialSize10、最大连接数maxTotal50、最大空闲连接数maxIdle20。如果连接不够了连接池会帮你排队等待比反复开关连接靠谱得多。另外还有一点容易被忽视testWhileIdle和validationQuery一定要配它们的意义是每隔一段时间向连接池里的连接发一条SELECT 1看这个连接还活着没。不然数据库重启一下你的连接池里全是坏死连接页面就直接白屏报连接异常。2.3 数据库操作的封装DAO层的设计模式如果你在每个Servlet里直接写JDBC代码十个方法里有九个是重复的。正确的姿势是把数据库操作封装成DAO层用DAO接口 实现类的模式。我写了BaseDAO作为基类把通用的增删改查方法都抽出来public class BaseDAO { // 通用更新操作insert、update、delete public int update(String sql, Object... params) { try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }这里有个关键点用 PreparedStatement 绝对不要用 Statement。原因有两个一是PreparedStatement会预编译SQL执行效率更高二是它能防止SQL注入攻击用Statement你只能靠字符串拼接读者在用户名框里输入 OR 11就能直接登进去我测试过相当恐怖。再往下我写了一个通用的查询方法通过反射把结果集映射成JavaBeanpublic T ListT query(String sql, ClassT clazz, Object... params) { ListT list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { ResultSetMetaData metaData rs.getMetaData(); int columnCount metaData.getColumnCount(); while (rs.next()) { T obj clazz.getDeclaredConstructor().newInstance(); for (int i 1; i columnCount; i) { String columnName metaData.getColumnLabel(i); Object value rs.getObject(i); // 通过反射调用 setter 方法赋值列名与属性名一致即可 BeanUtils.setProperty(obj, columnName, value); } list.add(obj); } } } catch (Exception e) { e.printStackTrace(); } return list; }这一层封装完成后BookDAO、UserDAO只需要专注写自己的业务SQL整个代码量直接缩减一半以上后面维护起来也非常舒服。2.4 核心业务场景借书还书的事务处理借书这个业务后端要做的事比表面上看起来多得多检查读者有没有被禁用。检查当前书可借数量是否大于0current_count 0。检查该读者是否已经借了这本书还没还避免重复借同一本书。插入一条借阅记录。把书的可借数量减1。这五步中第4步和第5步必须放在同一个数据库事务里——如果插入记录成功了但扣减库存失败了那记录和库存就对不上了后面盘点数据时全乱套。我直接用Connection开启手动提交事务conn DBUtil.getConnection(); conn.setAutoCommit(false); try { insertBorrowRecord(conn, userId, bookId); updateBookCurrentCount(conn, bookId, -1); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); // 恢复为自动提交 if (conn ! null) conn.close(); // 归还连接池 }注意细节事务一旦开始连接就不能在操作中途关掉必须要保证使用的是同一个连接。所以我下面两个方法都得接收一个Connection参数而不是像我之前的工具类那样自己内部开启连接。这个知识点也是面试官特别爱问的点如果不用事务会出现什么后果很典型的就是同一本书被两个读者同时借走超卖问题数据库可借数量变成负数。3. 实操过程与核心环节实现3.1 数据库初始化脚本可直接执行我直接贴一份版本SQL你在MySQL 8.0里执行就能建库建表CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARSET utf8mb4; USE library_system; -- 用户表 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 密码建议存加盐MD5, role TINYINT DEFAULT 1 COMMENT 角色1普通用户2管理员, status TINYINT DEFAULT 1 COMMENT 状态1正常0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, book_name VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) COMMENT 作者, isbn VARCHAR(50) NOT NULL UNIQUE COMMENT 国际标准书号, publisher VARCHAR(100) COMMENT 出版社, category VARCHAR(50) COMMENT 分类, total_count INT DEFAULT 0 COMMENT 总数量, current_count INT DEFAULT 0 COMMENT 当前可借数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表 CREATE TABLE borrow_record ( id INT AUTO_INCREMENT PRIMARY KEY, record_no VARCHAR(64) NOT NULL UNIQUE COMMENT 流水号, user_id INT NOT NULL COMMENT 读者ID, book_id INT NOT NULL COMMENT 图书ID, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借书时间, return_time DATETIME DEFAULT NULL COMMENT 还书时间, status TINYINT DEFAULT 0 COMMENT 0借出中1已归还, KEY idx_borrow_user_book (user_id, book_id), KEY idx_borrow_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关于record_no这个字段设计时我留了个小心思用它唯一标识一条借阅记录。生成方式用yyyyMMddHHmmss 随机数这样既直观又防止并发重复。3.2 环境准备JDK、MySQL、Tomcat 的安装与配置环境这块虽然网上教程一大堆但我在给朋友远程装的时候发现很多人卡在环境变量上。我简单说下关键点JDK安装完在系统变量里配置JAVA_HOME指向JDK安装目录然后在Path变量里添加%JAVA_HOME%\bin。配置完在命令行窗口输入java -version能显示版本号就是成功了。MySQL 8.0安装时选择自定义安装注意记住配置的密码root用户的密码后面连接池要用。另外8.0默认认证插件是caching_sha2_password如果你的JDBC驱动是5.x的老版本会报认证失败一定要用8.x的驱动。Tomcat是免安装版解压后在bin目录下双击startup.bat启动Windows访问http://localhost:8080看到首页就说明Tomcat起来了。如果启动Tomcat时一闪而过多半是JAVA_HOME没配置好。这是新手最常见的问题没有之一。3.3 核心代码落地登录逻辑的完整实现登录是图书管理系统的“第一道门”我贴一个精简版的登录Servlet重点看参数校验和密码匹配WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 1. 基础校验 if (username null || password null || username.trim().isEmpty() || password.trim().isEmpty()) { req.setAttribute(error, 用户名和密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 2. 调用业务层验证 User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } if (user.getStatus() 0) { req.setAttribute(error, 账号已被禁用请联系管理员); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 3. 登录成功存Session重定向到首页 req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /book/list); } }实际开发里我还会在前面加一层Filter做统一的登录状态校验未登录的人不能访问后台页面这个Filter就是后面学习Spring Security时那个拦截链的原始形态。密码存储我强烈建议不要用明文。我在项目里用MD5加盐处理public static String md5WithSalt(String password, String salt) { String str password salt; return DigestUtils.md5DigestAsHex(str.getBytes(StandardCharsets.UTF_8)); }把加盐后的密文存进数据库即使数据库泄露了攻击者拿到的也不是你的原始密码。这个习惯最好从练手项目就开始养成。3.4 前端页面渲染JSP EL表达式 JSTL原生的JSP虽然现在看起来土但其渲染逻辑其实跟Thymeleaf、Freemarker没啥本质区别。核心原则就一句话JSP里不要写Java业务逻辑只做展示。我在book_list.jsp里通过EL表达式取数据c:forEach items${bookList} varbook tr td${book.id}/td td${book.bookName}/td td${book.author}/td td${book.currentCount} / ${book.totalCount}/td td a href${pageContext.request.contextPath}/book/toEdit?id${book.id}编辑/a a href${pageContext.request.contextPath}/book/delete?id${book.id} onclickreturn confirm(确认删除这本书)删除/a /td /tr /c:forEach注意发送删除请求时用return confirm()这一步是前端兜底防止误操作导致数据丢失。后端也一定要做二次校验不能只靠前端。3.5 核心报表实现借阅排行榜图书管理系统要是能展示一个“本月借阅Top 10”一下子就有灵魂了。这个SQL我写了修改一版核心是聚合连表查询SELECT b.book_name, b.author, COUNT(br.id) AS borrow_count FROM borrow_record br JOIN book b ON br.book_id b.id WHERE br.borrow_time DATE_FORMAT(CURDATE(), %Y-%m-01) AND br.borrow_time DATE_ADD(CURDATE(), INTERVAL 1 MONTH) GROUP BY br.book_id, b.book_name, b.author ORDER BY borrow_count DESC LIMIT 10;笛卡尔积的理解这张表先用JOIN把借阅记录和图书关联起来再用WHERE限定当月然后用GROUP BY按书分组、COUNT统计借出次数最后排序取前十。这是非常典型的SQL聚合操作面试题里也爱出。4. 常见问题与排查技巧实录4.1 “Data source rejected establishment of connection” 一类连接报错这多半是数据库服务器没启动或者连接池配置里的URL、用户名、密码写错了。经验之谈遇到连接类错误首先在命令行敲mysql -uroot -p手动连一次数据库确认数据库本身没问题再回来看连接池配置。4.2 中文乱码页面、数据库、请求三层都要设置中文乱码大概是JavaWeb里最经典的问题。它发生的环节有三个数据库层面建表时没有使用utf8mb4导致保存中文报错“Incorrect string value”。解决办法是建库时就指定字符集老表的话用ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4;转一下。页面展示层面JSP页面顶部没有指定contentTypetext/html; charsetutf-8或者浏览器默认编码不是UTF-8。请求接收层面POST请求里中文参数默认是按ISO-8859-1去解码的所以Servlet里第一行一定要写req.setCharacterEncoding(UTF-8)必须在读取第一个参数之前调用才有效。4.3 Tomcat端口被占用启动Tomcat时报端口被占用八成是以前启动的Tomcat进程没杀掉。Windows下用netstat -ano | findstr 8080查看占用端口的PID然后taskkill /F /PID 对应进程号。还有一种可能是我遇到过很多次的Tomcat启动是“成功”了但页面一访问就报404几乎就是部署路径的问题。检查一下IDEA里Artifacts的配置一定要保证依赖库是打包进WEB-INF/lib的否则Tomcat找不到JDBC驱动运行就会报ClassNotFoundException。4.4 MySQL 8.0 的时区问题MySQL 8.0 安装后如果用默认配置接JDBC经常会遇到The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这种奇怪报错。解决办法有两种一是在连接串里加serverTimezoneAsia/Shanghai推荐项目到处部署都不怕二是在MySQL中执行SET GLOBAL time_zone 08:00;。要注意这种时区报错不是MySQL连不上了而是驱动拿到系统时区后发现无法把它转换为支持的格式于是报错。这个知识点在面试八股文里很常考容易让人紧张。4.5 PreparedStatement 索引越界异常这个坑看起来简单但特别容易翻车setObject(i 1, params[i])里i是从0开始的数组下标而数据库占位符的索引从1开始所以下标换算一定不能写错。如果params长度是3你却写了4个占位符运行时会直接报Parameter index out of range。我在写DAO时还遇到过一个更隐蔽的情况利用if (params.length 0)检查后再执行setObject。有些老代码不看参数数量空数组或null也会调用set方法照样报错。养成“先判断再操作”的习惯踩坑率会低很多。4.6 数据库死锁问题这个属于进阶问题了但我确实在借还书高频操作时遇到过两个事务同时修改同一本书的库存MySQL会检测到死锁其中一个事务被回滚。解决方案也简单所有更新操作按固定的顺序执行比如都先更新book表再插入borrow_record表这样可以大幅降低死锁概率。另外一个更实用的方案是在更新书库存时使用乐观锁给book表加一个version字段更新时带有WHERE version ?这样并发控制会优雅很多。5. 项目后续可以掘金的地方如果你已经完全消化了这套原生Servlet JDBC后面可以沿着两条路继续深挖。第一技术栈演进把Servlet换成一个常用的MVC框架比如Spring MVC然后把JDBC手动封装换成Spring管理的数据源再引入MyBatis实现ORM映射开发效率会瞬间提升因为你已经知道它替你把哪些重复劳动承担了。最后把JSP换成前后端分离方案后端纯提供JSON接口前端用Vue或React那就是现在企业中主流的开发模式了。第二业务功能扩展图书管理系统可以加很多有意思的模块比如图书封面图片上传与展示练习文件上传、二维码借阅练第三方SDK对接、逾期费用计算引入定时任务、热门图书推荐简单推荐算法、基于消息队列的借阅通知引入MQ。对我来说这个项目最大的价值不在于它有多时髦而在于它把一根完整的链路摆在你面前——从前端请求到后端处理再到数据库落库每一步都没有黑盒每一步都可以亲手验证。在技术圈里能在早期阶段把这条链路“看穿”和“跑通”的人后面学任何新技术都快很多。最后再分享一个小技巧我建议你在这个项目上做一个“代码重构记录文档”每学一个新知识点比如学会了连接池、学会了事务、学会了ORM就回项目上改一次。看着项目从最初几百行堆砌在一起的代码逐渐变成分层的、清晰的、规范的工程这才是练手项目给你的最好回报。本文还有配套的精品资源点击获取
返回列表