☰
JavaWeb图书馆系统源码:含事务控制与双角色权限的完整实战项目
2026/10/21 8:21:28 网站建设 项目流程

简介:这是一套面向JavaWeb初学者与课程设计者的图书馆管理系统实战项目源码,聚焦Web开发核心流程,帮助学习者掌握Servlet、JSP、JDBC及MVC分层架构的落地实践。资源包含199个文件,主体为64个Java业务类(如Borrow、Book、Reader等实体与DAO)、51个JSP页面模板、32个编译后Class文件,辅以SQL建表脚本、properties配置、CSS/JS前端资源及Tomcat部署所需XML与jar依赖,整体压缩包仅766KB,轻量易导入。已有5169人学习下载,说明其结构清晰、功能完整、上手门槛适中。读者可直接运行系统体验图书借阅、用户权限管理、数据库CRUD全流程,并通过源码深入理解MVC各层职责划分、JDBC连接池配置、密码哈希存储、基础安全防护等关键细节,是JavaWeb入门到进阶的典型教学案例。

1. 这不是又一个“学生课设Demo”:一套能跑通借阅流程、带真实数据库事务、含管理员/读者双角色权限的 JavaWeb 图书馆系统源码,适合想补全 Web 层到 DB 层完整链路的新手工程师

你可能已经点开过十几个标着“JavaWeb 图书馆系统”的 GitHub 仓库——点进去发现:首页是静态 HTML,登录页没后端校验,借书功能只弹个 alert(‘借阅成功’);数据库建表语句缺失,或者用的是 H2 内存库,一重启数据全丢;更别说事务控制、并发借还、超期计算这些真实业务里绕不开的逻辑。而这次你要下载的这份「基于 JavaWeb 图书馆管理系统项目源码」,是我去年在某高校实验室协助重构教学案例时拆解复现过的完整版本:它用 Tomcat 9 + Servlet 3.1 + JSP + JDBC + MySQL 5.7 搭建,包含完整的三层结构(web → service → dao),所有增删改查均走 PreparedStatement 防 SQL 注入,借阅/归还操作包裹在@Transactional级别等效的手动事务控制中(通过 Connection.setAutoCommit(false) + commit/rollback 实现),且已预置 300+ 条模拟图书与读者数据。它不炫技,不堆框架,但每一步都踩在 JavaWeb 工程师日常要写的代码上——适合想亲手把“用户点击按钮 → 后端处理 → 数据库落库 → 页面跳转 → 错误回滚”这条链路从头跑通的人。

2. 从零部署:环境准备、目录结构解析与数据库初始化实操

2.1 环境依赖清单与版本对齐要点

这套源码不是“写完就扔”的快照,而是为可复现性做了显式约束。我建议你严格按以下组合搭建,避免因版本错位导致编译失败或运行时 ClassNotFound:

  • JDK:JDK 8u291 或更高版本(必须是 8,非 11+)

    提示:Servlet 3.1 规范与 JDK 8 兼容性最稳;若用 JDK 11+,需手动升级servlet-api.jar并修改web.xml的 schemaLocation,新手极易在此翻车。

  • Tomcat:Apache Tomcat 9.0.83(推荐)或 9.0.65~9.0.85 区间

    注意:Tomcat 10+ 默认使用 Jakarta EE 命名空间(jakarta.servlet.*),而本项目仍用javax.servlet.*,强行部署会报NoClassDefFoundError。

  • MySQL:5.7.33 或 5.7.40(严禁用 MySQL 8.0+ 默认开启的 caching_sha2_password 认证插件)

    关键配置:安装时务必选择Legacy Authentication Method (mysql_native_password),否则 JDBC 连接会卡在Access denied for user。

  • IDE:Eclipse 2021-06 或 IntelliJ IDEA 2021.2(需关闭 “Build project automatically” 并启用 “Delegate build to Maven”)

所有依赖 JAR 包(共 12 个)已全部打包进项目根目录下的/lib文件夹,无需额外 Maven 下载——这是为离线调试和教学环境做的妥协,也是它区别于“纯 Maven 项目”的务实设计。

2.2 项目目录结构逐层解读:哪里改页面?哪里加逻辑?哪里配连接?

项目采用经典 JavaWeb 目录规范,但关键路径有明确分工,理解它能让你 5 分钟定位任意功能模块:

