在实际 Java 开发学习中,构建一个完整的项目是巩固基础、理解面向对象设计和数据库操作的最佳途径。图书管理系统作为经典的教学案例,涵盖了用户管理、图书信息维护、借阅归还流程、数据持久化等核心业务,非常适合初学者将零散的 Java 语法、集合框架、JDBC 等知识串联起来。很多新手在尝试时,往往卡在项目结构设计、数据库连接、业务逻辑分层和异常处理这些环节,导致代码混乱、难以调试。
本文将带你从零开始,构建一个控制台版本的图书管理系统。我们将采用分层架构思想,使用 MySQL 作为数据库,JDBC 进行数据操作,并融入面向对象的设计原则。整个过程不仅会展示代码怎么写,更会解释为什么这样设计,以及每一步可能遇到的坑和排查方法。完成这个项目后,你将能掌握一个典型 Java 应用从需求分析、数据库设计、编码实现到测试运行的全流程,为后续学习 Spring Boot 等框架打下坚实基础。
1. 系统需求分析与核心概念理解
在动手编码之前,明确系统要做什么、有哪些角色、涉及哪些核心概念至关重要。这能避免后续开发过程中频繁返工和逻辑混乱。
1.1 业务需求与角色定义
一个最简单的图书管理系统通常涉及两类用户:管理员和普通读者。他们的核心操作权限不同。
- 管理员:拥有系统的最高权限,负责基础数据的维护。
- 图书管理:新增图书、删除图书、修改图书信息、查询所有图书。
- 用户管理:新增读者、删除读者、修改读者信息、查询所有读者。
- 普通读者:只能进行与自身相关的借阅操作。
- 借阅图书:查询可借图书并进行借阅。
- 归还图书:归还自己已借阅的图书。
- 查询借阅记录:查看自己的借阅历史。
基于以上需求,我们可以抽象出几个核心的实体(Entity)或模型(Model),它们是系统数据的载体。
1.2 核心实体类设计
实体类对应数据库中的表,每个属性对应表中的字段。良好的实体设计是项目成功的一半。
图书(Book)
id: 图书唯一标识(主键,通常自增)。isbn: 国际标准书号,具有唯一性,可用于精确检索。name: 图书名称。author: 作者。publisher: 出版社。price: 价格(使用BigDecimal类型,避免浮点数精度问题)。stock: 库存数量。借阅时减少,归还时增加。
用户(User)
id: 用户唯一标识(主键)。username: 用户名(用于登录,应唯一)。password: 密码(存储时应加密,此处为简化先使用明文)。name: 真实姓名。role: 角色(如admin或reader),用于权限控制。
借阅记录(BorrowRecord)
id: 记录唯一标识。userId: 借阅用户ID(外键,关联User.id)。bookId: 借阅图书ID(外键,关联Book.id)。borrowTime: 借阅时间。returnTime: 归还时间(为空表示未归还)。status: 借阅状态(如BORROWED,RETURNED),方便查询。
注意:在实际项目中,密码必须加密存储(如使用 BCrypt),角色和权限可能更复杂(如使用 RBAC 模型)。本文为简化教学,先实现核心流程。
2. 开发环境与项目结构准备
一个清晰的项目结构能让代码管理事半功倍。我们使用 Maven 来管理依赖和构建项目。
2.1 环境与工具清单
在开始前,请确保你的开发环境已就绪。
| 工具/环境 | 推荐版本 | 作用说明 | 验证命令 |
|---|---|---|---|
| JDK | 8, 11 或 17 | Java 运行和编译环境 | java -version |
| IDE | IntelliJ IDEA | 集成开发环境,提高效率 | - |
| 构建工具 | Maven 3.6+ | 依赖管理和项目构建 | mvn -v |
| 数据库 | MySQL 5.7+ | 数据持久化存储 | mysql --version |
| 数据库客户端 | Navicat, DBeaver 或命令行 | 执行 SQL,管理数据库 | - |
2.2 创建 Maven 项目与目录结构
在 IntelliJ IDEA 中,选择New Project->Maven,使用合适的 JDK 版本,创建项目。项目创建完成后,手动补充以下目录结构。这不是 Maven 的标准结构,但更符合业务逻辑分层,便于理解。
book-management-system ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── example │ │ │ └── bookms │ │ │ ├── Main.java # 程序入口 │ │ │ ├── model # 实体类包 │ │ │ │ ├── Book.java │ │ │ │ ├── User.java │ │ │ │ └── BorrowRecord.java │ │ │ ├── dao # 数据访问层包 │ │ │ │ ├── BaseDao.java # 通用数据库操作 │ │ │ │ ├── BookDao.java │ │ │ │ ├── UserDao.java │ │ │ │ └── BorrowRecordDao.java │ │ │ ├── service # 业务逻辑层包 │ │ │ │ ├── BookService.java │ │ │ │ ├── UserService.java │ │ │ │ └── BorrowRecordService.java │ │ │ └── util # 工具类包 │ │ │ ├── DBUtil.java # 数据库连接工具 │ │ │ └── ConsoleUtil.java # 控制台输入输出工具 │ │ └── resources │ │ └── db.properties # 数据库配置文件 │ └── test # 测试代码(可选) └── pom.xml # Maven 配置文件2.3 配置 Maven 依赖与数据库
在pom.xml中添加 MySQL 驱动和单元测试依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>book-management-system</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> </project>在src/main/resources目录下创建db.properties文件,用于存放数据库连接信息,实现配置与代码分离。
# db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/book_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_password注意:请将
your_password替换为你本地 MySQL 的 root 密码。serverTimezone参数非常重要,不设置可能导致时区错误。
3. 数据库设计与数据访问层实现
数据层是应用的基石,设计不当会导致后续业务逻辑复杂且性能低下。
3.1 创建数据库与表
使用 MySQL 客户端连接数据库,执行以下 SQL 语句创建数据库和表。
-- 创建数据库,如果已存在则忽略 CREATE DATABASE IF NOT EXISTS `book_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `book_db`; -- 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '密码', `name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `role` VARCHAR(20) NOT NULL DEFAULT 'reader' COMMENT '角色:admin/reader' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 图书表 CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `isbn` VARCHAR(20) NOT NULL UNIQUE COMMENT 'ISBN号', `name` VARCHAR(200) NOT NULL COMMENT '图书名称', `author` VARCHAR(100) NOT NULL COMMENT '作者', `publisher` VARCHAR(100) COMMENT '出版社', `price` DECIMAL(10, 2) NOT NULL COMMENT '价格', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存数量' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 借阅记录表 CREATE TABLE `borrow_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '借阅用户ID', `book_id` INT NOT NULL COMMENT '借阅图书ID', `borrow_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '借阅时间', `return_time` DATETIME NULL COMMENT '归还时间', `status` VARCHAR(20) NOT NULL DEFAULT 'BORROWED' COMMENT '状态:BORROWED/RETURNED', FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表'; -- 插入初始管理员用户 (密码为明文 'admin123',仅用于演示) INSERT INTO `user` (`username`, `password`, `name`, `role`) VALUES ('admin', 'admin123', '系统管理员', 'admin');3.2 实现数据库连接工具类
为了避免在每个数据库操作中重复编写加载驱动、获取连接、关闭资源的代码,我们封装一个DBUtil工具类。
// src/main/java/com/example/bookms/util/DBUtil.java package com.example.bookms.util; import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块,在类加载时读取配置文件 static { try { InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("db.properties"); Properties props = new Properties(); props.load(is); driver = props.getProperty("jdbc.driver"); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(driver); // 加载驱动 } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException("数据库配置加载失败", e); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } // 关闭资源 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }3.3 实现实体类与数据访问层
以Book实体和BookDao为例,展示如何将数据库表映射为 Java 对象,并实现增删改查。
首先创建Book实体类,属性与数据库表字段一一对应。
// src/main/java/com/example/bookms/model/Book.java package com.example.bookms.model; import java.math.BigDecimal; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private BigDecimal price; private Integer stock; // 构造方法、Getter和Setter省略,实际开发中必须生成 // 可以使用IDE的Generate功能或Lombok注解简化 }然后创建BookDao类,负责所有与book表交互的数据库操作。这里采用经典的 JDBC 模板代码。
// src/main/java/com/example/bookms/dao/BookDao.java package com.example.bookms.dao; import com.example.bookms.model.Book; import com.example.bookms.util.DBUtil; import java.math.BigDecimal; 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, stock) VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement 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.getStock()); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 根据ID删除图书 public boolean deleteBookById(Integer id) { String sql = "DELETE FROM book WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 更新图书信息 public boolean updateBook(Book book) { String sql = "UPDATE book SET isbn=?, name=?, author=?, publisher=?, price=?, stock=? WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement 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.getStock()); pstmt.setInt(7, book.getId()); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 查询所有图书 public List<Book> getAllBooks() { List<Book> bookList = new ArrayList<>(); String sql = "SELECT * FROM book"; try (Connection conn = DBUtil.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { 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.setStock(rs.getInt("stock")); bookList.add(book); } } catch (SQLException e) { e.printStackTrace(); } return bookList; } // 根据ID查询图书 public Book getBookById(Integer id) { String sql = "SELECT * FROM book WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); try (ResultSet rs = pstmt.executeQuery()) { if (rs.next()) { 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.setStock(rs.getInt("stock")); return book; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }按照同样的模式,完成UserDao和BorrowRecordDao的实现。UserDao需要实现用户登录验证(根据用户名和密码查询),BorrowRecordDao需要实现借阅、归还和查询记录等功能。
4. 业务逻辑层与控制台交互实现
数据访问层(DAO)只负责数据的存取,业务规则(如借书前检查库存、还书时更新状态)应在业务逻辑层(Service)处理。
4.1 实现业务逻辑层
以BookService为例,它调用BookDao,并可以加入业务逻辑,比如新增图书前检查 ISBN 是否重复。
// src/main/java/com/example/bookms/service/BookService.java package com.example.bookms.service; import com.example.bookms.dao.BookDao; import com.example.bookms.model.Book; import java.util.List; public class BookService { private BookDao bookDao = new BookDao(); public boolean addBook(Book book) { // 这里可以加入业务逻辑,例如检查ISBN是否已存在 // 为了简化,我们假设直接插入 return bookDao.addBook(book); } public boolean deleteBook(Integer id) { // 删除前可以检查是否有未归还的借阅记录 return bookDao.deleteBookById(id); } public boolean updateBook(Book book) { return bookDao.updateBook(book); } public List<Book> getAllBooks() { return bookDao.getAllBooks(); } public Book getBookById(Integer id) { return bookDao.getBookById(id); } }BorrowRecordService是业务核心,它需要协调BookDao和BorrowRecordDao,在一个事务内完成借阅和归还操作。
// src/main/java/com/example/bookms/service/BorrowRecordService.java package com.example.bookms.service; import com.example.bookms.dao.BookDao; import com.example.bookms.dao.BorrowRecordDao; import com.example.bookms.model.Book; import com.example.bookms.model.BorrowRecord; import java.sql.Connection; import java.sql.SQLException; import java.util.Date; import java.util.List; public class BorrowRecordService { private BorrowRecordDao borrowRecordDao = new BorrowRecordDao(); private BookDao bookDao = new BookDao(); public boolean borrowBook(Integer userId, Integer bookId) { // 1. 检查图书库存 Book book = bookDao.getBookById(bookId); if (book == null || book.getStock() <= 0) { System.out.println("图书不存在或库存不足!"); return false; } // 2. 减少库存 book.setStock(book.getStock() - 1); boolean updateSuccess = bookDao.updateBook(book); if (!updateSuccess) { return false; } // 3. 创建借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setStatus("BORROWED"); return borrowRecordDao.addRecord(record); } public boolean returnBook(Integer recordId) { // 1. 查询借阅记录 BorrowRecord record = borrowRecordDao.getRecordById(recordId); if (record == null || "RETURNED".equals(record.getStatus())) { System.out.println("借阅记录无效或已归还!"); return false; } // 2. 更新记录状态和归还时间 record.setReturnTime(new Date()); record.setStatus("RETURNED"); boolean updateRecordSuccess = borrowRecordDao.updateRecord(record); if (!updateRecordSuccess) { return false; } // 3. 增加图书库存 Book book = bookDao.getBookById(record.getBookId()); book.setStock(book.getStock() + 1); return bookDao.updateBook(book); } public List<BorrowRecord> getRecordsByUser(Integer userId) { return borrowRecordDao.getRecordsByUserId(userId); } }注意:上面的
borrowBook和returnBook方法存在一个严重问题:库存更新和记录更新不是原子操作。如果更新库存成功但创建记录失败,会导致数据不一致。生产环境必须使用数据库事务(Connection.setAutoCommit(false))来保证。此处为简化演示,先忽略事务。
4.2 实现控制台菜单与用户交互
最后,在Main类中实现一个简单的控制台菜单,将各个模块串联起来。
// src/main/java/com/example/bookms/Main.java package com.example.bookms; import com.example.bookms.model.User; import com.example.bookms.service.UserService; import com.example.bookms.util.ConsoleUtil; import java.util.Scanner; public class Main { private static User currentUser = null; // 当前登录用户 private static Scanner scanner = new Scanner(System.in); private static UserService userService = new UserService(); public static void main(String[] args) { System.out.println("=== 图书管理系统 ==="); while (true) { if (currentUser == null) { showLoginMenu(); } else { if ("admin".equals(currentUser.getRole())) { showAdminMenu(); } else { showReaderMenu(); } } } } private static void showLoginMenu() { System.out.println("\n1. 登录"); System.out.println("0. 退出"); System.out.print("请选择: "); int choice = scanner.nextInt(); scanner.nextLine(); // 消耗换行符 switch (choice) { case 1: login(); break; case 0: System.out.println("感谢使用,再见!"); System.exit(0); break; default: System.out.println("无效选择!"); } } private static void login() { System.out.print("用户名: "); String username = scanner.nextLine(); System.out.print("密码: "); String password = scanner.nextLine(); currentUser = userService.login(username, password); if (currentUser != null) { System.out.println("登录成功!欢迎," + currentUser.getName()); } else { System.out.println("用户名或密码错误!"); } } private static void showAdminMenu() { System.out.println("\n=== 管理员菜单 ==="); System.out.println("1. 图书管理"); System.out.println("2. 用户管理"); System.out.println("3. 查看所有借阅记录"); System.out.println("9. 注销"); System.out.print("请选择: "); int choice = scanner.nextInt(); scanner.nextLine(); switch (choice) { case 1: // 调用图书管理子菜单 BookManagementMenu.showMenu(scanner); break; case 2: // 调用用户管理子菜单 UserManagementMenu.showMenu(scanner); break; case 3: // 查看所有借阅记录 break; case 9: currentUser = null; System.out.println("已注销。"); break; default: System.out.println("无效选择!"); } } private static void showReaderMenu() { System.out.println("\n=== 读者菜单 ==="); System.out.println("1. 查询图书"); System.out.println("2. 借阅图书"); System.out.println("3. 归还图书"); System.out.println("4. 我的借阅记录"); System.out.println("9. 注销"); System.out.print("请选择: "); int choice = scanner.nextInt(); scanner.nextLine(); switch (choice) { case 1: // 查询图书 break; case 2: // 借阅图书 break; case 3: // 归还图书 break; case 4: // 查看我的借阅记录 break; case 9: currentUser = null; System.out.println("已注销。"); break; default: System.out.println("无效选择!"); } } }你需要创建BookManagementMenu和UserManagementMenu等类来细化每个菜单项的功能,调用对应的Service方法,并通过ConsoleUtil(一个封装了Scanner的工具类)与用户交互。
5. 运行验证与常见问题排查
完成所有代码后,运行Main类的main方法。如果一切顺利,你将看到登录界面。使用初始插入的管理员账号admin/admin123登录,即可进入管理员菜单。
5.1 常见启动问题排查
在运行过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 检查与解决方式 |
|---|---|---|
ClassNotFoundException: com.mysql.cj.jdbc.Driver | 1. MySQL 驱动依赖未下载。 2. pom.xml中依赖版本错误或未同步。 | 1. 在 IDEA 中,点击 Maven 面板的刷新按钮。 2. 检查本地 Maven 仓库是否存在对应 jar 包。 |
Communications link failure或连接被拒绝 | 1. MySQL 服务未启动。 2. db.properties中的 URL、端口、用户名、密码错误。3. 数据库 book_db不存在。 | 1. 确保 MySQL 服务已运行 (net start mysql)。2. 仔细核对配置文件。 3. 执行本文 3.1 节的 SQL 创建数据库。 |
The server time zone value '...' is unrecognized | 数据库连接 URL 未指定时区。 | 在db.properties的jdbc.url中确保包含&serverTimezone=Asia/Shanghai(或其他有效时区)。 |
控制台输入Scanner跳转或出错 | nextInt()后未消耗换行符。 | 在nextInt()、nextDouble()后加一行scanner.nextLine();来读取多余的换行符。 |
| 增删改操作成功,但数据库没变化 | 未提交事务(如果使用了事务)或连接未关闭自动提交。 | 检查代码中是否显式调用了connection.commit()。对于简单的 JDBC,默认是自动提交的。更常见的是 SQL 语句写错,但未抛出异常。建议打印 SQL 语句或在数据库客户端直接执行测试。 |
查询结果始终为null或空列表 | 1. SQL 查询条件错误。 2. 实体类属性与 ResultSet字段名不匹配。3. 结果集遍历逻辑错误(如 if(rs.next())写成了while)。 | 1. 打印出执行的 SQL 语句进行调试。 2. 检查 rs.getXxx(“字段名”)中的字段名是否与数据库表完全一致(区分大小写)。3. 使用调试模式,查看 ResultSet对象内部数据。 |
5.2 项目结构检查清单
在遇到问题时,可以按照以下清单逐一核对:
- 依赖检查:
pom.xml中的 MySQL 驱动版本是否正确?Maven 是否已下载依赖? - 配置检查:
src/main/resources/db.properties文件是否存在?路径和文件名是否正确?配置项的值是否正确? - 数据库检查:MySQL 服务是否运行?
book_db数据库及三张表是否创建成功?初始管理员用户是否存在? - 实体类检查:
Book、User、BorrowRecord类的属性名、类型是否与数据库表字段对应?Getter/Setter 方法是否完整? - DAO 层检查:SQL 语句字符串是否正确?
PreparedStatement的参数设置(setXxx)顺序和类型是否与 SQL 中的?匹配? - Service 层检查:业务逻辑(如库存检查)是否正确?是否处理了可能的异常或边界情况?
- 控制台交互检查:菜单逻辑是否清晰?
Scanner的使用是否正确,有无输入流混乱?
6. 从演示到生产:关键优化与扩展方向
当前版本是一个为了教学清晰而简化的控制台程序。要将其变成一个健壮、可维护的项目,还需要在许多方面进行加强。
6.1 安全性优化
- 密码加密:绝对不要明文存储密码。使用
BCryptPasswordEncoder等工具进行哈希加盐存储。登录时比较哈希值。// 示例:使用 Spring Security 的 BCrypt // String encodedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // boolean matches = BCrypt.checkpw(rawPassword, storedHash); - SQL 注入防护:始终坚持使用
PreparedStatement,避免字符串拼接 SQL。 - 输入验证:对所有用户输入(如 ISBN、价格)进行有效性验证,防止非法数据进入系统。
6.2 代码结构与可维护性优化
- 引入接口:为
BookDao、BookService等定义接口(如IBookDao,IBookService),基于接口编程,提高可测试性和可替换性。 - 依赖注入:避免在类内部
new对象。可以引入简单的工厂模式或学习 Spring 框架的 IoC 容器。 - 异常处理:定义业务异常类,在 Service 层抛出,在 UI 层统一捕获并给出友好提示,而不是到处打印
e.printStackTrace()。 - 日志记录:使用
SLF4J+Logback替代System.out.println,可以灵活控制日志级别和输出目的地。
6.3 功能扩展方向
- Web 化:这是最自然的扩展。学习
Servlet+JSP或直接使用Spring Boot框架,将控制台菜单变为网页。 - 加入事务管理:如前所述,借阅和归还操作必须放在一个数据库事务中。可以使用
JDBC手动管理,或使用Spring的@Transactional注解。 - 增加分页查询:当图书或借阅记录很多时,需要分页功能。在 SQL 中使用
LIMIT ?, ?实现。 - 模糊搜索:实现按书名、作者进行模糊查询(
LIKE)。 - 借阅规则:增加借阅期限、超期罚款、同一用户最大借阅数等业务规则。
- 数据导出:将图书列表、借阅记录导出为 Excel 或 PDF 文件。
通过完成这个项目,你不仅实践了 Java 核心语法和 JDBC,更重要的是体会了如何将一个业务需求分解为模块、设计数据库、组织代码结构、处理异常以及迭代优化。接下来,你可以选择上述任何一个扩展方向进行深入研究,例如尝试用 Spring Boot 重写这个系统,这将让你对现代 Java 企业级开发有更深刻的理解。