简介:这是一套基于Java Web技术开发的图书馆管理系统源码,使用Servlet、JSP和JavaBean实现,配合MySQL数据库存储数据,适合高校图书馆场景和Java Web课程设计使用。压缩包共209个文件,包含51个JSP页面、32个Java源码及32个class字节码,另附9个jar依赖库、SQL脚本及多张MySQL数据表(.frm/.myd/.myi),整体大小1.91MB,目录结构清晰,便于直接导入IDE部署运行。目前已有358人学习下载。系统覆盖MVC分层设计、Servlet与JSP交互、用户登录鉴权、图书与借阅管理等核心流程,并附带完整的数据库建表文件,对理解Web应用请求转发、异常处理和项目部署均有实际参考价值。读者可基于源码快速搭建一套可用的图书管理系统,用于课程设计、毕业设计或Java Web进阶练习。
1. 图书馆管理系统:一个还在用 JSP + Servlet 的 JavaWeb 项目,为什么值得跑
前阵子接手一个「老人」项目——同事离职留给我一套 javaWeb 图书馆管理系统源码,MySQL 版本,Servlet + JSP + JDBC 全手动拼 SQL。第一眼觉得落后,跑起来才发现这套东西把 JavaWeb 的核心链路讲得明明白白:请求怎么进 Tomcat、Servlet 怎么转发、JDBC 怎么管事务、Session 怎么控权限,每一步都是可断点跟进去看的。对刚学完 JavaWeb 但没写过完整项目的人,或者毕设想找「能跑、能改、能讲清楚」的系统的同学,这套源码比 Spring Boot 黑盒式封装更适合当底子。本篇文章围绕这套源码,拆它的模块设计、跑通步骤和二次开发路径,新手照着操作能上线演示,熟手也能从中看到经典三层架构的实际落法。
2. 架构与数据模型:三层结构、5 张核心表和一整套权限划分
2.1 技术选型:为什么 JSP + Servlet + MySQL 是这套系统的底子
这个项目没有引入 Spring、MyBatis 这类框架,而是回归到了 JavaWeb 最原生的那套组合:Servlet 负责接收 HTTP 请求和控制页面跳转,JSP 负责渲染动态页面,JDBC 直接操作 MySQL 数据库。从教学和排查角度讲,这种选型把「请求进来之后发生了什么」完全暴露在代码里,不像 Spring MVC 那样还要经过 DispatcherServlet 的一堆映射规则。亮点在于它的分层特别干净:control 层只做参数接收和视图转发,dao 层只做 SQL 执行,model 层只承载数据,没有出现 Servlet 里写大段 SQL 的坏味道。
我一般会建议这样理解这套技术栈:JSP 是视图,Servlet 是控制器,DAO 是数据访问层,三者通过请求转发和 JavaBean 传值串联。相比现在流行的前后端分离,这套方案是服务端渲染的老路子,浏览器拿到的就是完整的 HTML 页面;好处是调试链路短,F12 里能看到表单提交到了哪个 Servlet,坏处是页面和 Java 代码耦合度高,改样式要动 JSP。作为学习项目,这个「短链路」反而是优势——你可以在一个请求的完整生命周期里搞清楚 MVC 到底怎么转的。
2.2 目录结构与包职责
拿到源码解压后,先不要急着丢进 Tomcat,建议花十分钟把包结构过一遍。这项目的目录不是随便堆的,每个包名都对应一类职责。展开看大概是这样的:
src/ com.library.control/ 控制层,存放各类 Servlet LoginControl.java 处理登录请求 BookControl.java 处理图书增删改查请求 BorrowControl.java 处理借书还书请求 ReaderControl.java 处理读者管理请求 com.library.dao/ 数据访问层,封装所有 SQL 操作 BookDao.java BorrowDao.java ReaderDao.java com.library.model/ 实体类,对应数据库表结构 Book.java Reader.java Borrow.java com.library.util/ 工具类 DBUtil.java 获取数据库连接 WebContent/ admin/ 管理员页面 book_list.jsp borrow_list.jsp reader/ 读者页面 index.jsp borrow.jsp login.jsp 登录页 WEB-INF/web.xml Servlet 映射配置 database/ library.sql 建库建表脚本控制层的命名很直白,LoginControl 就是处理登录的,BorrowControl 就是处理借还书的,没有任何绕弯子的抽象。DAO 层每个类对应一张表的操作,方法名 findByBookName、getBorrowListByReaderId 这种看到名字就知道干什么。这种「一个包一类职责」的组织方式,恰恰是很多框架项目里最容易丢的东西——Spring Boot 项目一个 Service 里塞五张表的逻辑是常事,而这个老项目反而保持了纪律。
2.3 数据库设计:权限分离靠一张表搞定
数据库脚本在 database 目录下,导入 MySQL 后会出现五张核心表,表结构如下:
| 表名 | 职责 | 关键字段 |
|---|---|---|
| reader | 用户表,区分管理员和普通读者 | reader_id, username, password, role |
| book | 图书表 | book_id, book_name, author, publisher, price, stock |
| borrow | 借阅记录表 | borrow_id, reader_id, book_id, borrow_time, return_time |
| category | 图书分类表 | category_id, category_name |
| penalty | 罚款记录表 | penalty_id, reader_id, amount, reason |
权限是怎么做的?答案在 reader 表里有一个 role 字段,用字符串区分「admin」和「reader」两种身份。登录成功后把这个值写进 Session,每次请求管理员页面时先判断 Session 里的 role 是不是 admin,不是就直接重定向到登录页。这种写法在小型系统里非常实用,省掉了独立的权限表,也降低了理解成本。建表时注意 reader 表里 username 加了唯一索引,这是防止重复注册的第一道防线。
CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role VARCHAR(20) DEFAULT 'reader' );role 字段默认值是 reader,意味着注册进来的人默认只有借书权限;管理员账号需要手动往数据库里插,或者后期在管理端开放角色提升功能。这套设计在演示环境完全够用,但如果要上生产,密码还是得做哈希存储,后面改造部分会单独说。
3. 核心模块拆解:从登录到借还书的完整代码链路
3.1 DBUtil:所有数据库操作的起点
整个项目的数据库操作都绕不开 util 包里的 DBUtil 类,它就是那个最基础的「连接工厂」。现在看这段代码会觉得原始,但它把 JDBC 的核心步骤压缩到了一个类里:加载驱动、建立连接、抛异常。这项目里没有用连接池,每次操作数据库都是 DriverManager 现拿现用,操作完再关闭。常见做法不外乎就是这么一回事。
public class DBUtil { private static String url = "jdbc:mysql://localhost:3306/library?useSSL=false&characterEncoding=utf8"; private static String user = "root"; private static String password = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }上述代码有两点要留意。第一个是 Class.forName 里的驱动名,如果是 MySQL 5.x 用 com.mysql.jdbc.Driver 没问题,换到 MySQL 8.0 就得改成 com.mysql.cj.jdbc.Driver,否则会直接报找不到驱动类——这个坑我在第 4 章避坑部分还会再提。第二个是关闭顺序,一定先关 ResultSet,再关 PreparedStatement,最后关 Connection,顺序反了容易出现资源没释放干净的情况。URL 参数里 characterEncoding=utf8 是强制客户端和服务端用 UTF-8 通信,少了它,中文写入数据库后查询出来就是问号。
3.2 登录验证:从请求参数到会话保持
登录模块是整个系统的门面,它串起了 JSP 表单、Servlet 参数解析、DAO 查询和 Session 写入四个环节。核心逻辑写在 LoginControl 里,流程是:从 request 里拿用户名和密码,调 ReaderDao 的 findByUsernameAndPassword 方法查数据库,查到就登录成功,把用户信息塞进 Session,然后按角色跳转到不同首页。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); ReaderDao dao = new ReaderDao(); Reader reader = dao.findByUsernameAndPassword(username, password); if (reader != null) { HttpSession session = request.getSession(); session.setAttribute("currentUser", reader); if ("admin".equals(reader.getRole())) { response.sendRedirect("admin/book_list.jsp"); } else { response.sendRedirect("reader/index.jsp"); } } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }逻辑说明:先用 getParameter 拿到表单提交的两个字符串,这里没有做非空校验,是这块代码一个明显短板——用户什么都不填直接点登录,也会走一次数据库查询。findByUsernameAndPassword 是把用户输入的密码原样拼进 SQL 去比对,属于明文比对方式。登录成功的标志是 reader 对象非空,然后把整个 reader 对象塞进 Session,后续页面想显示当前用户名,直接用 session.getAttribute("currentUser") 再往下拿属性就行。失败场景用的是 forward 转发而不是重定向,这样 request 里的 errorMsg 属性才能在 login.jsp 上显示出来,如果你在这里写成 sendRedirect,页面上永远看不到错误提示。
3.3 借书流程:库存判断与事务回滚
如果说登录模块是入门,那借书模块就是这项目里最有含金量的一段代码。它涉及两张表的写操作:往 borrow 表插一条借阅记录,同时要把 book 表的 stock 库存减一。这两个动作必须绑在一起,要么都成功,要么都失败。这个项目处理得挺规矩——手动开启事务,catch 到异常就回滚。
Connection conn = null; PreparedStatement insertBorrow = null; PreparedStatement updateStock = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); insertBorrow = conn.prepareStatement( "INSERT INTO borrow (reader_id, book_id, borrow_time) VALUES (?, ?, NOW())"); insertBorrow.setInt(1, readerId); insertBorrow.setInt(2, bookId); insertBorrow.executeUpdate(); updateStock = conn.prepareStatement( "UPDATE book SET stock = stock - 1 WHERE book_id = ? AND stock > 0"); updateStock.setInt(1, bookId); int rows = updateStock.executeUpdate(); if (rows == 0) { throw new RuntimeException("库存不足,借书失败"); } conn.commit(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } request.setAttribute("errorMsg", e.getMessage()); } finally { DBUtil.close(conn, insertBorrow, null); DBUtil.close(null, updateStock, null); }关键点在两个地方。第一个是 setAutoCommit(false),这会让连接进入手动提交模式,后续两条 SQL 都执行成功后才统一 commit,只要中间任何一步抛异常就执行 rollback,把前面已经执行的 SQL 撤销掉。第二个是 UPDATE 语句末尾的 AND stock > 0,这是库存判断的兜底逻辑——即使 Java 代码里先查过库存大于零,并发情况下也可能在这个连接读库存之后、更新之前被别的请求扣掉库存,SQL 层面的条件判断是最后一道防线。executeUpdate 返回受影响行数,小于 1 就说明没有匹配的库存记录可扣,直接抛出异常触发回滚,借阅记录也就不会残留。
参数说明:readerId 和 bookId 是从页面传过来的,通常一个来自 Session 里的当前用户,一个来自请求参数。borrow_time 直接用数据库函数 NOW(),避免 Java 和数据库时区不一致导致时间偏移。这套事务写法放在现在依然规范,遇到修改需求时注意别把 setAutoCommit(false) 删掉就行。
4. 部署与避坑:Tomcat + MySQL 跑通的每一步和翻车记录
4.1 环境准备与项目导入
运行这个项目需要的基础环境是:JDK 1.8、Tomcat 8.5 或 9.x、MySQL 5.7 或 8.0,外加一个能跑 Web 项目的 IDE,常见做法是用 Eclipse 的 Java EE 版本直接导入。如果是 IDEA,导入时选择「Import Project」然后选 Eclipse 项目结构,注意 JDK 级别选 1.8,别默认选到 17,否则老项目里一些写法会直接编译报错。
导入后有两处必须手工处理。第一处是把 mysql-connector-java 的 jar 包加到项目的构建路径里,同时确保它被同步到 WEB-INF/lib 目录下;第二处是确认 Tomcat 运行时已经配置到 IDE 里,否则项目右键找不到 Run on Server 选项。这里的常见错误是把 jar 包只加到了 Build Path 但没放进 WEB-INF/lib,结果 IDE 里编译不报错,启动 Tomcat 后一访问数据库相关页面就报 ClassNotFoundException。
4.2 数据库初始化与配置修改
打开 MySQL 命令行或者 Navicat,执行 database 目录下的 library.sql。这脚本会建库建表,并把一些测试数据一起 insert 进去,管理员账号和几本测试图书都在里面。执行完成后,确认 library 库下能看到 5 张表,然后打开 DBUtil.java,把 url、user、password 三个字段改成你自己 MySQL 的地址和账号。
mysql -u root -p < database/library.sql改完配置文件,有个简单的自检方法:写一个带 main 方法的测试类,直接 new DBUtil 然后调 getConnection,能拿到 Connection 对象就说明数据库层面通了。这一步值得单独做,因为很多 Tomcat 启动报错其实是数据库连接参数写错,而不是 Web 配置的问题,先把变量隔离掉,后面排查范围就小多了。
4.3 部署到 Tomcat 并验证功能
在 IDE 里右键项目选择 Run on Server,Tomcat 启动后浏览器访问 http://localhost:8080/项目名/login.jsp。这里项目名取决于你导入时设置的 Context Path,一般是工程名,也可以手动改成 library。第一次启动如果页面 404,优先检查 Tomcat 的 webapps 下有没有生成对应的部署目录,以及访问路径里项目名是否和实际部署名一致。
验证功能建议按这条路径走:先用管理员账号登录,确认能跳转到 admin 首页,然后走一遍添加图书、查看借阅记录的完整流程;再退出登录,用普通读者账号登录,去试借一本书,看看库存是不是减一了。这套流程能覆盖大部分核心代码的分支,走通了基本就说明这个项目跑起来了。
4.4 避坑记录:五个容易翻车的现场
现象一:Tomcat 启动后访问页面,报 ClassNotFoundException: com.mysql.jdbc.Driver
原因九成是 mysql 驱动 jar 包没有真正部署到 WEB-INF/lib 目录。IDEA 里加 Build Path 只是编译期有效,运行期类加载是找 WEB-INF/lib 下的包。解决方法是把驱动 jar 直接复制进 WebContent/WEB-INF/lib 里,然后重新部署。
现象二:往数据库里插中文,查出来全是问号
原因分两层:数据库表字符集不是 utf8,或者 JDBC 连接串没加 characterEncoding=utf8。解决方法是建库时指定 DEFAULT CHARSET=utf8,同时把 DBUtil 里 url 参数补上字符集声明,两层都对了才能彻底解决。
现象三:借书操作偶尔出现「借阅记录在,库存没减」或者反过来
原因是开发时把两条 SQL 放在两个独立方法里执行了,或者虽然在一个方法里但没开事务。解决方法是把插入借阅记录和扣减库存放进同一个事务块,参照第 3 章的写法,setAutoCommit(false) 加 catch 里 rollback。
现象四:MySQL 8.0 连接时报 Public Key Retrieval is not allowed
比现象三更隐蔽,本质是 MySQL 8 默认认证插件是 caching_sha2_password,老版本驱动不认识。解决方法是换 mysql-connector-java 8.x 版本,同时连接串加 allowPublicKeyRetrieval=true&useSSL=false。
现象五:系统用着用着变卡,数据库连接数飙高
原因是 DBUtil.close 在部分异常路径上没被执行,连接泄漏了。解决方法是检查每个 finally 块是否把 Connection 关闭,或者在 catch 块里也调用 close。老项目里最容易漏的是查询方法里 ResultSet 不为空时提前 return,导致后面的关闭代码没跑到。
5. 二次开发思路:给老项目加一个热门图书排行接口
跑通只是第一步,真正能体现这套源码价值的是改造它。我最建议做的一个改造是「热门图书排行」——按借阅次数倒序展示 Top 10,这个功能不复杂,但正好覆盖了 DAO 层加方法、控制层加 Servlet、前端加展示三个环节,适合作为从读到写的过渡练习。
第一步在 BookDao 里加一个方法,查询借阅次数最多的前 10 本书:
public List<Book> findHotBooks() { List<Book> books = new ArrayList<>(); String sql = "SELECT b.*, COUNT(br.borrow_id) AS borrow_count " + "FROM book b LEFT JOIN borrow br ON b.book_id = br.book_id " + "GROUP BY b.book_id ORDER BY borrow_count DESC LIMIT 10"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setBookId(rs.getInt("book_id")); book.setBookName(rs.getString("book_name")); books.add(book); } } catch (SQLException e) { e.printStackTrace(); } return books; }这里用 LEFT JOIN 而不是 INNER JOIN,是为了把一本都没被借过的书也统计进来,借阅次数按 COUNT(borrow.brrow_id) 计,没借过的会显示 0。这个写法也解决了排序里 NULL 值的问题——如果借阅表里没有对应记录,COUNT 返回 0 而不是 NULL,排序不会出错。第二步新建一个 HotBookServlet,把查询结果塞进 request,转发到一个新建的 hot_book.jsp 页面去展示。改造完这几步,你会发现原来的分层结构完全撑得住新功能扩展:数据操作进 DAO,请求处理进控制层,展示进 JSP,不需要动任何已有代码文件。
从那以后我每次拿到老项目,都会强制自己先走一遍「跑通 → 加一个小功能 → 看改动落在哪些层」的完整流程,而不是只顾着跑通就收工。这个习惯帮我在改任何历史代码时都心里有底——毕竟刚接手的项目,能跑起来只是开始,能在一小时内加上一个新功能才是真正摸透了它的脾气。希望帮到你。
本文还有配套的精品资源,点击获取