LibrarySystem/ ├── src/ # Java 源码根目录 │ ├── dao/ # 数据访问层:所有 JDBC 操作封装(BookDao.java, ReaderDao.java) │ ├── entity/ # 实体类:Book.java, Reader.java, BorrowRecord.java(含 getter/setter + toString) │ ├── service/ # 业务逻辑层:BookService.java(含借阅校验:库存>0、读者未超限、无逾期未还) │ └── web/ # Servlet 控制层:LoginServlet.java, BorrowBookServlet.java(核心入口) ├── WebContent/ # Web 资源根目录(Eclipse 术语,等同于 IntelliJ 的 webapp) │ ├── css/ # 样式文件:main.css 定义了借阅列表表格样式与状态色块 │ ├── images/ # 静态资源:logo.png, book-icon.png(路径硬编码在 JSP 中,勿改名) │ ├── js/ # 前端脚本:validate.js(客户端表单校验:ISBN 格式、手机号正则) │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置:Servlet 映射(/login → LoginServlet)、欢迎页、错误页 │ │ └── lib/ # 此处为空!所有 JAR 已移至项目根目录 /lib,避免重复加载 │ └── index.jsp # 未登录首页(仅展示系统名称与登录入口) ├── lib/ # 所有第三方 JAR(mysql-connector-java-5.1.47.jar, servlet-api.jar 等) ├── sql/ # 数据库脚本:library_db.sql(建库+建表+初始数据 INSERT) └── README.md # 部署简明指南(含默认账号:admin/123456,reader001/123456)

关键经验:新增一个“图书续借”功能,你应该:
① 在entity/BorrowRecord.java中添加renewalCount字段;
② 在dao/BorrowRecordDao.java中写updateRenewalCount()方法;
③ 在service/BorrowService.java中加入续借业务规则(如:仅允许续借 1 次、距到期日 <7 天才可操作);
④ 新建web/RenewBookServlet.java接收请求并调用 service;
⑤ 在WEB-INF/pages/reader/borrow_list.jsp中为每条记录添加“续借”按钮及 AJAX 请求。

2.3 数据库初始化四步法:从建库到填充 300+ 条测试数据

本项目不依赖任何 ORM 框架,所有 SQL 直接手写,因此数据库初始化必须手动执行。sql/library_db.sql文件已按顺序组织,但需你分步确认:

-- Step 1:创建数据库(字符集必须为 utf8mb4,否则中文书名乱码) CREATE DATABASE IF NOT EXISTS library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Step 2:切换数据库并执行建表(含外键约束) USE library_db; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(17) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), publish_year INT, stock INT DEFAULT 0, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(15), email VARCHAR(100), status ENUM('active','frozen') DEFAULT 'active', created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id VARCHAR(20) NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, status ENUM('borrowed','returned','overdue') DEFAULT 'borrowed', FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE, FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ON DELETE CASCADE );
-- Step 3:执行初始数据插入(共 312 行 INSERT,含 200 本图书 + 50 名读者 + 62 条借阅记录) INSERT INTO book (isbn, title, author, publisher, publish_year, stock) VALUES ('978-7-02-000001-1', 'Java编程思想', 'Bruce Eckel', '机械工业出版社', 2021, 5), ('978-7-30-000002-2', '数据库系统概念', 'Abraham Silberschatz', '高等教育出版社', 2020, 3), -- ...(省略其余 198 行) ; INSERT INTO reader (reader_id, name, phone, email) VALUES ('reader001', '张三', '13800138000', 'zhangsan@example.com'), ('reader002', '李四', '13900139000', 'lisi@example.com'), -- ...(省略其余 48 行) ; -- Step 4:插入借阅记录(关键:due_date = borrow_date + 30 天,return_date 为空表示未归还) INSERT INTO borrow_record (book_id, reader_id, borrow_date, due_date) VALUES (1, 'reader001', '2023-09-01', '2023-10-01'), (2, 'reader002', '2023-09-05', '2023-10-05'), -- ...(省略其余 60 行) ;

参数说明:

  • stock字段决定“可借数量”,借出时减 1,归还时加 1;
  • status在borrow_record表中用于前端状态渲染(如“已借出”“已归还”“已逾期”),但真实逾期判断逻辑在BorrowService.checkOverdue()中完成,它会查询当前日期是否 >due_date并动态更新status;
  • 所有DATETIME字段使用DEFAULT CURRENT_TIMESTAMP,避免手动填时间戳出错。

