简介:这是一套面向Java初学者与课程设计者的图书管理系统完整项目源码,基于IntelliJ IDEA开发,适合用于毕业设计、课程作业或Java Web入门实战。项目围绕用户、书籍、借阅三大模块展开,涵盖注册登录、权限控制、图书增删改查及借还流程,并采用MVC设计模式与RESTful接口思路组织代码,帮助读者理解从数据库设计到前后端交互的完整链路。压缩包共68个文件,约7.09MB,包含16个java源文件与16个class编译文件、5个html与2个jsp页面、5个xml配置、4个jar依赖,以及png图片、js脚本、css样式和docx设计模式类图等,结构清晰,便于对照学习。目前已有1642人学习下载,读者可借此掌握IDEA环境配置、Servlet与Service分层、数据库操作及项目部署等关键技能,快速积累一个可运行的Java Web实践案例。
1. 从一次课程设计翻车说起:图书管理系统到底该怎么做
很多人第一次接触 Java 课程设计,选的就是图书管理系统。原因很实在:需求看得懂、业务不复杂、能塞进面向对象编程、集合、IO、JDBC 这些知识点。但真正动手才发现,翻车点根本不在业务逻辑,而在环境。IDEA 社区版下载完,新建项目选错 JDK,Maven 依赖拉不下来,数据库驱动版本对不上,控制台报ClassNotFoundException,一上午就没了。这篇笔记就围绕「基于 IDEA 的图书管理系统」这条主线,把环境搭建、分层设计、数据库落地、功能实现和排错串成一条能照着走的路径。适合正在做课程设计的学生、刚转 Java 的初学者,以及想用一个小项目把 Java 基础串起来的开发工程师。读完你至少能拿到一个可运行、可扩展、能写进简历的版本,而不是一个只能跑一次的 demo。
2. 环境与选型:IDEA 里跑通图书管理系统的前置条件
2.1 JDK、IDEA 与构建工具怎么配才不返工
图书管理系统本质是一个 Java SE 或 Java Web 项目。如果只做控制台版,JDK 17 加 IDEA 社区版就够;如果要做带界面的 Web 版,还需要 Tomcat 和 Maven。我一般建议课程设计直接上 JDK 17 + Maven + MySQL 8,原因是这套组合资料最多,遇到问题最容易搜到答案。IDEA 社区版没有 Spring Boot 初始化和数据库工具,但写纯 Java 项目完全够用,不需要去折腾激活码那类东西,社区版下载安装后把 JDK 配好就能开工。
安装完 JDK 后,在命令行验证:
java -version javac -version两条命令输出的版本号必须一致。很多人只配了JAVA_HOME却忘了把%JAVA_HOME%\bin加进Path,结果java能跑、javac找不到,IDEA 里编译直接失败。IDEA 中还要再确认一次:File -> Project Structure -> Project,把 Project SDK 指到你安装的 JDK,Language level 选 17。这一步不做,后面新建类时连record和var都用不了。
Maven 的作用是管依赖。图书管理系统至少需要 MySQL 驱动和连接池。在pom.xml里写清楚:
<dependencies> <!-- MySQL 驱动,版本要和本地 MySQL 大版本匹配 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency> <!-- HikariCP 连接池,比手写 DriverManager 更稳 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency> </dependencies>参数说明:mysql-connector-j是 MySQL 8 之后的官方驱动坐标,老教程里的mysql-connector-java在 8.0.31 之后已经改名,照抄旧坐标会拉不到包。HikariCP 的版本要跟 JDK 匹配,JDK 17 用 5.x,JDK 8 用 4.x。写完pom.xml后点 IDEA 右侧 Maven 面板的刷新按钮,依赖变蓝才算成功。如果一直转圈,检查 Maven 镜像是否配了国内源,settings.xml里加阿里云镜像能省很多时间。
2.2 分层结构:为什么图书管理系统一定要分 dao、service、ui
新手最容易犯的错,是把所有代码写进一个Main类,菜单、输入、SQL 全混在一起。功能少的时候能跑,一旦要加「按作者模糊查询」或者「借书时校验库存」,改一处崩三处。图书管理系统虽然小,但它是练分层最好的载体。常见做法是分四层:实体层entity、数据访问层dao、业务层service、界面层ui。实体层放Book、User、BorrowRecord;dao 层只负责增删改查 SQL;service 层做业务校验,比如借书前判断库存是否大于零;ui 层只处理菜单和输入输出。
这样分的好处是,控制台版和 Web 版可以共用 entity、dao、service,只换 ui 层。后面想把控制台换成 Swing 或者 Servlet,业务代码一行不用动。目录结构建议这样建:
src/main/java/com/example/library ├── entity │ ├── Book.java │ ├── User.java │ └── BorrowRecord.java ├── dao │ ├── BookDao.java │ └── UserDao.java ├── service │ └── BookService.java ├── util │ └── DBUtil.java └── ui └── MainMenu.javautil包里放数据库连接工具类,全局一个连接池,避免每次查询都新建连接。这一步是很多课程设计忽略的地方,但恰恰是「java 怎么保证数据一致性」这类面试题会追问的点:连接池配合事务,才能保证借书时「扣库存」和「写借阅记录」要么都成功,要么都回滚。
3. 数据库与实体映射:图书管理系统落地的核心一步
3.1 建表语句与三个关键字段设计
图书管理系统的数据库不复杂,但字段设计有讲究。至少三张表:book、user、borrow_record。建表时把主键、唯一索引、外键想清楚,后面写 SQL 会顺很多。
CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), stock INT NOT NULL DEFAULT 0, total INT NOT NULL DEFAULT 0 ); CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'reader' ); CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, user_id BIGINT NOT NULL, borrow_date DATE NOT NULL, return_date DATE, status TINYINT NOT NULL DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (user_id) REFERENCES user(id) );参数说明:isbn加唯一索引,防止同一本书重复录入;stock表示可借数量,total表示馆藏总量,还书时 stock 加一但不能超过 total;status用 0 表示未还、1 表示已还,比直接删记录更利于统计。外键约束在课程设计里建议加上,能帮你提前发现脏数据。如果用的是 MySQL 8,注意默认字符集选utf8mb4,否则书名里的特殊字符会乱码。
3.2 用 JDBC 写一个可复用的 DBUtil
连接数据库的代码只写一次,放在DBUtil里。用 HikariCP 管理连接,比DriverManager每次新建连接快得多,也更接近真实项目。
public class DBUtil { private static final HikariDataSource DATA_SOURCE; static { HikariConfig config = new HikariConfig(); // 时区必须写,否则 MySQL 8 会报 serverTimezone 错误 config.setJdbcUrl("jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("your_password"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); DATA_SOURCE = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } }逻辑说明:静态代码块在类加载时执行一次,连接池只初始化一次。maximumPoolSize设 10 对课程设计足够,设太大反而占资源。serverTimezone不写,MySQL 8 驱动会直接抛异常,这是新手最常见的翻车点之一。characterEncoding=utf8保证中文书名不乱码。拿到连接后,所有 dao 方法都用 try-with-resources 关闭PreparedStatement和ResultSet,连接由连接池回收,不要手动 close。
3.3 实体类与 ResultSet 的手动映射
实体类用普通 JavaBean 就行,字段名和表字段对应,但用驼峰命名。比如表里的borrow_date对应 Java 的borrowDate。映射时手动写,别急着上 ORM 框架,课程设计阶段手写一遍才能理解 JDBC 的流程。
public class Book { private Long id; private String isbn; private String title; private String author; private Integer stock; private Integer total; // 省略 getter/setter }dao 层查询示例:
public List<Book> findAll() throws SQLException { String sql = "SELECT id, isbn, title, author, stock, total FROM book"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setId(rs.getLong("id")); b.setIsbn(rs.getString("isbn")); b.setTitle(rs.getString("title")); b.setAuthor(rs.getString("author")); b.setStock(rs.getInt("stock")); b.setTotal(rs.getInt("total")); list.add(b); } } return list; }参数说明:用PreparedStatement而不是Statement,能防 SQL 注入,这是面试常问的点。rs.getLong("id")里的列名要和 SQL 里的别名一致,写错会抛ColumnNotFoundException。如果字段允许为空,用rs.getInt返回 0 而不是 null,需要区分时改用rs.getObject。
4. 功能实现:借书、还书与查询的完整链路
4.1 借书业务为什么要放在 service 层做事务
借书不是一条 SQL 能搞定的。它要同时做三件事:检查库存、扣减库存、插入借阅记录。如果扣完库存插入记录时失败,库存就白白少了。所以必须放在一个事务里。
public boolean borrowBook(Long bookId, Long userId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 扣库存,stock > 0 才更新,避免超借 String updateSql = "UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0"; try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setLong(1, bookId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 库存不足 } } // 2. 插入借阅记录 String insertSql = "INSERT INTO borrow_record(book_id, user_id, borrow_date, status) VALUES(?, ?, CURDATE(), 0)"; try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setLong(1, bookId); ps.setLong(2, userId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明:setAutoCommit(false)开启事务,两步都成功才 commit,任何一步失败就 rollback。UPDATE ... WHERE stock > 0这个条件很关键,它把「检查」和「扣减」合并成一条原子操作,避免并发下超借。参数说明:CURDATE()是 MySQL 函数,取当前日期;status=0表示未还。finally 里恢复自动提交再关闭连接,否则连接回到池里还带着事务状态,下一个使用者会莫名其妙。
4.2 还书与库存回补的边界处理
还书逻辑比借书多一个判断:这条借阅记录是不是已经还过了。如果重复还书,库存会被多加。
public boolean returnBook(Long recordId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 把未还记录改成已还,status=0 才更新 String updateRecord = "UPDATE borrow_record SET status = 1, return_date = CURDATE() WHERE id = ? AND status = 0"; try (PreparedStatement ps = conn.prepareStatement(updateRecord)) { ps.setLong(1, recordId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 记录不存在或已还 } } // 2. 回补库存,但不能超过 total String updateStock = "UPDATE book b JOIN borrow_record r ON b.id = r.book_id " + "SET b.stock = b.stock + 1 WHERE r.id = ? AND b.stock < b.total"; try (PreparedStatement ps = conn.prepareStatement(updateStock)) { ps.setLong(1, recordId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }参数说明:status = 0作为更新条件,保证同一条记录只能还一次。b.stock < b.total防止库存超过馆藏总量。这里用 JOIN 更新,一条 SQL 同时关联两张表,比先查再改更安全。如果业务要求记录还书操作员,可以在 borrow_record 里加operator_id字段。
4.3 模糊查询与分页:别让全表扫描拖垮界面
图书列表一多,一次性SELECT *会卡。加一个按书名模糊查询加分页的接口。
public List<Book> searchByTitle(String keyword, int page, int size) throws SQLException { String sql = "SELECT id, isbn, title, author, stock, total FROM book " + "WHERE title LIKE ? LIMIT ? OFFSET ?"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setInt(2, size); ps.setInt(3, (page - 1) * size); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setId(rs.getLong("id")); b.setTitle(rs.getString("title")); b.setAuthor(rs.getString("author")); b.setStock(rs.getInt("stock")); list.add(b); } } } return list; }参数说明:LIMIT ? OFFSET ?是 MySQL 分页写法,page 从 1 开始,offset 等于(page-1)*size。LIKE的%拼在 Java 里,不要拼进 SQL 字符串,否则又回到 SQL 注入的老路。如果数据量上万,LIKE '%keyword%'用不上索引,可以考虑全文索引,但课程设计阶段没必要。
5. 避坑与排查:IDEA 里图书管理系统最常见的 5 个翻车点
5.1 现象:控制台报 ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:驱动依赖没进 classpath,或者pom.xml里坐标写成了旧的mysql-connector-java,Maven 没拉下来。解决:确认pom.xml用的是com.mysql:mysql-connector-j,然后在 IDEA 右侧 Maven 面板点刷新,等依赖下载完。如果还不行,File -> Project Structure -> Modules -> Dependencies里看有没有这个 jar,没有就手动加。另外,JDBC URL 里驱动类名在 MySQL 8 是com.mysql.cj.jdbc.Driver,老教程写的com.mysql.jdbc.Driver已经废弃。
5.2 现象:中文书名存进数据库变成问号
原因:数据库、表、连接三处字符集不一致。解决:建库时用CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4;JDBC URL 加characterEncoding=utf8;IDEA 的File Encodings里把 Global 和 Project 都设成 UTF-8。三处都对了才不会乱码。如果已经存了乱码数据,改字符集救不回来,只能重新录入。
5.3 现象:IDEA 不显示 target 目录,但磁盘上确实存在
原因:IDEA 把 target 标记为排除目录,或者 Maven 没有执行编译。解决:先执行mvn clean compile,然后在项目树右上角齿轮里勾选Show Excluded Files。如果 target 是空的,说明编译没成功,看 Build 窗口的报错。这个现象本身不影响运行,但会让人误以为代码没编译。
5.4 现象:借书时库存扣成负数
原因:并发下两个线程同时读到 stock=1,都判断通过,各自扣一次。解决:把判断和扣减合并成一条UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0,根据executeUpdate()返回值判断是否成功。这是最省事的乐观锁写法,不需要额外加锁。如果业务更复杂,可以在 book 表加version字段做版本控制。
5.5 现象:IDEA 代码格式化失效,快捷键没反应
原因:快捷键被输入法或其他软件占用,或者格式化配置被改乱。解决:File -> Settings -> Keymap搜Reformat Code,看绑定的快捷键是不是被覆盖,重新绑一个不冲突的。如果是配置文件问题,Settings -> Editor -> Code Style -> Java里恢复默认。这个坑不影响功能,但写代码时很影响心情,早点解决。
6. 进阶技巧:把图书管理系统改造成能写进简历的项目
课程设计做完能跑只是及格线。想让这个项目在面试里拿得出手,得加一点真实项目里才有的东西。我一般会从三个方向改:加登录鉴权、加操作日志、加单元测试。
登录鉴权不用上 Spring Security,自己写一个简单的 Session 或 Token 就够。用户登录后把userId和role存进一个全局的CurrentUser对象,借书时从里面取,而不是让界面层传参。这样 service 层的方法签名更干净,也符合「行级权限」的思路:普通读者只能查自己的借阅记录,管理员能查全部。实现时在 dao 查询里加WHERE user_id = ?,参数从CurrentUser取。
操作日志用 AOP 或者手动在 service 里记都行。建一张operation_log表,字段包括操作人、操作类型、目标 id、时间。每次借书、还书、新增图书后插一条。面试时被问到「怎么排查线上问题」,你可以说通过操作日志定位到具体是谁在什么时候改了数据,这比空谈理论有说服力。
单元测试用 JUnit 5,重点测 service 层的边界:库存为 0 时借书返回 false、重复还书返回 false、还书后库存不超过 total。测试数据库用 H2 内存库,不依赖本地 MySQL,跑起来快。示例:
@Test void borrowBook_shouldFail_whenStockIsZero() throws SQLException { // 准备一本库存为 0 的书 BookService service = new BookService(); boolean result = service.borrowBook(999L, 1L); assertFalse(result); }参数说明:测试方法名用「被测方法_预期结果_条件」的格式,读起来清楚。断言用assertFalse、assertEquals,不要只打印结果。跑测试时如果报连接不上数据库,检查测试用的配置文件是不是指向了 H2 而不是 MySQL。
最后说一个我自己的习惯:每加一个功能,先在纸上画一遍数据流,从界面输入到 SQL 执行再到返回,确认每一步的入参和出参。图书管理系统看着简单,但借书这条链路涉及库存、记录、用户三个实体,画清楚再写,比写完再 debug 省时间。这个项目值不值得做?如果你刚学完 Java 基础,想找一个能把面向对象、集合、JDBC、事务串起来的载体,它非常合适;如果你已经工作两年,可以把它当成练分层和测试的靶子,改造成带鉴权和日志的版本。希望帮到你。
本文还有配套的精品资源,点击获取