简介:本资源是一套基于JavaWeb技术栈开发的完整图书管理系统,面向计算机专业毕业设计学生及JavaWeb初学者,提供可直接部署运行的实战项目方案。系统采用JSP+Servlet+MySQL+Tomcat经典架构,覆盖图书馆核心业务流程,包括管理员设置、读者与图书全生命周期管理、借阅续借归还、多维度查询及到期提醒等功能,具备良好的功能完整性与工程规范性。压缩包共202个文件,含50个JSP页面(实现前后端交互)、33个Java源码与33个Class字节码(构成MVC分层逻辑)、23个GIF图标与6个JPG图片(支撑界面展示)、15个JAR依赖库及SQL数据库脚本等,整体4.57MB,结构清晰、模块分明,便于学习源码组织与调试排错。目前已有241人下载学习,配套内容涵盖可执行源码、建库脚本、详细功能说明及典型DAO类(如BookDAO、BorrowDAO、ReaderDAO等),助读者快速掌握JavaWeb项目开发全流程与企业级数据库操作实践。
1. 这不是另一个“Hello World”JSP项目:它是一套能直接进答辩、跑通借阅全流程的图书管理系统实战源码
你手头正赶毕设,导师刚说“系统要有真实业务闭环”,而你还在网上搜“jsp怎么连mysql”;或者你刚学完Servlet生命周期,想找个不拼凑、不缺表、不报NullPointerException的JavaWeb项目练手——那这套基于JavaWeb+JSP+Tomcat+MySQL的图书管理系统,就是为你卡在临界点时准备的。它不是教学Demo,而是完整覆盖“管理员登录→添加读者类型→录入图书→借书→续借→归还→到期提醒→导出查询结果”全链路的可运行系统。所有DAO层接口清晰、Service逻辑分层合理、JSP页面带基础校验和跳转逻辑,连数据库字段命名都遵循book_name而非bookName这种半驼峰混乱写法。它不炫技,不堆Spring Boot新特性,但每一行代码都在解决真实问题:比如BorrowDAO.class里对borrow_date和return_date的空值安全处理,ReaderTypeDAO.class中对类型权重排序的SQL优化,甚至BorrowForm.class里对借阅数量上限的硬编码校验——这些细节,才是你调试到凌晨三点后真正想抄的作业。
2. 从零启动:环境搭建、数据库导入与项目部署三步闭环
2.1 环境版本选型:为什么必须是Tomcat 7+ 和 MySQL 5+?
这不是拍脑袋定的。Tomcat 7 是 JSP 2.2 和 Servlet 3.0 的分水岭——本项目中web.xml使用了<welcome-file-list>声明首页,且<servlet>配置未用注解(即纯XML驱动),这要求容器至少支持Servlet 3.0规范;而MySQL 5.5起才稳定支持utf8mb4字符集,项目中reader_name字段明确存中文姓名,若用MySQL 4.x或早期5.0,插入“范冰”这类带生僻字的读者名会直接报错Incorrect string value。更关键的是,BookDAO.class里有一处GROUP BY book_type_id HAVING COUNT(*) > 1的统计查询,MySQL 5.7默认开启sql_mode=STRICT_TRANS_TABLES,若环境低于此版本,该语句可能静默返回空结果而非报错,导致“热门图书排行”功能失效却查不出原因。所以,别图省事用旧版——我当年在实验室用Tomcat 6跑这个项目,卡在HttpSession.setAttribute()序列化失败整整两天,最后发现是Tomcat 6对java.util.Date序列化兼容性有Bug,升级到7.0.96后秒解。
2.2 数据库脚本执行:不只是source xxx.sql,还有字符集和外键陷阱
项目提供的SQL脚本(通常命名为library_db.sql)需按以下顺序执行,顺序错一步,后续全崩:
# 1. 登录MySQL,强制指定字符集避免乱码 mysql -u root -p --default-character-set=utf8mb4 # 2. 创建数据库并指定字符集(关键!) CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切换数据库 USE library_db; # 4. 执行建表脚本(注意路径) SOURCE /path/to/library_db.sql;提示:若执行时报错
ERROR 1064 (42000),大概率是SQL文件头部有BOM头(Windows记事本保存的UTF-8带签名)。用VS Code打开该SQL文件,右下角点击编码 → “Save with Encoding” → 选UTF-8(无BOM)。这是新手最常踩的坑,报错信息完全不提BOM,只说语法错。
脚本中关键表结构需特别注意:
reader表的reader_type_id字段为TINYINT UNSIGNED,对应reader_type表的主键,但ReaderTypeDAO.class里查询时用了SELECT * FROM reader_type WHERE type_id = ?,若type_id定义为TINYINT而实际插入值超127(如设为200),MySQL会自动截断为127,导致读者类型匹配失败;borrow表的book_id和reader_id均为INT UNSIGNED,但BookDAO.class中getBookById(int id)方法参数是int(Java有符号),当数据库ID存入2147483647(INT最大值)时,Java端接收会溢出为-1,引发NullPointerException。解决方案:要么改Java方法参数为long,要么在建表时将ID字段设为BIGINT——本项目选择后者,故SQL脚本中已修正。
2.3 Eclipse/IDEA部署:绕过“HTTP Status 404 – Not Found”的真实路径映射
在IDEA中配置Tomcat时,不要直接Import项目为“Maven”或“Gradle”,因为这是纯JavaWeb项目,无pom.xml。正确流程:
- File → New → Project → 选
Java Enterprise→Web Application→ 勾选Create web.xml→ Finish; - 将下载包中的
src目录内容复制到新建项目的src/main/java(若无此目录则手动创建); - 将
WebContent目录(含WEB-INF/web.xml、index.jsp等)整体复制到新建项目的web目录(IDEA默认叫webapp,需重命名为web); - 关键一步:在
Project Structure→Artifacts中,确认Output directory指向web/WEB-INF/classes,且Available Elements里已包含所有.class文件(特别是BorrowDAO.class等编译后的类); - 配置Tomcat Server时,在
Deployment选项卡中,Application context必须设为/library(或其他非/的路径),否则访问http://localhost:8080/会404——因为项目首页是index.jsp,而Tomcat根路径默认映射到ROOT应用,你的项目需要独立上下文。
注意:若启动后访问
http://localhost:8080/library/仍报404,请检查web.xml中<welcome-file-list>是否为:<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>而不是
login.jsp或admin/index.jsp。本项目入口统一为index.jsp,它会根据session判断跳转至登录页或主页。
3. 核心模块拆解:DAO层设计、JSP交互逻辑与业务边界控制
3.1 DAO层真相:不是简单CRUD,而是事务与空值防御的战场
看BorrowDAO.class,表面是借阅记录增删,实则藏着三层防御:
// BorrowDAO.java public boolean borrowBook(int bookId, int readerId) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); // 复用连接池,非每次new conn.setAutoCommit(false); // 开启事务,借书+减库存必须原子性 // 步骤1:检查图书是否可借(status=1且stock>0) String checkSql = "SELECT stock, status FROM book WHERE book_id = ?"; ps = conn.prepareStatement(checkSql); ps.setInt(1, bookId); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("status") != 1 || rs.getInt("stock") <= 0) { throw new SQLException("图书不可借阅"); } // 步骤2:插入借阅记录 String insertSql = "INSERT INTO borrow(book_id, reader_id, borrow_date) VALUES(?, ?, NOW())"; ps = conn.prepareStatement(insertSql); ps.setInt(1, bookId); ps.setInt(2, readerId); ps.executeUpdate(); // 步骤3:更新图书库存 String updateSql = "UPDATE book SET stock = stock - 1 WHERE book_id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, bookId); ps.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } e.printStackTrace(); return false; } finally { DBUtil.close(ps, conn); // 必须关闭,否则连接泄漏 } }参数说明:
bookId和readerId必须为正整数,DAO层未做校验,依赖上层BorrowForm.class的validate()方法(见3.3节);NOW()函数由MySQL执行,确保时间精准,避免Java端new Date()与数据库时区不一致;stock = stock - 1用SQL原生运算,防止并发借阅时超借(如两个线程同时读到stock=1,都执行-1后变-1)。
3.2 JSP页面逻辑:表单验证、状态跳转与防重复提交
borrow.jsp不是静态页面,它嵌入了三层控制逻辑:
<!-- borrow.jsp 片段 --> <% // 1. 防未登录访问 if (session.getAttribute("admin") == null && session.getAttribute("reader") == null) { response.sendRedirect("index.jsp"); return; } %> <!-- 2. 表单提交前JS校验(防前端绕过) --> <script> function validateForm() { var bookId = document.getElementById("bookId").value; var readerId = document.getElementById("readerId").value; if (!bookId || !readerId) { alert("请填写完整信息!"); return false; } // 关键:禁用提交按钮,防双击重复提交 document.getElementById("submitBtn").disabled = true; return true; } </script> <!-- 3. 后端接收时二次校验(BorrowForm.class) --> <% BorrowForm form = new BorrowForm(request); if (form.isValid()) { // 检查ID是否存在、是否已借出等 if (new BorrowDAO().borrowBook(form.getBookId(), form.getReaderId())) { request.setAttribute("msg", "借阅成功!"); } else { request.setAttribute("msg", "借阅失败,请重试。"); } } else { request.setAttribute("msg", "参数错误:" + form.getErrors()); } %>避坑点:
session.getAttribute("admin")判空必须用== null,不能用.equals(null),否则NPE;document.getElementById("submitBtn").disabled = true是防重复提交的底线,比后端加Token更直接有效;BorrowForm.class的isValid()方法会查borrow表中book_id=? AND return_date IS NULL,确保同一本书未被他人借走——这是业务强约束,不是可选项。
3.3 业务边界控制:为什么“续借”功能必须限制次数?
ManagerDAO.class中renewBorrow(int borrowId)方法看似简单,但藏着业务规则:
public boolean renewBorrow(int borrowId) { // 规则1:只能续借未归还的记录 String checkSql = "SELECT return_date FROM borrow WHERE borrow_id = ? AND return_date IS NULL"; // 规则2:每人每本书最多续借2次(字段renew_times默认0,最大值2) String updateSql = "UPDATE borrow SET renew_times = renew_times + 1 WHERE borrow_id = ? AND renew_times < 2"; // 规则3:续借后归还日期延后7天(非固定值,可配) String extendSql = "UPDATE borrow SET due_date = DATE_ADD(due_date, INTERVAL 7 DAY) WHERE borrow_id = ?"; }为什么设上限?
图书馆管理规范要求借阅周期可控。若不限制,用户可能无限续借,导致热门图书长期无法流通。本项目将renew_times设为TINYINT(0~255),但业务层硬编码< 2,即最多2次。若需调整,只需改此处数字,无需动数据库结构——这是业务规则与数据模型解耦的典型做法。
4. 避坑指南:那些让项目“明明能跑却总报错”的5个血泪现场
4.1 现象:启动Tomcat后访问/library/index.jsp显示空白页,日志无报错
原因:JSP编译失败,但Tomcat默认不打印JSP编译错误到控制台。常见于web.xml中<jsp-config>缺失或<jsp-property-group>配置错误。本项目web.xml未显式配置JSP,依赖Tomcat默认行为,但若你曾手动添加过<jsp-config>节点且<url-pattern>写错(如*.jspa),会导致所有JSP不解析。
解决:删除web.xml中所有<jsp-config>相关节点,确保其为空或完全不存在;清空Tomcat/work/Catalina/localhost/library/目录(强制重新编译JSP)。
4.2 现象:登录管理员账号后跳转到admin/main.jsp,但页面显示HTTP Status 404
原因:main.jsp中引用了<%@ include file="/common/header.jsp" %>,而/common/header.jsp路径错误。本项目实际路径是WebContent/common/header.jsp,但web.xml中<welcome-file-list>设置后,相对路径解析基准是/library/,因此/common/header.jsp应为common/header.jsp(去开头斜杠)。
解决:将所有<%@ include file="/xxx.jsp" %>改为<%@ include file="xxx.jsp" %>,或统一用<jsp:include page="xxx.jsp"/>(动态包含,路径解析更可靠)。
4.3 现象:添加新读者时,reader_name存入数据库变成乱码“???”
原因:MySQL连接URL未指定字符集。DBUtil.java中getConnection()方法里的URL应为:
String url = "jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8";若漏掉characterEncoding=utf8mb4,即使数据库和表设了utf8mb4,JDBC驱动仍用默认latin1传输。
解决:检查DBUtil.java第12行(通常),补全URL参数;重启Tomcat。
4.4 现象:执行SELECT * FROM borrow WHERE return_date IS NULL查不到未归还记录,但手动UPDATE borrow SET return_date=NULL后能查到
原因:MySQL中return_date字段定义为DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',而非NULL。IS NULL永远不成立,因为默认值是零日期,不是NULL。BorrowDAO.class中所有return_date IS NULL查询均失效。
解决:修改borrow表结构:
ALTER TABLE borrow MODIFY COLUMN return_date DATETIME NULL; UPDATE borrow SET return_date = NULL WHERE return_date = '0000-00-00 00:00:00';并同步修改Borrow.class中returnDate字段的getter/setter,初始化为null而非new Date(0)。
4.5 现象:在IDEA中Debug时,断点打在BookDAO.class的getBookById()方法内,但永远不触发
原因:你调试的是.class文件,但IDEA默认关联的是反编译的源码(无行号信息),导致断点无效。项目提供的是编译后的.class,未附.java源码(部分下载包有,但常被压缩工具损坏)。
解决:将下载包中的src目录(含所有.java文件)复制到项目src/main/java,然后在IDEA中右键src→Mark Directory as→Sources Root;删除web/WEB-INF/classes下所有.class,重新Build →Rebuild Project,让IDEA用源码编译而非加载旧class。
5. 进阶技巧:把“能跑”变成“能交差”,三个答辩级优化实操
5.1 给借阅到期提醒加邮件通知:不用Spring Mail,纯JavaMail API轻量集成
答辩时老师常问:“系统怎么提醒读者还书?”本项目原版只在admin/borrow_query.jsp中用SQL查due_date < NOW()标红显示,但真实场景需主动通知。我们用JavaMail API补上,不引入任何框架:
- 下载
javax.mail.jar(JavaEE 8+需用jakarta.mail.jar),放入WEB-INF/lib/; - 在
ManagerDAO.class中新增方法:
public void sendDueReminder() { String sql = "SELECT r.reader_email, b.book_name, br.due_date " + "FROM borrow br " + "JOIN reader r ON br.reader_id = r.reader_id " + "JOIN book b ON br.book_id = b.book_id " + "WHERE br.return_date IS NULL AND br.due_date < DATE_ADD(NOW(), INTERVAL 1 DAY)"; // 查询明天到期的记录 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { String to = rs.getString("reader_email"); String subject = "【图书管理系统】您的借阅即将到期"; String content = String.format("尊敬的读者,您借阅的《%s》将于%s到期,请及时归还。", rs.getString("book_name"), rs.getDate("due_date")); sendEmail(to, subject, content); // 调用下方sendEmail方法 } } catch (SQLException e) { e.printStackTrace(); } } private void sendEmail(String to, String subject, String content) { Properties props = new Properties(); props.put("mail.smtp.host", "smtp.163.com"); // 以163邮箱为例 props.put("mail.smtp.auth", "true"); Session session = Session.getInstance(props, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("your_email@163.com", "your_auth_code"); } }); try { Message message = new MimeMessage(session); message.setFrom(new InternetAddress("your_email@163.com")); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); message.setText(content); Transport.send(message); } catch (MessagingException e) { e.printStackTrace(); } }关键参数说明:
smtp.163.com需替换为你邮箱服务商的SMTP地址(QQ邮箱为smtp.qq.com,Gmail为smtp.gmail.com);your_auth_code不是邮箱密码,而是163/QQ邮箱的“授权码”,需在邮箱设置中开启SMTP服务并生成;DATE_ADD(NOW(), INTERVAL 1 DAY)查明日到期,避免当天发送导致读者来不及处理。
5.2 把“图书借阅查询”导出为Excel:Apache POI零依赖实现
答辩演示时,老师可能要求“导出查询结果”。本项目原版只有页面表格,我们加一个exportExcel.jsp:
<%@ page import="org.apache.poi.hssf.usermodel.*" %> <%@ page import="java.io.*" %> <% // 从request获取查询条件(如book_name模糊查询) String bookName = request.getParameter("bookName"); String sql = "SELECT b.book_name, r.reader_name, br.borrow_date, br.due_date " + "FROM borrow br JOIN book b ON br.book_id=b.book_id " + "JOIN reader r ON br.reader_id=r.reader_id " + "WHERE br.return_date IS NULL"; if (bookName != null && !bookName.trim().isEmpty()) { sql += " AND b.book_name LIKE ?"; } HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("借阅查询结果"); String[] headers = {"图书名称", "读者姓名", "借阅日期", "应还日期"}; HSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 执行查询并写入Excel try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (bookName != null && !bookName.trim().isEmpty()) { ps.setString(1, "%" + bookName + "%"); } ResultSet rs = ps.executeQuery(); int rowNum = 1; while (rs.next()) { HSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(rs.getString("book_name")); row.createCell(1).setCellValue(rs.getString("reader_name")); row.createCell(2).setCellValue(rs.getDate("borrow_date").toString()); row.createCell(3).setCellValue(rs.getDate("due_date").toString()); } } // 输出Excel文件 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=borrows_" + System.currentTimeMillis() + ".xls"); workbook.write(response.getOutputStream()); %>部署要点:
- 下载
poi-3.17.jar(兼容JDK 1.7+),放入WEB-INF/lib/; - 导出链接加在
admin/borrow_query.jsp中:<a href="exportExcel.jsp?bookName=<%=request.getParameter("bookName")%>">导出Excel</a>; HSSFWorkbook生成.xls格式(兼容老Office),若需.xlsx,换用XSSFWorkbook和poi-ooxml包。
5.3 系统性能压测:用JMeter模拟100并发借书,定位DAO层瓶颈
答辩可能被问“系统能支撑多少人同时使用?”我们用JMeter实测:
- 安装JMeter,新建Thread Group,设
Number of Threads = 100,Ramp-up Period = 10(10秒内启动100线程); - 添加HTTP Request:
- Server Name:
localhost - Port Number:
8080 - Path:
/library/servlet/BorrowServlet(假设借书请求由该Servlet处理) - Parameters:
bookId=1&readerId=1(用真实存在的ID);
- Server Name:
- 添加View Results Tree和Aggregate Report监听器;
- 运行后观察:若
90% Line响应时间>2s,或Error Rate>5%,说明瓶颈在DAO层。
定位与优化:
- 查Tomcat日志,若大量
Waiting for connection,说明连接池太小。修改DBUtil.java中BasicDataSource配置:dataSource.setMaxActive(50); // 默认20,调高至50 dataSource.setMaxIdle(20); dataSource.setMinIdle(5); - 若
BorrowDAO.borrowBook()耗时长,重点优化SELECT stock, status FROM book WHERE book_id = ?——给book_id加索引:ALTER TABLE book ADD INDEX idx_book_id (book_id);
从那以后我每次交付JavaWeb项目,都强制走一遍JMeter压测:先跑10线程看基线,再50、100递增,截图Response Times Over Time图表放进答辩PPT。不是为了炫技,是让老师一眼看到——这系统不是玩具,它经得起真实压力。希望帮到你。
本文还有配套的精品资源,点击获取