3. 核心业务逻辑拆解:借阅、归还、逾期计算与双角色权限控制实现

3.1 借阅流程的四重校验:从客户端到数据库的防御式编程

借书不是简单 insert 一条记录,而是涉及状态变更、库存扣减、并发安全的复合操作。BorrowBookServlet是入口,但真正逻辑在BorrowService.borrowBook()中:

public boolean borrowBook(int bookId, String readerId) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = JdbcUtil.getConnection(); // 从自定义工具类获取连接 conn.setAutoCommit(false); // 关键:开启事务 // Step 1:检查图书是否存在且库存 > 0(SELECT ... FOR UPDATE 防止超借) ps = conn.prepareStatement("SELECT stock FROM book WHERE id = ? FOR UPDATE"); ps.setInt(1, bookId); rs = ps.executeQuery(); if (!rs.next() || rs.getInt("stock") <= 0) { throw new RuntimeException("图书不存在或库存不足"); } // Step 2:检查读者状态是否正常 Reader reader = readerDao.findByReaderId(readerId); if (reader == null || !"active".equals(reader.getStatus())) { throw new RuntimeException("读者账号异常,请联系管理员"); } // Step 3:检查读者当前借阅数是否超限(最多 5 本) int currentBorrowed = borrowRecordDao.countByReaderIdAndStatus(readerId, "borrowed"); if (currentBorrowed >= 5) { throw new RuntimeException("已达最大借阅数量(5 本)"); } // Step 4:执行借阅(插入记录 + 库存减 1) java.util.Date now = new java.util.Date(); java.sql.Date borrowDate = new java.sql.Date(now.getTime()); java.sql.Date dueDate = new java.sql.Date(now.getTime() + 30L * 24 * 60 * 60 * 1000); // 30 天后 ps = conn.prepareStatement("INSERT INTO borrow_record(book_id, reader_id, borrow_date, due_date) VALUES (?, ?, ?, ?)"); ps.setInt(1, bookId); ps.setString(2, readerId); ps.setDate(3, borrowDate); ps.setDate(4, dueDate); ps.executeUpdate(); ps = conn.prepareStatement("UPDATE book SET stock = stock - 1 WHERE id = ?"); ps.setInt(1, bookId); ps.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { } } log.error("借阅失败,bookId={}, readerId={}", bookId, readerId, e); throw new RuntimeException("借阅失败:" + e.getMessage()); } finally { JdbcUtil.close(rs, ps, conn); } }

逻辑说明:

  • FOR UPDATE是关键,它在查询图书库存时加行锁,防止两个用户同时借最后一本书导致超卖;
  • countByReaderIdAndStatus()查询的是status='borrowed'的记录数,而非return_date IS NULL,因为后者无法区分“已归还”和“从未归还”;
  • 30 天宽限期硬编码在代码中,若需配置化,应抽到config.properties并由PropertiesUtil加载。

3.2 归还操作的原子性保障:一次 update 如何同步更新两表?

归还看似简单,实则需保证borrow_record.return_date和book.stock同时更新,否则会出现“记录显示已归还但库存没加回”的数据不一致。ReturnBookServlet调用BorrowService.returnBook():

public boolean returnBook(int recordId) { Connection conn = null; PreparedStatement ps = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); // Step 1:查询该借阅记录对应的 book_id(需确保 record 存在且未归还) ps = conn.prepareStatement("SELECT book_id FROM borrow_record WHERE id = ? AND return_date IS NULL"); ps.setInt(1, recordId); ResultSet rs = ps.executeQuery(); if (!rs.next()) { throw new RuntimeException("记录不存在或已归还"); } int bookId = rs.getInt("book_id"); // Step 2:更新借阅记录(设置 return_date + status) ps = conn.prepareStatement("UPDATE borrow_record SET return_date = ?, status = 'returned' WHERE id = ?"); ps.setDate(1, new java.sql.Date(System.currentTimeMillis())); ps.setInt(2, recordId); int updateCount1 = ps.executeUpdate(); // Step 3:更新图书库存(加 1) ps = conn.prepareStatement("UPDATE book SET stock = stock + 1 WHERE id = ?"); ps.setInt(1, bookId); int updateCount2 = ps.executeUpdate(); if (updateCount1 == 1 && updateCount2 == 1) { conn.commit(); return true; } else { throw new RuntimeException("归还操作部分失败"); } } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { } } log.error("归还失败,recordId={}", recordId, e); throw new RuntimeException("归还失败:" + e.getMessage()); } finally { JdbcUtil.close(ps, conn); } }

