资讯详情 Java图书管理系统源码实战:环境搭建、核心模块与避坑指南
📅 2026/10/14 22:50:36
简介这是一套面向Java初学者与课程设计学习者的图书管理系统完整源码基于Java Swing构建图形界面采用SQL Server作为数据库适合用于毕业设计参考、课程作业实践或Java桌面应用入门练手。资源包共35个文件以23个java源文件为核心另含2个sql建库脚本、1个pom.xml构建配置、1个yaml配置文件及png界面截图、md说明与pdf报告文档压缩包约1.51MB结构清晰便于按模块阅读。系统覆盖书籍增删改查与库存管理、借书还书及借阅历史、读者卡注册与查询并支持通过配置文件更换数据库、数据库重置以及多线程并发借书处理能帮助读者理解Swing界面与数据库交互的完整实现思路。目前已有79人学习下载适合需要一份可直接运行、便于二次开发的Java管理类项目参考的读者。1. 图书管理系统源码拆包从解压到跑通我踩过的那些坑前阵子帮一个做行政的朋友处理他们内部图书角的借阅台账Excel 已经撑不住了——三张表关联查询全靠 VLOOKUP借书超期提醒得靠人肉比对日期。我翻了一圈找到一份基于 Java 的图书管理系统源码包解压后直接跑起来图书录入、借阅归还、超期查询这些核心功能都在。这份资源适合谁一是刚学完 Java Web 想做课程设计的学生二是中小型图书室、社区阅览室需要一套轻量级管理工具的开发者三是想拿一个完整 CRUD 项目练手、理解 MVC 分层架构的初级工程师。它不是那种企业级高并发系统但胜在结构清晰、依赖少、改起来不费劲。下面我把从环境搭建到二次开发的完整路径拆开讲包括几个让我折腾了半天的配置坑。2. 环境搭建与数据库初始化JDK、Tomcat、MySQL 的版本对齐2.1 先看依赖清单别急着导入 IDE拿到源码包第一件事不是打开 IDEA 或 Eclipse而是先看项目根目录下有没有pom.xml或者lib文件夹。这份图书管理系统用的是传统 Java Web 项目结构没有走 Maven依赖 jar 包直接放在WebContent/WEB-INF/lib下面。常见做法是先把这些 jar 包列个清单确认 JDK 版本和 Servlet 版本是否匹配。我一般会先看web.xml里的web-app版本声明。如果是 3.0 以上说明支持注解配置Tomcat 7 及以上都能跑如果是 2.5那就得用 Tomcat 6 或 7Tomcat 8 以上会报web.xml解析错误。这份源码用的是 Servlet 3.0对应 Tomcat 7 或 8 都行但 JDK 必须锁在 1.7 或 1.8别用 JDK 11 以上否则javax.servlet包名会找不到——这是第一个血泪坑。数据库方面源码里用的是 MySQL连接配置通常在src/db.properties或src/c3p0-config.xml里。先别改代码把配置文件打开看jdbcUrl指向哪个库名。常见的是library_db或book_manage记下来等会儿建库要用。2.2 建库建表字符集和引擎别选错打开 MySQL 客户端先建库。字符集用utf8mb4排序规则用utf8mb4_general_ci别用utf8——图书名称里出现生僻字或者特殊符号时utf8会截断导致插入失败。存储引擎选 InnoDB因为借阅记录表需要事务支持MyISAM 不支持回滚借书扣库存和写借阅记录如果中途失败数据就对不上了。-- 建库字符集必须 utf8mb4 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 切库 USE library_db; -- 图书表核心字段是 isbn、书名、作者、库存 CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), total_copies INT DEFAULT 1, available_copies INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 借阅记录表status 0 表示已归还1 表示借出中 CREATE TABLE borrow_records ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, borrower_name VARCHAR(30) NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, status TINYINT DEFAULT 1, FOREIGN KEY (book_id) REFERENCES books(id) ) ENGINEInnoDB;建完表把源码里sql文件夹下的初始数据脚本跑一遍通常会有几十条测试图书数据。注意看脚本里有没有DROP TABLE语句有的话先注释掉别把刚建好的表删了。2.3 改配置文件路径、账号、端口三处必查数据库建好之后回到源码找到数据库连接配置文件。如果是db.properties改这几项# 数据库驱动MySQL 5.x 用 com.mysql.jdbc.Driver # MySQL 8.x 用 com.mysql.cj.jdbc.Driver别搞混 jdbc.drivercom.mysql.jdbc.Driver # 连接地址库名要和刚才建的一致 jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8mb4 # 账号密码换成你自己的 jdbc.usernameroot jdbc.passwordyour_password改完配置别急着启动 Tomcat。先检查WEB-INF/classes目录下有没有编译好的.class文件。如果没有说明源码还没编译需要在 IDE 里 Build 一下。Eclipse 用户右键项目 → Build Path → Configure Build Path确认 JRE 是 1.8IDEA 用户在 Project Structure 里把 SDK 设成 1.8Language Level 也设成 8。最后一步把项目部署到 Tomcat。Eclipse 里右键项目 → Run As → Run on ServerIDEA 里配置 Tomcat ServerDeployment 选项卡添加 Artifact。启动后浏览器访问http://localhost:8080/项目名/login.jsp能看到登录页就算环境通了。如果报 404检查项目名大小写如果报 500看 Tomcat 控制台日志十有八九是数据库连接失败。3. 核心功能模块拆解登录鉴权、图书 CRUD 与借阅逻辑3.1 登录鉴权Session 存什么、过滤器怎么配登录模块的入口是LoginServlet接收前端表单提交的username和password查users表比对。源码里密码是明文存储的这在课程设计里常见但如果你要放到实际环境用建议改成 MD5 或 BCrypt 加盐哈希。登录成功后把用户对象塞进HttpSessionkey 通常是user或loginUser。// LoginServlet 核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) { String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null) { // 登录成功写 Session request.getSession().setAttribute(user, user); response.sendRedirect(bookList.jsp); } else { // 登录失败回传错误信息 request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }参数说明request.getParameter拿的是表单字段名要和 JSP 里input name...保持一致。sendRedirect是重定向地址栏会变forward是转发地址栏不变。登录失败用转发是为了把错误信息带回原页面。鉴权过滤器AuthFilter拦截所有/*请求检查 Session 里有没有user对象。没有就跳回登录页。注意放行登录页和静态资源否则会死循环。// AuthFilter 放行规则 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String uri request.getRequestURI(); // 登录页、登录接口、静态资源放行 if (uri.endsWith(login.jsp) || uri.endsWith(LoginServlet) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, res); return; } // 其他请求检查 Session if (request.getSession().getAttribute(user) null) { response.sendRedirect(login.jsp); } else { chain.doFilter(req, res); } }3.2 图书 CRUD分页查询的 SQL 写法与参数传递图书列表页bookList.jsp通过BookServlet?actionlist拿数据。源码里分页用的是LIMIT子句这是 MySQL 的方言换成 Oracle 或 SQL Server 得改。分页参数一般有两个pageNo当前页码和pageSize每页条数默认pageSize设 10 或 15。// BookDao 分页查询 public ListBook findByPage(int pageNo, int pageSize) { ListBook list new ArrayList(); String sql SELECT * FROM books ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 起始偏移量 (页码 - 1) * 每页条数 ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setIsbn(rs.getString(isbn)); book.setTitle(rs.getString(title)); book.setAuthor(rs.getString(author)); book.setAvailableCopies(rs.getInt(available_copies)); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } return list; }参数说明LIMIT第一个参数是偏移量从 0 开始第二个参数是返回行数。页码从 1 开始算所以偏移量是(pageNo - 1) * pageSize。如果页码传 0 或负数SQL 会报错前端要做校验。新增图书走BookServlet?actionadd表单字段包括isbn、title、author、publisher、totalCopies。插入前先查isbn是否已存在存在就提示重复。删除图书用actiondeleteidxx注意外键约束——如果这本书有未归还的借阅记录直接删会报foreign key constraint fails。常见做法是软删除加一个is_deleted字段查询时过滤掉。3.3 借阅与归还库存扣减和事务边界借书逻辑在BorrowServlet里核心是两步扣库存、写借阅记录。这两步必须在一个事务里否则扣了库存没写记录或者写了记录没扣库存数据就乱了。// BorrowService 借书事务 public boolean borrowBook(int bookId, String borrowerName) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步检查库存并扣减 String checkSql SELECT available_copies FROM books WHERE id ? FOR UPDATE; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs checkPs.executeQuery(); if (!rs.next() || rs.getInt(available_copies) 0) { conn.rollback(); return false; // 库存不足 } String updateSql UPDATE books SET available_copies available_copies - 1 WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); updatePs.executeUpdate(); // 第二步写借阅记录默认借期 30 天 String insertSql INSERT INTO borrow_records (book_id, borrower_name, borrow_date, due_date, status) VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 1); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, bookId); insertPs.setString(2, borrowerName); insertPs.executeUpdate(); conn.commit(); // 提交事务 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }参数说明FOR UPDATE是行级锁防止并发借同一本书时超卖。DATE_ADD(CURDATE(), INTERVAL 30 DAY)算应还日期借期天数可以改成 15 或 60看实际规则。归还时把status改成 0return_date填当前日期同时available_copies加 1。超期查询用SELECT * FROM borrow_records WHERE status 1 AND due_date CURDATE()前端把结果渲染成表格超期天数用DATEDIFF(CURDATE(), due_date)算。4. 避坑排查从 404 到乱码五个高频翻车现场4.1 启动报 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动后访问登录页控制台抛ClassNotFoundException提示找不到 MySQL 驱动。原因MySQL 驱动 jar 包没放进WEB-INF/lib目录或者放进去但 IDE 没把它加到部署路径。Eclipse 里如果 jar 包只在 Build Path 里部署时不会自动复制到 Tomcat 的webapps目录。解决把mysql-connector-java-x.x.x.jar手动复制到WebContent/WEB-INF/lib下然后在 Eclipse 的 Deployment Assembly 里确认这个目录被映射到/WEB-INF/lib。IDEA 用户在 Artifact 配置里检查Available Elements把 lib 目录加进去。4.2 中文乱码图书名称显示问号或方块现象新增图书时输入中文书名保存到数据库变成???或者页面显示乱码。原因三个环节都可能出问题——JSP 页面编码、Servlet 请求编码、数据库连接编码。源码里如果没设request.setCharacterEncoding(UTF-8)POST 请求默认用 ISO-8859-1 解码。解决在AuthFilter或单独的EncodingFilter里统一设request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)。数据库连接 URL 加上useUnicodetruecharacterEncodingutf8mb4。JSP 页面头部加% page contentTypetext/html;charsetUTF-8 languagejava %。三处都对齐乱码就没了。4.3 借书时库存扣成负数现象两个人同时借同一本书库存只剩 1 本结果两个人都借成功了available_copies变成 -1。原因检查库存和扣减库存是两条独立 SQL中间没有锁。并发场景下两个线程都查到库存为 1都执行了扣减。解决用SELECT ... FOR UPDATE在事务里锁行或者直接用UPDATE books SET available_copies available_copies - 1 WHERE id ? AND available_copies 0根据executeUpdate返回的影响行数判断是否扣减成功。后者更简洁不用显式锁。4.4 删除图书报外键约束错误现象在图书列表点删除页面报Cannot delete or update a parent row: a foreign key constraint fails。原因borrow_records表里有这本书的借阅记录外键指向books.id直接删主表记录会被拒绝。解决两种方案。一是软删除books表加is_deleted字段删除时UPDATE books SET is_deleted 1 WHERE id ?查询时加WHERE is_deleted 0。二是先删从表记录再删主表但借阅历史就丢了不推荐。我一般用软删除留个后悔药。4.5 Tomcat 启动慢或端口被占用现象启动 Tomcat 时卡在Starting ProtocolHandler或者报Address already in use: JVM_Bind。原因8080 端口被其他程序占了常见的是之前没关干净的 Tomcat 进程或者某个开发工具内置了服务器。解决Windows 下netstat -ano | findstr 8080找到 PID任务管理器结束进程Linux 下lsof -i:8080然后kill -9 PID。或者改 Tomcat 的conf/server.xml把Connector port8080改成 8081 或 9090。5. 二次开发进阶把借阅提醒做成定时任务顺带聊聊代码结构优化跑通基础功能之后我一般会做两件事一是把超期提醒从手动查询改成定时推送二是把 DAO 层里重复的close逻辑抽出来。先说定时任务源码里没有集成 Quartz 或 Spring Task但加一个简单的ServletContextListener就能实现。// 在 web.xml 里注册监听器应用启动时开启定时线程 public class ReminderListener implements ServletContextListener { private ScheduledExecutorService scheduler; Override public void contextInitialized(ServletContextEvent sce) { scheduler Executors.newSingleThreadScheduledExecutor(); // 每天上午 8 点执行一次超期检查 scheduler.scheduleAtFixedRate(() - { try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT b.title, r.borrower_name, r.due_date FROM borrow_records r JOIN books b ON r.book_id b.id WHERE r.status 1 AND r.due_date CURDATE())) { ResultSet rs ps.executeQuery(); while (rs.next()) { // 这里可以写日志、发邮件或站内信 System.out.println(超期提醒 rs.getString(borrower_name) 借的《 rs.getString(title) 》已超期应还日期 rs.getDate(due_date)); } } catch (SQLException e) { e.printStackTrace(); } }, 0, 24, TimeUnit.HOURS); } Override public void contextDestroyed(ServletContextEvent sce) { if (scheduler ! null) { scheduler.shutdown(); } } }参数说明scheduleAtFixedRate四个参数分别是任务体、初始延迟、间隔时间、时间单位。初始延迟设 0 表示启动就查一次间隔 24 小时。实际部署时可以把结果写进日志文件或者对接邮件服务。注意线程池要在contextDestroyed里关掉否则 Tomcat 停止时线程还在跑会导致内存泄漏。再说代码结构。源码里每个 DAO 方法都手写Connection、PreparedStatement、ResultSet的关闭重复代码多容易漏关导致连接池耗尽。我一般会抽一个DBUtil工具类用 try-with-resources 统一管理。// DBUtil 简化版用 ThreadLocal 或连接池管理连接 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 通用查询方法回调处理 ResultSet public static T T query(String sql, ResultSetHandlerT handler, Object... params) { try (Connection conn 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()) { return handler.handle(rs); } } catch (SQLException e) { e.printStackTrace(); return null; } } }这样 DAO 层只需要关注 SQL 和结果映射连接关闭交给工具类。如果项目要上生产建议换成 Druid 或 HikariCP 连接池DBUtil.getConnection()从池里拿性能会好很多。验证定时任务是否生效可以在ReminderListener里加一行System.out.println(超期检查任务已启动)启动 Tomcat 后看控制台有没有输出。再手动把某条借阅记录的due_date改成昨天等任务触发后看日志有没有打印超期信息。如果没打印检查web.xml里监听器配置的类名是否写全包名有没有漏。从那以后我每次拿到一份 Java Web 源码都强制走一遍「看 web.xml 版本 → 对 JDK 和 Tomcat → 建库改配置 → 跑通登录 → 再动业务代码」的流程顺序乱了就容易在环境问题上耗时间。希望帮到你。本文还有配套的精品资源点击获取