简介:一份基于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数据库操作、表结构设计、事务控制。把这套流程彻底搞懂,后面学什么框架都觉得理所当然,因为那些框架不过是把这些环节的重复劳动给优化掉了而已。
我开发的这个版本是基于纯粹的Java(Servlet + 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.30(8.0 相比5.7在窗口函数、json支持、性能上有明显提升)。
- MySQL Connector/J 8.0.30 驱动(注意,驱动版本必须跟数据库版本大版本一致,不然会报时区或认证协议错误)。
- Tomcat 9.x(Servlet 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表示图书管理员)、status(0禁用、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、status(0借出中、1已归还)。这套设计是经典的“中间表”,它把“谁借了哪本书”这样一个多对多关系拆成了两张一对多关系,这是管理类系统的核心思想。
第四张表,分类表(category):为了规范化也可以拆出来,但初期用字符串也能跑。如果后续要做分类统计报表,还是拆出来更合理。
另外还有一个登录日志表(login_log),用来记录用户每次登录的时间和IP地址,这既是安全层面的考量,也是后面做数据分析的基础数据。
SQL脚本我会在后面直接贴出来,可以直接拿去执行,每个字段我尽量加上COMMENT,养成好习惯。
2.2 JDBC连接池:不能用 DriverManager 硬刚
刚学JDBC时,大家都是用DriverManager.getConnection()来获取连接,用完了conn.close()。这在单机练手时没问题,但一旦涉及到并发访问(比如几十个读者同时登录查书),数据库连接就会反复创建、销毁,性能惨不忍睹。
正规项目必须用连接池。我用的方案是Apache DBCP2,这是最经典的老牌连接池之一。考虑到项目是原生Servlet,不引入Spring,所以我直接在applicationContext.xml(其实更准确的名字是 db.properties + 一个连接池工具类)里配置。
核心配置参数就三组:初始连接数(initialSize=10)、最大连接数(maxTotal=50)、最大空闲连接数(maxIdle=20)。如果连接不够了,连接池会帮你排队等待,比反复开关连接靠谱得多。
另外还有一点容易被忽视: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 '1'='1就能直接登进去,我测试过,相当恐怖。
再往下,我写了一个通用的查询方法,通过反射把结果集映射成JavaBean:
public <T> List<T> query(String sql, Class<T> clazz, Object... params) { List<T> 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 核心业务场景:借书还书的事务处理
借书这个业务,后端要做的事比表面上看起来多得多:
- 检查读者有没有被禁用。
- 检查当前书可借数量是否大于0(current_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 '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录表 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关于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}" var="book"> <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}" onclick="return 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页面顶部没有指定
contentType="text/html; charset=utf-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这种奇怪报错。
解决办法有两种:一是在连接串里加serverTimezone=Asia/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),就回项目上改一次。看着项目从最初几百行堆砌在一起的代码,逐渐变成分层的、清晰的、规范的工程,这才是练手项目给你的最好回报。
本文还有配套的精品资源,点击获取