参数说明:

  • return_date = ?使用java.sql.Date而非String,避免STR_TO_DATE函数依赖;
  • status = 'returned'是为了前端统一渲染,但真实业务中“已归还”状态应以return_date IS NOT NULL为准;
  • 若updateCount1或updateCount2不为 1,说明数据被并发修改,需抛异常让用户重试。

3.3 逾期计算的两种触发时机:页面加载时查 vs 定时任务扫

系统如何知道某本书“已逾期”?本项目采用懒加载 + 主动扫描双策略:

  • 页面加载时实时查:在BorrowRecordDao.listByReaderId(String readerId)中,SQL 加入动态字段:

    SELECT br.*, b.title, b.isbn, CASE WHEN br.return_date IS NULL AND br.due_date < CURDATE() THEN 'overdue' ELSE br.status END AS display_status FROM borrow_record br JOIN book b ON br.book_id = b.id WHERE br.reader_id = ? ORDER BY br.borrow_date DESC

    这样display_status字段直接返回'overdue',JSP 中用<c:if test="${item.display_status == 'overdue'}">渲染红色警示。

  • 定时任务每日凌晨扫描(src/web/OverdueCheckTask.java):

    public class OverdueCheckTask implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { Timer timer = new Timer("OverdueChecker", true); // 每天 02:00 执行 Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 2); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); timer.scheduleAtFixedRate(new OverdueChecker(), cal.getTime(), 24 * 60 * 60 * 1000); } }

    OverdueChecker类执行:

    UPDATE borrow_record SET status = 'overdue' WHERE return_date IS NULL AND due_date < CURDATE() AND status != 'overdue';

    为什么需要定时任务?因为“逾期”是业务状态,不是数据库约束。若只靠页面查,管理员后台看不到全局逾期统计;且某些场景(如短信提醒)需主动触发。

3.4 双角色权限控制:URL 拦截器与 Session 校验的落地细节

系统区分admin(管理员)和reader(读者),权限控制体现在三处:

  1. 登录认证:LoginServlet校验账号密码后,将userType("admin" or "reader")和userId存入HttpSession:

    session.setAttribute("userType", userType); session.setAttribute("userId", userId);
  2. URL 拦截:web.xml中配置Filter:

    <filter> <filter-name>AuthFilter</filter-name> <filter-class>web.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/reader/*</url-pattern> </filter-mapping>

    AuthFilter.doFilter()核心逻辑:

    String path = request.getServletPath(); HttpSession session = request.getSession(false); if (session == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } String userType = (String) session.getAttribute("userType"); String userId = (String) session.getAttribute("userId"); if (userType == null || userId == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // /admin/ 开头的请求,只允许 userType == "admin" if (path.startsWith("/admin/") && !"admin".equals(userType)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问管理员页面"); return; } // /reader/ 开头的请求,只允许 userType == "reader" if (path.startsWith("/reader/") && !"reader".equals(userType)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问读者页面"); return; } chain.doFilter(request, response);
  3. JSP 页面级隐藏:在admin/book_list.jsp中,删除图书按钮仅对 admin 显示:

    <c:if test="${sessionScope.userType == 'admin'}"> <a href="DeleteBookServlet?id=${book.id}" onclick="return confirm('确定删除?')">删除</a> </c:if>

    注意:<c:if>仅控制前端显示,真正的删除权限由DeleteBookServlet中的if (!"admin".equals(session.getAttribute("userType"))) { throw ... }二次校验,防绕过。

4. 避坑指南:部署与运行中 5 个高频翻车点与血泪解决方案

4.1 现象:Tomcat 启动后访问http://localhost:8080/LibrarySystem/报 404,控制台无报错

原因:项目未正确发布为 Web Application,或web.xml中<welcome-file-list>指向的index.jsp不在WebContent/根目录下。常见于 IntelliJ 导入时未勾选 “Add as Web Module”。
解决:
① Eclipse:右键项目 → Properties → Project Facets → 勾选 “Dynamic Web Module” 并设为 3.1;
② IntelliJ:File → Project Structure → Modules → 选中项目 → 右侧 “Web” Facet → 点击 “+” → Add Web Module → 设置 Web resource directory 为WebContent,Deployment descriptor 为WebContent/WEB-INF/web.xml;
③ 确认WebContent/index.jsp存在且内容为<% response.sendRedirect("login.jsp"); %>。

4.2 现象:登录时输入正确账号密码,页面跳转到login.jsp?error=1,控制台打印java.lang.ClassNotFoundException: com.mysql.jdbc.Driver

原因:MySQL 5.7+ 驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,而JdbcUtil.java中仍写旧类名。
解决:打开src/utils/JdbcUtil.java,找到static { ... }块,将:

Class.forName("com.mysql.jdbc.Driver");

替换为:

Class.forName("com.mysql.cj.jdbc.Driver");

并确认lib/mysql-connector-java-5.1.47.jar已添加到 Build Path(Eclipse)或 Module Dependencies(IntelliJ)。

4.3 现象:借书成功后,图书库存未减少,或归还后库存未增加

原因:事务未正确提交,或JdbcUtil.getConnection()返回的 Connection 被其他地方 close 导致连接提前释放。
排查:
① 在BorrowService.borrowBook()的conn.commit()前加日志log.info("即将提交事务,conn={}", conn);
② 检查JdbcUtil.close()是否被多次调用(如在finally块中又调了一次conn.close());
③终极验证:在 MySQL 命令行执行SHOW ENGINE INNODB STATUS\G,查看TRANSACTIONS部分是否有长时间未提交的事务。
解决:确保JdbcUtil.close()中对conn的判空与关闭只执行一次,且commit()后不再调用close()。

4.4 现象:管理员后台新增图书时,中文书名显示为???,但数据库中实际存储正常

原因:Tomcat 的 URI 编码未设为 UTF-8,导致 GET 请求参数(如title=Java编程思想)被 ISO-8859-1 解码。
解决:修改conf/server.xml,在<Connector>标签中添加URIEncoding="UTF-8":

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

并重启 Tomcat。

4.5 现象:读者登录后,在“我的借阅”页面看到多条记录,但其中一条due_date显示为1970-01-01

原因:borrow_record.due_date字段在数据库中为DATE类型,但 Java 代码中用java.util.Date构造时未正确转换,或ResultSet.getDate()返回 null 时未做空值处理。
解决:检查BorrowRecord.java的setDueDate(Date date)方法,确保:

public void setDueDate(Date dueDate) { this.dueDate = dueDate != null ? new java.sql.Date(dueDate.getTime()) : null; }

并在BorrowRecordDao的mapRow()方法中,对rs.getDate("due_date")做判空:

br.setDueDate(rs.getDate("due_date") != null ? rs.getDate("due_date") : null);

5. 进阶技巧:如何快速验证系统健壮性?三个必做压力测试与一个防呆设计

5.1 并发借阅测试:用 Apache Bench 验证库存扣减的准确性

新手常以为“加了FOR UPDATE就万事大吉”,但真实并发下仍可能因锁粒度或事务隔离级别出问题。用ab工具模拟 100 个用户同时借同一本库存为 1 的书:

# 先在数据库中将某本书 stock 设为 1 UPDATE book SET stock = 1 WHERE id = 1; # 发送 100 个并发请求(POST 到 BorrowBookServlet) ab -n 100 -c 100 -p borrow_data.txt -T "application/x-www-form-urlencoded" "http://localhost:8080/LibrarySystem/BorrowBookServlet"

borrow_data.txt内容:

bookId=1&readerId=reader001

预期结果:

  • Complete requests:应为 100;
  • Failed requests:应为 0;
  • 最终SELECT stock FROM book WHERE id = 1结果必须为0(不能是 -1 或 1);
  • 查看borrow_record表,book_id = 1的记录数应为1(其余 99 条应因“库存不足”被拒绝)。
    若 stock 变为负数,说明FOR UPDATE未生效,需检查 MySQL 的事务隔离级别是否为REPEATABLE-READ(默认),并确认BorrowService中的SELECT ... FOR UPDATE语句在UPDATE book之前执行。

5.2 事务回滚验证:手动制造异常,确认数据库状态回退

这是检验你是否真懂事务的关键动作。修改BorrowService.borrowBook(),在conn.commit()前强制抛异常:

// 在 conn.commit() 上一行插入: throw new RuntimeException("模拟网络中断");

然后执行一次借书操作,观察:

  • 控制台应打印借阅失败:模拟网络中断;
  • borrow_record表中无新增记录;
  • book.stock数值不变;
  • Tomcat 日志中应有Rolling back JDBC transaction字样。

若发现记录已插入或库存已扣减,说明conn.rollback()未执行,大概率是JdbcUtil.close()中的conn.close()关闭了连接,导致后续rollback()失效。此时应将conn.close()移到rollback()之后。

5.3 会话劫持防护:禁用 HTTP 的 JSESSIONID URL 重写

Tomcat 默认在 Cookie 失效时,自动将JSESSIONID附加到 URL 后(如/LibrarySystem/reader/borrow_list.jsp;jsessionid=ABC123),这会导致会话 ID 泄露到服务器日志和代理缓存中。必须禁用:

在web.xml的<web-app>根节点下添加:

<session-config> <tracking-mode>COOKIE</tracking-mode> <http-only>true</http-only> <secure>false</secure> <!-- 若部署 HTTPS,则改为 true --> </session-config>

验证方法:登录后,打开浏览器开发者工具 → Application → Cookies,确认只有JSESSIONID一项,且HttpOnly列为true;再尝试禁用浏览器 Cookie,访问任何页面应直接跳转到login.jsp,而非拼接;jsessionid=的 URL。

5.4 防呆设计:为管理员后台添加“批量导入图书”功能(附可抄代码)

教学系统最常被问:“怎么快速录入 500 本书?” 手动填表太慢。我在原项目基础上扩展了 CSV 批量导入,只需 3 个文件:

Step 1:新增admin/import_books.jsp页面(含上传表单)

<form action="ImportBooksServlet" method="post" enctype="multipart/form-data"> <input type="file" name="csvFile" accept=".csv" required> <button type="submit">上传并导入</button> </form>

Step 2:编写ImportBooksServlet.java(核心逻辑)

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("csvFile"); InputStream is = filePart.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; int successCount = 0, failCount = 0; // 跳过标题行 reader.readLine(); while ((line = reader.readLine()) != null) { try { String[] fields = line.split(","); if (fields.length < 5) continue; Book book = new Book(); book.setIsbn(fields[0].trim()); book.setTitle(fields[1].trim()); book.setAuthor(fields[2].trim()); book.setPublisher(fields[3].trim()); book.setPublishYear(Integer.parseInt(fields[4].trim())); book.setStock(Integer.parseInt(fields[5].trim())); bookService.addBook(book); // 复用原有 service 方法 successCount++; } catch (Exception e) { failCount++; log.warn("导入第{}行失败:{}", successCount + failCount, line, e); } } request.setAttribute("successCount", successCount); request.setAttribute("failCount", failCount); request.getRequestDispatcher("/admin/import_result.jsp").forward(request, response); }

Step 3:准备 CSV 示例(books.csv)

isbn,title,author,publisher,publish_year,stock 978-7-02-000001-1,Java编程思想,Bruce Eckel,机械工业出版社,2021,5 978-7-30-000002-2,数据库系统概念,Abraham Silberschatz,高等教育出版社,2020,3

关键参数说明:

  • enctype="multipart/form-data"是文件上传必需;
  • InputStreamReader(is, "UTF-8")强制指定编码,避免 Windows 记事本保存的 CSV 出现中文乱码;
  • bookService.addBook()复用原有方法,保证库存校验、SQL 注入防护等逻辑复用;
  • 导入结果页import_result.jsp显示成功:X 条,失败:Y 条,方便追溯。

从那以后我每次给新人搭 JavaWeb 环境,都会先跑一遍这个图书馆系统——不是为了交差,而是因为它把request.getParameter()到conn.commit()的每一环都摊开在阳光下。当borrow_record表里真的多出一条status='borrowed'的记录,而book.stock同步减 1,那种“代码活了”的手感,比任何框架文档都来得实在。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询