Java图书管理系统实战:从数据库设计到JDBC编程完整指南
1. 项目概述与核心价值
最近在整理过去的项目,翻到了这个“图书管理系统”。这几乎是每个Java初学者在掌握了基础语法和数据库操作后,第一个会尝试去做的综合性练手项目。它麻雀虽小,五脏俱全,涵盖了从数据建模、业务逻辑处理到用户交互的完整流程。今天,我就把这个项目的完整实现思路、代码结构以及我踩过的那些“坑”系统地梳理一遍,并附上完整的、可直接运行的代码。无论你是正在寻找课程设计素材的学生,还是想通过一个完整项目巩固Java SE和数据库知识的自学者,这篇文章都能给你提供一个清晰的、可复现的蓝本。
这个系统本质上是一个控制台(Console)应用程序,它模拟了图书馆的核心管理功能:图书的录入、查询、借阅、归还以及读者信息的管理。我们不会使用任何图形界面(如Swing或JavaFX),而是专注于用最纯粹的Java代码和MySQL数据库来构建业务逻辑。这样做的好处是,能让你剥离掉复杂UI的干扰,真正理解数据是如何在程序与数据库之间流动的,业务规则是如何被代码所定义的。这也是为什么它成为经典入门项目的原因——它强迫你去思考如何组织类(Class)、如何设计方法(Method)、如何处理异常(Exception),以及如何编写高效的SQL语句。
2. 系统整体设计与技术选型
2.1 核心需求与功能模块拆解
在动手写第一行代码之前,我们必须明确这个系统要做什么。基于常见的图书馆管理场景,我将其核心功能拆解为以下几个模块:
- 图书信息管理模块:这是系统的基石。需要能对图书进行增(录入新书)、删(下架旧书)、改(更新信息,如价格、状态)、查(按书名、作者、ISBN等条件检索)操作。每本书的关键属性包括:图书ID(主键)、ISBN号、书名、作者、出版社、价格、总数量、在馆数量、入库日期等。
- 读者信息管理模块:管理图书馆的会员或读者。功能同样包括增删改查。读者属性包括:读者ID(主键)、姓名、性别、联系方式、办卡日期、可借阅数量上限、已借数量等。
- 借阅与归还管理模块:这是系统的业务核心。它连接了“图书”和“读者”两个实体。
- 借阅功能:检查读者是否存在、是否超过借阅上限;检查图书是否存在、在馆数量是否大于0。满足条件后,生成一条借阅记录,并更新图书的“在馆数量”和读者的“已借数量”。
- 归还功能:根据借阅记录ID或图书+读者信息,找到对应的借阅记录。检查是否超期(这里我们引入“借阅天数”和“应还日期”的概念),如果超期则计算罚金。然后,更新借阅记录状态为“已归还”,并同步更新图书的“在馆数量”和读者的“已借数量”。
- 借阅记录查询模块:用于查询历史借阅记录,可以按读者、按图书、按时间范围进行筛选,特别是能方便地查看当前未归还的借阅记录。
- 系统管理与统计模块(进阶):例如,简单的用户登录(区分管理员和普通读者)、数据备份/导出、借阅排行榜、图书流通率统计等。作为基础版本,我们可以先实现一个简单的控制台菜单和权限判断。
2.2 技术栈与工具选型解析
为什么选择这些技术?每一个选择背后都有其考量。
- Java SE 8+:这是我们的编程语言和运行环境。选择8或以上版本,是为了能使用
LocalDate等现代日期时间API来处理日期,这比老的Date和Calendar类要直观和稳定得多。 - MySQL 5.7+:关系型数据库的首选。它免费、流行、资料丰富,非常适合学习JDBC。数据库的设计是整个项目的灵魂,表结构设计得好,后续编码会事半功倍。
- JDBC (Java Database Connectivity):Java连接数据库的标准API。我们不会使用任何ORM框架(如MyBatis, Hibernate),因为对于初学者,亲手编写SQL语句、处理
ResultSet是理解数据库操作原理的必经之路。这能让你深刻体会到SQL注入的风险,以及使用PreparedStatement的重要性。 - Maven/Gradle:项目管理工具。我强烈建议使用Maven,哪怕项目很小。它帮你管理依赖(如MySQL驱动包),统一项目结构,一键打包。
pom.xml文件是项目的“配料表”。 - IDE: IntelliJ IDEA:社区版就完全够用。它的智能提示、代码重构、数据库工具插件对开发效率提升巨大。
注意:在正式编码前,请确保你的开发环境已就绪:安装好JDK并配置好
JAVA_HOME环境变量,安装好MySQL并创建好数据库和用户,IDE中配置好Maven。这些是前置步骤,网上教程很多,这里不再赘述。
2.3 数据库表结构设计
数据库设计是重中之重。一个糟糕的设计会让代码变得极其复杂。这里我采用三张核心表的设计:
表1:book(图书表)
CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `isbn` varchar(20) NOT NULL UNIQUE COMMENT 'ISBN号,唯一标识', `name` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) NOT NULL COMMENT '作者', `publisher` varchar(100) DEFAULT NULL COMMENT '出版社', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `total_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '总数量', `available_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '在馆可借数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', PRIMARY KEY (`id`), KEY `idx_isbn` (`isbn`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';设计思路:id是自增主键,用于内部关联。isbn具有业务唯一性,所以设为UNIQUE键并加索引,方便精确查询。total_quantity和available_quantity分开存储是关键,前者是图书馆拥有该书的副本总数,后者是当前在馆可借的数量。每次借阅,available_quantity减1;归还时加1。total_quantity一般只在购入新书或旧书报废时变更。
表2:reader(读者表)
CREATE TABLE `reader` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `card_number` varchar(20) NOT NULL UNIQUE COMMENT '借书卡号', `name` varchar(50) NOT NULL COMMENT '姓名', `gender` char(1) DEFAULT NULL COMMENT '性别(M/F)', `phone` varchar(20) DEFAULT NULL COMMENT '电话', `max_borrow_limit` int(11) NOT NULL DEFAULT '5' COMMENT '最大借阅数量限制', `current_borrowed` int(11) NOT NULL DEFAULT '0' COMMENT '当前已借数量', `register_date` date DEFAULT NULL COMMENT '注册日期', PRIMARY KEY (`id`), KEY `idx_card_number` (`card_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者信息表';设计思路:card_number(借书卡号)是读者的业务标识,唯一。max_borrow_limit和current_borrowed用于控制借阅上限。每次借阅前,需要判断current_borrowed < max_borrow_limit。
表3:borrow_record(借阅记录表)
CREATE TABLE `borrow_record` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `book_id` int(11) NOT NULL COMMENT '图书ID', `reader_id` int(11) NOT NULL COMMENT '读者ID', `borrow_date` date NOT NULL COMMENT '借阅日期', `due_date` date NOT NULL COMMENT '应还日期', `return_date` date DEFAULT NULL COMMENT '实际归还日期', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-借出未还,1-已归还', `fine_amount` decimal(10,2) DEFAULT '0.00' COMMENT '超期罚金', PRIMARY KEY (`id`), KEY `idx_book_id` (`book_id`), KEY `idx_reader_id` (`reader_id`), KEY `idx_status` (`status`), KEY `idx_due_date` (`due_date`), CONSTRAINT `fk_borrow_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_borrow_reader` FOREIGN KEY (`reader_id`) REFERENCES `reader` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';设计思路:这是系统的“流水账”。book_id和reader_id是外键,关联到另外两张表,并设置了ON DELETE CASCADE,意味着当图书或读者被删除时,其关联的借阅记录也会自动删除(根据实际业务,你可能想改为ON DELETE RESTRICT禁止删除,这里仅为示例)。borrow_date是借出当天,due_date是根据规则计算出的应还日期(例如,借阅周期30天)。return_date为空表示未还。status是一个状态标志,方便查询。fine_amount记录罚金。为reader_id,book_id,status,due_date都建立了索引,因为这些都是高频查询条件。
3. 项目架构与核心代码实现
3.1 项目工程结构(Maven)
一个清晰的项目结构能让代码维护性大增。我们的项目结构如下:
book-management-system/ ├── pom.xml # Maven配置文件 ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── library/ │ │ │ ├── MainApp.java # 程序主入口,控制台菜单 │ │ │ ├── dao/ # 数据访问层 │ │ │ │ ├── BaseDao.java # 数据库连接和通用关闭方法 │ │ │ │ ├── BookDao.java │ │ │ │ ├── ReaderDao.java │ │ │ │ └── BorrowRecordDao.java │ │ │ ├── entity/ # 实体类,对应数据库表 │ │ │ │ ├── Book.java │ │ │ │ ├── Reader.java │ │ │ │ └── BorrowRecord.java │ │ │ ├── service/ # 业务逻辑层 │ │ │ │ ├── BookService.java │ │ │ │ ├── ReaderService.java │ │ │ │ └── BorrowService.java │ │ │ └── util/ # 工具类 │ │ │ ├── DBUtil.java # 数据库连接池(简化版) │ │ │ ├── DateUtil.java # 日期处理工具 │ │ │ └── InputUtil.java # 控制台输入工具 │ │ └── resources/ │ │ └── db.properties # 数据库配置文件 │ └── test/ # 测试目录(可选) └── target/ # 编译输出目录分层架构解析:
entity:纯数据类,只有属性和getter/setter方法,与数据库表字段一一对应。这是ORM思想的雏形。dao(Data Access Object):数据访问对象。这一层只负责和数据库打交道,执行最原始的CRUD SQL操作。它接收和返回的都是entity对象。这里隐藏了所有JDBC的细节。service:业务逻辑层。这是核心,它调用一个或多个dao的方法,组合成完整的业务操作。例如,“借书”这个业务,需要在BorrowService中先后调用ReaderDao查询读者、BookDao查询图书、检查条件、调用BorrowRecordDao插入记录,并更新读者和图书的数量。事务控制也应该在这一层考虑。MainApp:表示层(虽然很简陋)。它展示菜单,接收用户输入,调用对应的service完成功能,并输出结果。
3.2 核心实体类(Entity)定义
以Book类为例,展示如何用Java类映射数据库表:
package com.library.entity; import java.math.BigDecimal; import java.time.LocalDateTime; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private BigDecimal price; // 使用BigDecimal处理金额,避免精度丢失 private Integer totalQuantity; private Integer availableQuantity; private LocalDateTime createTime; // 无参构造、全参构造、getter和setter方法省略... // 务必使用IDE生成,保证准确性。 }关键点:
- 数据类型匹配:
price使用BigDecimal,createTime使用LocalDateTime,这与数据库的decimal和datetime类型对应。 - 封装性:所有属性私有,通过公共的getter和setter方法访问。这是JavaBean的规范。
Reader和BorrowRecord实体类依此类推。BorrowRecord中的日期字段建议使用LocalDate。
3.3 数据库连接与工具类
直接在每个DAO里写连接代码是灾难。我们需要一个集中管理数据库连接的地方。这里实现一个简易的DBUtil,使用DBCP或HikariCP等连接池是生产级做法,但为了简化,我们先使用传统的DriverManager,但将其单例化。
package com.library.util; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; // 静态代码块,在类加载时读取配置文件 static { try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(is); url = props.getProperty("db.url"); user = props.getProperty("db.user"); password = props.getProperty("db.password"); // 加载驱动(MySQL 6.0+ 可以省略,但写上更稳妥) Class.forName(props.getProperty("db.driver")); } catch (IOException | ClassNotFoundException e) { throw new ExceptionInInitializerError("Failed to load database configuration", e); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 关闭连接的方法(重载,用于关闭Connection, Statement, ResultSet) public static void close(AutoCloseable... closeables) { for (AutoCloseable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { // 日志记录,这里简单打印 System.err.println("Error closing resource: " + e.getMessage()); } } } } }对应的src/main/resources/db.properties文件:
db.driver=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false db.user=your_username db.password=your_password实操心得:
serverTimezone=Asia/Shanghai这个参数至关重要,尤其是在使用高版本MySQL驱动和Java 8+的日期API时,不设置可能导致时间错误。useSSL=false在本地开发环境通常需要,否则可能连接失败。
3.4 数据访问层(DAO)实现详解
DAO层是JDBC操作的核心。我们以BookDao为例,展示如何实现增删改查。最重要的一点:使用PreparedStatement防止SQL注入,并确保资源被正确关闭。
package com.library.dao; import com.library.entity.Book; import com.library.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { // 新增图书 public boolean addBook(Book book) { String sql = "INSERT INTO book (isbn, name, author, publisher, price, total_quantity, available_quantity) VALUES (?, ?, ?, ?, ?, ?, ?)"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, book.getIsbn()); pstmt.setString(2, book.getName()); pstmt.setString(3, book.getAuthor()); pstmt.setString(4, book.getPublisher()); pstmt.setBigDecimal(5, book.getPrice()); pstmt.setInt(6, book.getTotalQuantity()); pstmt.setInt(7, book.getAvailableQuantity()); // 新书在馆数量等于总数量 int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DBUtil.close(pstmt, conn); // 使用工具类关闭资源 } } // 根据ID查询图书 public Book getBookById(int id) { String sql = "SELECT * FROM book WHERE id = ?"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; Book book = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); rs = pstmt.executeQuery(); if (rs.next()) { book = extractBookFromResultSet(rs); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return book; } // 根据书名模糊查询 public List<Book> getBooksByName(String nameKeyword) { List<Book> bookList = new ArrayList<>(); String sql = "SELECT * FROM book WHERE name LIKE ?"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, "%" + nameKeyword + "%"); // 模糊查询 rs = pstmt.executeQuery(); while (rs.next()) { bookList.add(extractBookFromResultSet(rs)); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return bookList; } // 更新图书信息(例如,修改价格或数量) public boolean updateBook(Book book) { // 注意:这里通常不会更新isbn,因为它是业务唯一标识。 String sql = "UPDATE book SET name=?, author=?, publisher=?, price=?, total_quantity=?, available_quantity=? WHERE id=?"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, book.getName()); pstmt.setString(2, book.getAuthor()); pstmt.setString(3, book.getPublisher()); pstmt.setBigDecimal(4, book.getPrice()); pstmt.setInt(5, book.getTotalQuantity()); pstmt.setInt(6, book.getAvailableQuantity()); pstmt.setInt(7, book.getId()); int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DBUtil.close(pstmt, conn); } } // 删除图书(根据ID) public boolean deleteBookById(int id) { String sql = "DELETE FROM book WHERE id = ?"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DBUtil.close(pstmt, conn); } } // 一个内部方法,用于从ResultSet中提取Book对象,避免代码重复 private Book extractBookFromResultSet(ResultSet rs) throws SQLException { Book book = new Book(); book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); book.setName(rs.getString("name")); book.setAuthor(rs.getString("author")); book.setPublisher(rs.getString("publisher")); book.setPrice(rs.getBigDecimal("price")); book.setTotalQuantity(rs.getInt("total_quantity")); book.setAvailableQuantity(rs.getInt("available_quantity")); // 处理时间戳转换 Timestamp timestamp = rs.getTimestamp("create_time"); if (timestamp != null) { book.setCreateTime(timestamp.toLocalDateTime()); } return book; } }代码解析与避坑指南:
PreparedStatement的使用:所有SQL语句中的变量都用?占位,然后通过setXxx方法赋值。这能有效防止SQL注入攻击,也是JDBC编程的铁律。- 资源关闭:
Connection,Statement(包括PreparedStatement),ResultSet都是需要关闭的资源。必须在finally块中关闭,确保即使发生异常,资源也能被释放。我们将其封装在DBUtil.close()中。 - 结果集处理:
ResultSet使用前必须调用next()方法移动光标。extractBookFromResultSet方法是一个好实践,它把从ResultSet到Entity对象的映射逻辑集中在一处,便于维护。 - 异常处理:这里只是简单打印了堆栈信息。在实际项目中,应该记录日志,并根据业务需要向上层抛出更明确的业务异常。
ReaderDao和BorrowRecordDao的实现逻辑类似,主要区别在于SQL语句和实体映射。
3.5 业务逻辑层(Service)实现与事务控制
Service层是业务规则的落脚点。我们以最复杂的BorrowService.borrowBook(借书)方法为例。
package com.library.service; import com.library.dao.*; import com.library.entity.Book; import com.library.entity.BorrowRecord; import com.library.entity.Reader; import com.library.util.DateUtil; import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDate; public class BorrowService { private BookDao bookDao = new BookDao(); private ReaderDao readerDao = new ReaderDao(); private BorrowRecordDao borrowRecordDao = new BorrowRecordDao(); /** * 借阅图书 * @param bookId 图书ID * @param readerId 读者ID * @param borrowDays 借阅天数 * @return 成功返回true,失败返回false */ public boolean borrowBook(int bookId, int readerId, int borrowDays) { // 1. 验证图书是否存在且可借 Book book = bookDao.getBookById(bookId); if (book == null) { System.out.println("错误:图书不存在。"); return false; } if (book.getAvailableQuantity() <= 0) { System.out.println("错误:该图书已全部借出。"); return false; } // 2. 验证读者是否存在且未超借阅上限 Reader reader = readerDao.getReaderById(readerId); if (reader == null) { System.out.println("错误:读者不存在。"); return false; } if (reader.getCurrentBorrowed() >= reader.getMaxBorrowLimit()) { System.out.println("错误:该读者借阅数量已达上限。"); return false; } // 3. 核心业务逻辑:需要在一个数据库事务中完成 // 模拟事务管理(简化版,实际应用应使用Spring等框架管理事务) Connection conn = null; try { // 假设我们有一个能获取同一连接的方法(需改造DBUtil和Dao层支持事务) // conn = TransactionManager.getConnection(); // conn.setAutoCommit(false); // 开启事务 // 3.1 插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(LocalDate.now()); record.setDueDate(LocalDate.now().plusDays(borrowDays)); record.setStatus(0); // 0-未还 boolean recordSaved = borrowRecordDao.save(record); // save方法需要接收Connection参数 // 3.2 更新图书在馆数量 (-1) book.setAvailableQuantity(book.getAvailableQuantity() - 1); boolean bookUpdated = bookDao.updateBook(book); // update方法需要接收Connection参数 // 3.3 更新读者已借数量 (+1) reader.setCurrentBorrowed(reader.getCurrentBorrowed() + 1); boolean readerUpdated = readerDao.updateReader(reader); // update方法需要接收Connection参数 if (recordSaved && bookUpdated && readerUpdated) { // conn.commit(); // 提交事务 System.out.println("借阅成功!借阅记录ID: " + record.getId()); return true; } else { // conn.rollback(); // 回滚事务 System.out.println("借阅失败,数据操作异常。"); return false; } } catch (Exception e) { // if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); System.out.println("借阅过程发生异常,操作已回滚。"); return false; } finally { // DBUtil.close(conn); } } }业务逻辑与事务要点:
- 原子性操作:借书不是一个简单的插入操作,它涉及三个表的更新(
borrow_record插入,book和reader更新)。这三个操作必须全部成功或全部失败,这就是事务(Transaction)。上面的代码注释展示了事务的雏形。在真正的项目中,你会使用像Spring的@Transactional注解来优雅地管理事务。 - 业务规则校验:在操作数据库前,先进行业务规则校验(图书可借、读者未超限),这能提前避免无效的数据库操作。
- 状态与数量同步:
book.available_quantity和reader.current_borrowed必须与borrow_record的状态严格同步。这是业务一致性的关键。
returnBook(还书)方法的逻辑类似,但需要检查是否超期并计算罚金,然后更新borrow_record的return_date、status和fine_amount,并反向更新book和reader的数量。
3.6 主程序与控制台交互
MainApp类负责提供用户界面。这里展示一个简单的控制台菜单驱动程序。
package com.library; import com.library.service.BookService; import com.library.service.BorrowService; import com.library.service.ReaderService; import com.library.util.InputUtil; import java.util.Scanner; public class MainApp { private static Scanner scanner = new Scanner(System.in); private static BookService bookService = new BookService(); private static ReaderService readerService = new ReaderService(); private static BorrowService borrowService = new BorrowService(); public static void main(String[] args) { while (true) { showMainMenu(); int choice = InputUtil.readInt("请选择操作:"); switch (choice) { case 1: manageBooks(); break; case 2: manageReaders(); break; case 3: borrowBook(); break; case 4: returnBook(); break; case 5: queryRecords(); break; case 0: System.out.println("感谢使用图书管理系统,再见!"); scanner.close(); System.exit(0); default: System.out.println("输入无效,请重新选择。"); } } } private static void showMainMenu() { System.out.println("\n========== 图书管理系统 =========="); System.out.println("1. 图书管理"); System.out.println("2. 读者管理"); System.out.println("3. 借阅图书"); System.out.println("4. 归还图书"); System.out.println("5. 借阅记录查询"); System.out.println("0. 退出系统"); System.out.println("================================="); } private static void manageBooks() { // 子菜单:新增、查询、修改、删除图书 // 调用对应的 BookService 方法 System.out.println("\n--- 图书管理 ---"); System.out.println("1. 新增图书"); System.out.println("2. 查询图书"); System.out.println("3. 修改图书信息"); System.out.println("4. 删除图书"); System.out.println("0. 返回上级"); int choice = InputUtil.readInt("请选择:"); switch (choice) { case 1: // 收集图书信息,调用bookService.addBook(...) break; case 2: // 输入书名关键字,调用bookService.getBooksByName(...)并打印结果 break; // ... 其他case } } // manageReaders, borrowBook, returnBook, queryRecords 方法类似,需要调用相应的Service }InputUtil是一个工具类,用于安全地读取控制台输入,处理nextInt()后残留的换行符等问题,提升用户体验。
4. 项目运行、测试与常见问题
4.1 如何运行本项目
- 环境准备:确保安装JDK 8+,MySQL 5.7+,Maven 3.6+,以及IntelliJ IDEA。
- 创建数据库:在MySQL中执行前面提供的SQL语句,创建
library_db数据库及三张表。 - 修改配置:将
db.properties文件中的your_username和your_password改为你自己的MySQL凭据。 - 导入项目:在IDEA中直接打开
pom.xml文件,将其作为Maven项目导入。 - 下载依赖:Maven会自动下载
mysql-connector-java驱动包。 - 运行:找到
MainApp.java,右键运行main方法。
4.2 核心功能测试用例
在编写代码时或完成后,应对核心流程进行测试。你可以编写简单的JUnit测试,或者在main方法中写一段测试代码。
测试借书流程:
// 这是一个在MainApp中临时测试的方法 private static void testBorrowFlow() { // 1. 先确保有数据和读者 Book book = new Book(); book.setIsbn("978-7-121-12345-6"); book.setName("Java编程思想"); book.setAuthor("Bruce Eckel"); book.setTotalQuantity(5); book.setAvailableQuantity(5); bookService.addBook(book); // 假设已实现 Reader reader = new Reader(); reader.setCardNumber("R20240001"); reader.setName("张三"); reader.setMaxBorrowLimit(5); readerService.addReader(reader); // 假设已实现 // 2. 执行借阅 boolean success = borrowService.borrowBook(book.getId(), reader.getId(), 30); System.out.println("借阅测试结果:" + (success ? "成功" : "失败")); // 3. 验证数据 Book updatedBook = bookService.getBookById(book.getId()); Reader updatedReader = readerService.getReaderById(reader.getId()); System.out.println("图书在馆数量:" + updatedBook.getAvailableQuantity()); // 应为4 System.out.println("读者已借数量:" + updatedReader.getCurrentBorrowed()); // 应为1 }4.3 常见问题与排查技巧实录
在开发过程中,你几乎一定会遇到以下问题:
ClassNotFoundException: com.mysql.cj.jdbc.Driver- 原因:MySQL驱动包未正确引入。
- 解决:检查
pom.xml中是否有mysql-connector-java依赖,版本是否合适(如8.0.x)。然后检查IDEA的Maven面板,点击Reimport。确保依赖包已下载到本地仓库。
Communications link failure或Access denied for user- 原因:数据库连接失败。可能是URL、用户名、密码错误,或者MySQL服务未启动,或者网络问题。
- 解决:
- 检查
db.properties配置文件。 - 在命令行或MySQL客户端用相同信息尝试连接。
- 确认MySQL服务已运行(
sudo systemctl status mysql或netstat -an | grep 3306)。 - 检查防火墙是否屏蔽了3306端口。
- 检查
时间错误,插入的日期比实际少8小时
- 原因:经典的时区问题。数据库服务器时区、连接字符串时区、Java应用时区不一致。
- 解决:在JDBC连接URL中强制指定时区,如
serverTimezone=Asia/Shanghai。同时,确保数据库和操作系统的时区设置正确。
PreparedStatement设置参数时索引错误- 原因:
pstmt.setString(1, ...)中的索引1对应SQL中第一个?,如果顺序弄错,会导致数据类型不匹配或值错位。 - 解决:仔细核对SQL语句中
?的顺序与setXxx方法的索引。建议将SQL字符串和设置参数的代码对齐书写。
- 原因:
事务问题:部分操作成功,部分失败,导致数据不一致
- 原因:如借书时,借阅记录插入成功,但更新图书数量失败。
- 解决:这就是为什么需要事务。在当前的简化DAO中,每个方法自己获取和关闭连接,无法实现跨方法的事务。改进方案:可以引入一个
TransactionManager类,在Service层开始时获取一个连接,并将其通过参数传递给所有需要在这个事务中执行的DAO方法,最后在Service层统一提交或回滚。这是向Spring等框架的事务管理迈进的第一步。
内存泄漏:数据库连接未关闭
- 原因:在复杂的异常处理路径中,
Connection,Statement,ResultSet可能没有被正确关闭。 - 解决:严格遵守“在finally块中关闭资源”的原则。使用
DBUtil.close()这样的工具方法可以简化代码并减少遗漏。Java 7的try-with-resources语法是更优雅的解决方案。
- 原因:在复杂的异常处理路径中,
这个“图书管理系统”项目虽然基础,但它像一块敲门砖,敲开了Java企业级应用开发的大门。通过它,你实践了MVC(或分层)架构的思想,理解了数据库连接、SQL操作、事务控制等核心概念。你可以在此基础上无限扩展:增加Web界面(使用Spring Boot)、添加缓存(Redis)、引入消息队列处理借阅超时提醒、实现更复杂的权限管理等等。希望这份详细的拆解和完整的代码,能成为你Java学习路上一个扎实的起点。代码仓库的链接我会放在文末,你可以克隆下来,对照着文章一步步理解和修改。编程最好的学习方式就是动手,遇到问题就解决它,每一个踩过的坑都会让你变得更强大。
