简介:这是一套面向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 或 7,Tomcat 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 ) ENGINE=InnoDB; -- 借阅记录表: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) ) ENGINE=InnoDB;建完表,把源码里sql文件夹下的初始数据脚本跑一遍,通常会有几十条测试图书数据。注意看脚本里有没有DROP TABLE语句,有的话先注释掉,别把刚建好的表删了。
2.3 改配置文件:路径、账号、端口三处必查
数据库建好之后,回到源码,找到数据库连接配置文件。如果是db.properties,改这几项:
# 数据库驱动,MySQL 5.x 用 com.mysql.jdbc.Driver # MySQL 8.x 用 com.mysql.cj.jdbc.Driver,别搞混 jdbc.driver=com.mysql.jdbc.Driver # 连接地址,库名要和刚才建的一致 jdbc.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8mb4 # 账号密码换成你自己的 jdbc.username=root jdbc.password=your_password改完配置,别急着启动 Tomcat。先检查WEB-INF/classes目录下有没有编译好的.class文件。如果没有,说明源码还没编译,需要在 IDE 里 Build 一下。Eclipse 用户右键项目 → Build Path → Configure Build Path,确认 JRE 是 1.8;IDEA 用户在 Project Structure 里把 SDK 设成 1.8,Language Level 也设成 8。
最后一步,把项目部署到 Tomcat。Eclipse 里右键项目 → Run As → Run on Server;IDEA 里配置 Tomcat Server,Deployment 选项卡添加 Artifact。启动后浏览器访问http://localhost:8080/项目名/login.jsp,能看到登录页就算环境通了。如果报 404,检查项目名大小写;如果报 500,看 Tomcat 控制台日志,十有八九是数据库连接失败。
3. 核心功能模块拆解:登录鉴权、图书 CRUD 与借阅逻辑
3.1 登录鉴权:Session 存什么、过滤器怎么配
登录模块的入口是LoginServlet,接收前端表单提交的username和password,查users表比对。源码里密码是明文存储的,这在课程设计里常见,但如果你要放到实际环境用,建议改成 MD5 或 BCrypt 加盐哈希。登录成功后,把用户对象塞进HttpSession,key 通常是"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?action=list拿数据。源码里分页用的是LIMIT子句,这是 MySQL 的方言,换成 Oracle 或 SQL Server 得改。分页参数一般有两个:pageNo(当前页码)和pageSize(每页条数),默认pageSize设 10 或 15。
// BookDao 分页查询 public List<Book> findByPage(int pageNo, int pageSize) { List<Book> 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?action=add,表单字段包括isbn、title、author、publisher、totalCopies。插入前先查isbn是否已存在,存在就提示重复。删除图书用action=delete&id=xx,注意外键约束——如果这本书有未归还的借阅记录,直接删会报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改成 0,return_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;charset=UTF-8")。数据库连接 URL 加上useUnicode=true&characterEncoding=utf8mb4。JSP 页面头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>。三处都对齐,乱码就没了。
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 port="8080"改成 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?useUnicode=true&characterEncoding=utf8mb4"; 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, ResultSetHandler<T> 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 → 建库改配置 → 跑通登录 → 再动业务代码」的流程,顺序乱了就容易在环境问题上耗时间。希望帮到你。
本文还有配套的精品资源,点击获取