简介:这是一份基于Java Web的图书馆管理系统源码,主要面向Java初学者、高校课程设计与毕业设计人员,用于掌握Servlet、JSP、MVC设计模式及MySQL数据库的整合开发。压缩包总大小1.91MB,共209个文件,其中包含51个JSP页面、32个Java源文件与32个class文件,另有SQL脚本、Struts配置、属性文件及JAR依赖库等,结构清晰,便于导入开发工具并部署到Tomcat运行。系统覆盖图书信息管理、读者账户、借阅与归还等关键业务,代码中体现了用户认证授权、请求转发与重定向、JSTL/EL表达式、异常处理等JavaWeb核心知识点。目前已有358人学习下载,对希望从零构建完整Web项目、理解前后端交互与数据库设计的开发者而言,是一份有参考价值的完整项目示例。
1. 起底:一个JavaWeb图书馆管理系统源码里有什么
拿到这份javaWeb图书馆管理系统源码mysql版本 (2).zip的时候,我第一反应是这类教学级项目大同小异,无非是JSP页面加几个Servlet。但真正解压看完目录结构之后,我得说这个包的完整度超出预期:图书管理、读者管理、借书还书、逾期统计、分类检索全都有,而且数据库脚本、部署说明、前端页面一应俱全。它不是那种只有一个登录页的"半成品",而是一套能直接丢进Tomcat跑起来的完整Web应用。适合谁?正在学JavaWeb课程设计、需要交毕设或者想看看经典的Servlet+JSP+JDBC分层到底怎么组织代码的同学,这份源码是一个很好的参照物。下面我从头拆一遍。
2. 项目架构与技术选型:从Servlet到JDBC的经典分层
2.1 整体目录结构与分层设计
解压之后先看目录,这个项目的包结构是典型的Web工程布局。src下面按com.xxx的包名分了bean、dao、servlet、filter、util五类,WebRoot(或者叫web)下面则是admin、reader、css、js、images等静态资源和JSP页面目录。这种分法在早期JavaWeb项目里非常标准,bean放实体类,dao放数据访问层,servlet充当控制器,filter做登录拦截和字符编码过滤,util放数据库连接工具。
我见过不少学生项目把SQL直接写在JSP里,或者用一个巨无霸Servlet处理所有请求,这个项目没有这么干。它的分层逻辑是:JSP只负责展示和收集参数,Servlet收到请求后调用DAO层的方法,DAO层通过JDBC操作MySQL表,返回结果再封装成JavaBean转发回JSP。这个链路虽然比Spring Boot的Controller-Service-Mapper多了一些手工代码,但恰恰因为没有框架的魔法,反而能看清每一次请求经历了什么。
2.2 技术栈盘点:为什么选这些组件
技术选型是上古时期的标准套装:JDK 1.8、Tomcat 8.5、MySQL 5.7、Servlet 3.0、JSP 2.3,数据库驱动是mysql-connector-java-5.1.x。前端没有用框架,是原生的HTML+CSS+JavaScript,配合JSTL标签和EL表达式渲染数据。这个组合在今天看起来朴素,但对于学习JavaWeb的人来说是个好东西。
为什么这么说?因为没有Spring那一套依赖注入和自动装配,你必须自己new一个DAO对象、自己管理Connection的开关、自己在Filter里写chain.doFilter()。这些"麻烦"恰恰是理解JavaWeb底层最好的教材。比如你会在DBUtil里看到经典的Class.forName("com.mysql.jdbc.Driver"),这行代码现在很多人只知道加依赖却说不清它干了什么——其实是向DriverManager注册驱动类。跑通这个项目再去看MyBatis,你会豁然开朗。
2.3 数据库表设计:图书、读者、借阅三大核心表
数据库脚本在sql目录下,一个.sql文件把建库、建表、初始数据全包了。我打开看了下,核心表是四张:book(图书表)、reader(读者表)、borrow(借阅表)、admin(管理员表)。值得注意的细节是book表里有一个stock字段表示库存余量,borrow表用borrow_time和return_time两个datetime字段记录借出和归还时间。
这里有个设计上的小心思:借阅表没有用单独的状态字段标识"已借出/已归还",而是通过return_time是否为空来判断。这个设计在日常开发里很常见,查询进行中的借阅只需要WHERE return_time IS NULL,而统计历史记录则不需要排除任何行。另外book表的主键用的不是自增id,而是书的条形码book_id,这在图书馆场景里更合理——每本实体书本来就有唯一的条形码。
3. 核心模块拆解:借书还书全流程的代码实现
3.1 登录与权限校验:Session是怎么拦住未登录用户的
登录逻辑拆开看分三段:登录页表单提交、Servlet中校验用户名密码、Filter统一拦截未登录请求。先说LoginServlet的核心代码,它从request里拿到用户名和密码,调用DAO查询,查到就把用户对象塞进Session然后重定向到主页,查不到就返回错误提示。
// LoginServlet.java 核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); AdminDAO adminDAO = new AdminDAO(); Admin admin = adminDAO.findByUsernameAndPassword(username, password); if (admin != null) { HttpSession session = request.getSession(); session.setAttribute("admin", admin); session.setMaxInactiveInterval(30 * 60); // 30分钟未操作自动失效 response.sendRedirect(request.getContextPath() + "/admin/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }这里两个参数值得注意:request.setCharacterEncoding("UTF-8")是解决POST请求中文乱码的关键,不写这一行,页面上输入的中文到后端就是乱码;session.setMaxInactiveInterval(30 * 60)设置了会话超时时间。登录成功走的是重定向而非转发,这是个好习惯——避免用户刷新页面时重复提交表单。对应的,AuthFilter在doFilter里判断Session是否为空,为空就重定向到登录页。
3.2 图书管理模块:增删改查背后的SQL拼装
图书管理是典型的分页查询加CRUD。先说最有价值的分页查询,BookDAO里有一个findByPage(int pageNum, int pageSize)方法,用MySQL的LIMIT关键字实现分页。
// BookDAO.java 分页查询 public List<Book> findByPage(int pageNum, int pageSize) { List<Book> bookList = new ArrayList<Book>(); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "SELECT * FROM book ORDER BY book_id LIMIT ?, ?"; ps = conn.prepareStatement(sql); ps.setInt(1, (pageNum - 1) * pageSize); // 偏移量 ps.setInt(2, pageSize); // 每页条数 rs = ps.executeQuery(); while (rs.next()) { Book book = new Book(); book.setBookId(rs.getString("book_id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setStock(rs.getInt("stock")); bookList.add(book); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return bookList; }注意这里用的是PreparedStatement而不是Statement,好处是防止SQL注入,因为参数通过setInt、setString绑定,不会被拼进SQL字符串里。(pageNum - 1) * pageSize是分页的核心,第1页取0到pageSize条,第2页取pageSize到2*pageSize条。finally块关闭资源是Java7之前的标准写法,虽然啰嗦但能避免连接泄漏。如果你看到DBUtil.close的实现在维护一个ThreadLocal的Connection,那说明这项目还做了简单的线程绑定。
新增和修改图书用的是同一个页面,addBook.jsp提交到AddBookServlet,修改时则是先把数据查出来回显到表单,提交后走UpdateBookServlet。删除操作只有一行SQLDELETE FROM book WHERE book_id = ?,但要注意如果这本图书有未归还的借阅记录,直接删会报外键约束错误——这是我在跑这个项目时踩到的第一个坑,后面避坑章会详细说。
3.3 借阅模块:事务边界和库存扣减的实现
借书是整个系统里最需要谨慎的模块,因为它涉及两张表的修改:往borrow表插入一条借阅记录,同时把book表的stock减1。这两步操作必须在一个数据库事务里完成,否则会出现借阅记录插进去了但库存没扣,或者反过来。
// BorrowServlet.java 借书核心逻辑 public void borrowBook(String readerId, String bookId) throws Exception { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一步:检查库存是否充足 String checkSql = "SELECT stock FROM book WHERE book_id = ? FOR UPDATE"; PreparedStatement checkPs = conn.prepareStatement(checkSql); checkPs.setString(1, bookId); ResultSet rs = checkPs.executeQuery(); int stock = 0; if (rs.next()) { stock = rs.getInt("stock"); } if (stock <= 0) { throw new RuntimeException("图书库存不足"); } // 第二步:插入借阅记录 String insertSql = "INSERT INTO borrow (reader_id, book_id, borrow_time) VALUES (?, ?, NOW())"; PreparedStatement insertPs = conn.prepareStatement(insertSql); insertPs.setString(1, readerId); insertPs.setString(2, bookId); insertPs.executeUpdate(); // 第三步:扣减库存 String updateSql = "UPDATE book SET stock = stock - 1 WHERE book_id = ?"; PreparedStatement updatePs = conn.prepareStatement(updateSql); updatePs.setString(1, bookId); updatePs.executeUpdate(); conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn != null) { conn.rollback(); // 任何一步失败都回滚 } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }这段代码有几个关键点。第一,conn.setAutoCommit(false)是手动开启事务,默认JDBC是每条SQL自动提交的,不开这个借阅记录和库存扣减就是两次独立事务。第二,查询库存用了FOR UPDATE,这是行级锁,防止两个人同时借同一本书的最后一本时出现超卖。第三,rollback保证了原子性——如果插入借阅记录成功但扣库存失败,整个操作回滚,不会留下脏数据。说实话,很多培训机构的JavaWeb项目都不写事务,这个源码里有完整的实现,说明作者是认真做过并发考虑的。
4. 环境搭建与运行:从JDK到Tomcat的完整启动步骤
4.1 本地环境准备:JDK、Tomcat、MySQL版本匹配
跑这个项目的第一步是把环境对齐。我个人推荐三件套:JDK 1.8(注意是8,不是11或17,老项目在JDK11下跑会有权限问题)、Tomcat 8.5或者9.0、MySQL 5.7。MySQL 8.0也能跑,但需要把驱动换成mysql-connector-java-8.0.x,并且连接URL里要加useSSL=false&serverTimezone=Asia/Shanghai,不然会报时区错误。
环境变量方面,JAVA_HOME指向JDK安装目录,CATALINA_HOME指向Tomcat目录,Path里加上%JAVA_HOME%\bin。装完之后在命令行敲java -version和startup.bat验证。这里有个新手容易翻车的地方:很多人装了多个JDK版本,命令行里java -version显示的是15,但JAVA_HOME还指向8,这时候启动Tomcat会因为UnsupportedClassVersionError崩掉。我的习惯是在启动Tomcat前先echo %JAVA_HOME%确认,这个步骤不花十秒钟,能省掉半小时排查时间。
4.2 数据库初始化:导入SQL脚本与连接配置
打开SQL脚本看一眼开头的CREATE DATABASE语句,确认库名。然后用命令行导入:
mysql -u root -p < sql/library.sql或者打开Navicat、DataGrip,新建查询,把整个脚本粘贴进去执行也行。执行完后USE library; SHOW TABLES;确认四张核心表都在。接着改连接配置,打开src/util/DBUtil.java或者db.properties配置文件,把数据库地址、用户名、密码改成你自己的。
# db.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8&useSSL=false jdbc.username=root jdbc.password=123456这里characterEncoding=UTF-8是必须的,它保证数据库连接层面使用UTF-8传输数据,不写的话中文乱码几乎是必然的。useSSL=false是MySQL 5.7和8.0的常见要求,本地开发没必要走SSL加密。如果你的密码有特殊字符比如@,在properties文件里不用转义,但如果是直接写在Java代码里的字符串常量,记得@不需要转义,&需要转义成\&。
4.3 部署到Tomcat:三种常见方式与验证
部署方式我推荐三种,按使用场景排序。第一种是直接把整个WebRoot目录改名为library复制到Tomcat的webapps下;第二种是打成WAR包丢进去;第三种是开发时在IDE里配置Tomcat虚拟目录映射。对学习这个项目来说,第一种最直观,能让你看到Tomcat解压部署的真实过程。
把项目目录放到webapps下之后,启动Tomcat,浏览器访问http://localhost:8080/library/,应该能跳转到登录页。看到默认登录页面后,用脚本初始化的管理员账号密码登录,比如admin/admin123。登录进去后重点验证三件事:新建一本图书、模拟借书操作、查看借阅列表是否展示。如果这三条链路都通,说明数据库连接、DAO层、Servlet跳转都没问题。
有个细节容易忽略:项目里如果有META-INF/context.xml定义了DataSource,那数据源配置可能不在db.properties里,而在那个XML中。我拿到这个包时一开始只改了DBUtil.java里的账号密码,结果启动时一直报连接失败,后来发现context.xml里还写了一份带死密码的JNDI配置,两个配置同时存在时JNDI优先。这个血的教训让我每次拿到新项目都会先全局搜索password=和jdbc:,把所有数据库配置找齐再动手。
5. 避坑指南:配置、乱码、连接池的常见问题排查
5.1 数据库连接报错但MySQL能正常登录
现象:启动Tomcat后访问页面,报Cannot create PoolableConnectionFactory或者Access denied for user,但用命令行mysql -u root -p能正常登录。
原因分三种:第一,项目里配置的密码和实际MySQL密码不一致,这多半是改了db.properties忘了改context.xml;第二,MySQL 8.0的加密规则是caching_sha2_password,旧版驱动不支持,报Public Key Retrieval is not allowed;第三,MySQL连接数被占用完,报Too many connections。
解决:先全局搜索项目文件和配置文件里的所有jdbc连接串,确保只保留一份配置且密码正确;如果是MySQL 8.0,把驱动换成8.x版本,URL加allowPublicKeyRetrieval=true;连接数问题用SHOW PROCESSLIST看有没有大量Sleep连接,这是代码里没关闭Connection的典型症状,检查DAO层有没有在finally里关连接。
5.2 页面中文乱码的三种根源
现象:登录后页面上的书名、作者全是???,或者页面显示正常但往数据库里插入的中文变成乱码。
原因:乱码是JavaWeb项目最玄学的问题,根源无非三个地方。第一,JSP页面本身没有<%@ page contentType="text/html;charset=UTF-8" %>,导致浏览器按默认编码解析;第二,Servlet接收POST参数时没调request.setCharacterEncoding("UTF-8");第三,数据库表或连接URL的字符集不是UTF-8。
解决:检查JSP头部是否有pageEncoding声明,没有就统一加上;在Servlet的doPost第一行加编码设置;数据库表的字符集用ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;强制转换。排查顺序先看数据库里存的对不对,再看页面显示对不对,逐步缩小范围,不要一上来就改一堆代码。
5.3 Tomcat部署后404或ClassNotFoundException
现象:启动Tomcat没报错,但访问http://localhost:8080/library/是404;或者访问页面时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。
原因:404多半是项目部署路径不对——你可能把项目文件夹名字取错了,或者在IDE里部署的Artifact路径和访问URL不一致。ClassNotFoundException则是典型的驱动包没放进WEB-INF/lib,注意不是IDE的Build Path里加了依赖就行,Tomcat运行时是在WEB-INF/lib下找jar包的。
解决:404时先看Tomcat的webapps目录下实际解压出了什么文件夹名和层级,直接到浏览器按实际路径访问;ClassNotFound时把mysql-connector-java-5.1.49.jar复制到WEB-INF/lib下,重启Tomcat。还有个隐蔽问题:如果你在IDE里部署,看下Project Structure里Artifact的Output Layout,确认依赖jar有没有被正确打进去。
5.4 外键约束导致无法删除图书
现象:删除一本已经有借阅记录的图书时,SQL执行报错挂掉,页面直接500。
原因:borrow表有外键关联到book表,如果存在未归还的借阅记录,删除主表记录会违反外键约束。
解决:这是业务逻辑问题,不是代码bug。正确处理是删除前先判断是否存在未归还的借阅记录,存在就禁止删除并提示"该图书有未归还记录";如果想强行删除,需要先删除或归档借阅记录。我在二次开发时给删除操作加了前置校验,判断逻辑就是查borrow表里有没有return_time IS NULL且book_id = ?的记录,有就拦截。
6. 二次开发实战:给系统加一个图书预约功能
这个系统跑通之后,你要往上加功能,思路是固定的:先在MySQL里加表和字段,然后写Bean、写DAO方法、写Servlet、写JSP页面,四步走。我以"图书预约"为例讲一个完整的改造链路。
6.1 数据层扩展:新增预约表
CREATE TABLE reserve ( reserve_id INT AUTO_INCREMENT PRIMARY KEY, reader_id VARCHAR(20) NOT NULL, book_id VARCHAR(20) NOT NULL, reserve_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0, INDEX idx_reader (reader_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status字段用0表示待处理、1表示已通知取书、2表示已取消。这里用TINYINT而不用VARCHAR存储状态,是省空间且方便用int比较的习惯。
6.2 后端与前端改造
然后在ReserveDAO里写插入和查询方法。插入前先查一下该读者是否已经预约过同一本书且status为0,避免重复预约。Servlet里按reader_id查询当前读者的预约列表,转发到新写的reserveList.jsp。
JSP页面直接用JSTL遍历:
<c:forEach items="${reserveList}" var="reserve"> <tr> <td>${reserve.bookId}</td> <td>${reserve.reserveTime}</td> <td> <c:if test="${reserve.status == 0}">待处理</c:if> <c:if test="${reserve.status == 1}">已通知</c:if> <c:if test="${reserve.status == 2}">已取消</c:if> </td> </tr> </c:forEach>6.3 验证方法与收尾
改完重启Tomcat,用读者账号登录,对一本库存为0的书提交预约,然后到预约列表看是否出现记录且状态为待处理。再用管理员账号在后台把status改成1,回到读者端刷新确认状态变化。
这次改造让我养成了个习惯:每次给这种老项目加功能,我一定先画一条请求路径图,比如"预约提交 → ReserveServlet.doPost → ReserveDAO.save → mysql.reserve表",沿着路径把每一层涉及的文件都列出来再动手,改完后按路径反序验证。从那以后我每次接手旧项目都强制走一遍这条流程——先画链路,再写代码,最后按链路回测,踩坑率大幅下降。希望帮到你。
本文还有配套的精品资源,点击获取