很多Java初学者在学完基础语法后,都会陷入一个迷茫期:知道if-else、for循环,也了解类和对象,但就是不知道如何把这些零散的知识点串联起来,做出一个“像样”的东西。网上的“图书管理系统”项目看似简单,但如果你只是跟着视频敲一遍代码,很可能只知其然,不知其所以然,遇到真实需求依然无从下手。
这篇文章要解决的,正是这个痛点。我们不只提供一个能运行的代码,而是要拆解一个图书管理系统从需求分析、技术选型、数据库设计到代码实现的完整思维过程。你会发现,这个经典项目背后,藏着Java面向对象设计、三层架构、数据库操作和异常处理的核心思想。掌握了这套思维,你不仅能做出图书管理系统,更能举一反三,应对任何中小型业务系统的开发。
本文将带你从零开始,构建一个控制台版本的图书管理系统。我们会重点讲解:为什么选用三层架构而不是把所有代码写在一个类里?实体类(Entity)、数据访问层(DAO)、业务逻辑层(Service)各自承担什么职责?如何设计数据库表才能避免后期修改的麻烦?以及,那些教程里很少提,但实际开发中一定会遇到的“坑”,比如事务处理、输入验证和资源关闭。
1. 图书管理系统:不只是CRUD,更是工程思维的起点
当你搜索“Java项目”时,“图书管理系统”总是高频出现。它被当作新手练手项目,但很多人低估了它的教学价值。它本质上是一个标准的CRUD(增删改查)应用,涵盖了后端开发最核心的数据操作。但它的意义远不止于此:
- 面向对象思想的实战场:如何将“图书”、“读者”、“借阅记录”这些现实概念抽象成Java类(实体类)?
- 三层架构的入门样板:如何将数据访问、业务逻辑和用户界面分离,让代码更清晰、更易维护?
- 数据库设计的初级演练:表结构如何设计?主键、外键有什么用?如何避免数据冗余?
- 异常处理与健壮性培养:用户输入错误怎么办?数据库连接失败如何处理?
如果你能独立完成并真正理解这个项目,就意味着你已经跨过了“写语法片段”的阶段,开始具备“构建小型应用”的工程化思维能力。这是你Java学习路上一个重要的里程碑。
2. 核心概念与系统架构设计
在动手写代码之前,我们必须先理清几个关键概念和整体的系统设计蓝图。这是避免代码混乱的前提。
2.1 核心实体(Entity)
这是对系统中核心业务对象的抽象。在我们的系统里,主要有三个:
- 图书(Book):属性包括图书ID(主键)、书名、作者、出版社、库存数量等。
- 读者(Reader):属性包括读者ID(主键)、姓名、联系方式等。
- 借阅记录(BorrowRecord):这是关联图书和读者的纽带,属性包括记录ID、图书ID(外键)、读者ID(外键)、借出日期、应还日期、实际归还日期等。它清晰地反映了实体间的关系。
2.2 三层架构(3-Tier Architecture)
这是本项目采用的经典架构,目的是解耦,让每一层只关心自己的事。
- 数据访问层(DAO - Data Access Object):只负责和数据库打交道。它的方法名通常是
insert,delete,update,selectById,selectAll等。这一层不关心业务规则(比如“读者最多借5本书”)。 - 业务逻辑层(Service):负责实现具体的业务规则和流程。例如,“借书”这个业务,Service层会先调用DAO层检查图书库存,再检查读者是否超借,最后才调用DAO层插入借阅记录并更新图书库存。它是连接表示层和DAO层的桥梁。
- 表示层(UI / Controller):负责与用户交互。在控制台项目中,就是那个打印菜单、接收用户输入、调用Service方法的
main函数所在类。它不应该包含任何SQL或复杂的业务判断。
为什么不用所有代码都写在main方法里?那么做会导致代码高度耦合,修改数据库或业务规则时犹如牵一发而动全身,调试和扩展将是噩梦。三层架构是软件工程中应对复杂性的基本手段。
2.3 数据库表结构设计
良好的表设计是系统的基石。这里我们设计三张表:
book表
| 字段名 | 类型 | 说明 |
|---|---|---|
id | INT PRIMARY KEY AUTO_INCREMENT | 主键,自增 |
isbn | VARCHAR(20) UNIQUE | 国际标准书号,具有唯一性 |
name | VARCHAR(100) NOT NULL | 书名 |
author | VARCHAR(50) | 作者 |
publisher | VARCHAR(50) | 出版社 |
total_quantity | INT DEFAULT 0 | 总数量 |
current_quantity | INT DEFAULT 0 | 当前可借数量 |
reader表
| 字段名 | 类型 | 说明 |
|---|---|---|
id | INT PRIMARY KEY AUTO_INCREMENT | 主键,自增 |
card_number | VARCHAR(20) UNIQUE NOT NULL | 借书证号,唯一 |
name | VARCHAR(50) NOT NULL | 读者姓名 |
type | VARCHAR(10) | 读者类型(如:学生、教师) |
borrow_record表
| 字段名 | 类型 | 说明 |
|---|---|---|
id | INT PRIMARY KEY AUTO_INCREMENT | 主键,自增 |
book_id | INT NOT NULL | 外键,关联book.id |
reader_id | INT NOT NULL | 外键,关联reader.id |
borrow_date | DATETIME NOT NULL | 借出日期 |
due_date | DATETIME NOT NULL | 应还日期 |
return_date | DATETIME | 实际归还日期(为空表示未还) |
设计要点:
book表中的current_quantity(当前库存)是专门为查询效率设计的业务字段。如果每次都要通过总数量 - 已借出数量来计算,会涉及多表关联和聚合查询,性能较差。borrow_record表通过book_id和reader_id两个外键,建立了图书和读者的多对多关系(一个读者可借多本书,一本书可被多个读者借,但不同时)。- 使用
AUTO_INCREMENT让数据库自动生成主键,比在程序里生成更简单、更高效。
3. 环境准备与项目搭建
我们使用最通用的技术栈,确保任何新手都能顺利复现。
- JDK:版本 8 或以上(推荐JDK 11或17,LTS版本更稳定)。
- IDE:IntelliJ IDEA Community Edition(免费且强大)或 Eclipse。
- 数据库:MySQL 5.7 或 8.0。你需要安装MySQL并启动服务。
- 构建工具:Maven。用于管理项目依赖(JDBC驱动)。
- 数据库客户端:可以使用IDEA自带的Database工具,或Navicat、MySQL Workbench。
第一步:创建Maven项目在IDEA中,选择File -> New -> Project,选择Maven,确认JDK版本,点击Next。填写GroupId(如com.example)和ArtifactId(如library-management),完成创建。
第二步:添加依赖打开项目根目录下的pom.xml文件,在<dependencies>标签内添加MySQL驱动依赖。
<!-- pom.xml --> <dependencies> <!-- MySQL JDBC 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 请根据你的MySQL版本调整 --> </dependency> </dependencies>添加后,IDEA通常会自动下载依赖包。如果没有,可以点击Maven工具栏的刷新按钮。
第三步:创建数据库和表使用你的数据库客户端(如MySQL Workbench)连接MySQL,然后执行以下SQL语句:
-- 创建数据库 CREATE DATABASE IF NOT EXISTS library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_db; -- 创建图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE, name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), total_quantity INT DEFAULT 0, current_quantity INT DEFAULT 0 ); -- 创建读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, card_number VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, type VARCHAR(10) ); -- 创建借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME NOT NULL, due_date DATETIME NOT NULL, return_date DATETIME, FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE, FOREIGN KEY (reader_id) REFERENCES reader(id) ON DELETE CASCADE ); -- 插入一些测试数据 INSERT INTO book (isbn, name, author, publisher, total_quantity, current_quantity) VALUES ('978-7-02-123456-7', 'Java核心技术 卷I', 'Cay S. Horstmann', '机械工业出版社', 5, 5), ('978-7-111-54321-0', 'Effective Java', 'Joshua Bloch', '机械工业出版社', 3, 3); INSERT INTO reader (card_number, name, type) VALUES ('R20240001', '张三', '学生'), ('R20240002', '李四', '教师');4. 核心代码实现:从实体类到三层架构
接下来,我们按照三层架构,从底层到上层依次实现。请在src/main/java下创建对应的包结构:com.yourdomain.library.entity,com.yourdomain.library.dao,com.yourdomain.library.service,com.yourdomain.library.util,com.yourdomain.library.ui。
4.1 实体类(Entity)
实体类是表的映射,每个属性对应表的一个字段。
// 文件路径:src/main/java/com/yourdomain/library/entity/Book.java package com.yourdomain.library.entity; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private Integer totalQuantity; // 总数量 private Integer currentQuantity; // 当前可借数量 // 构造方法、Getter和Setter省略,但实际必须要有。 // 可以使用IDE的Generate功能快速生成(Alt+Insert)。 // 示例:无参构造和全参构造 public Book() {} public Book(Integer id, String isbn, String name, String author, String publisher, Integer totalQuantity, Integer currentQuantity) { this.id = id; this.isbn = isbn; this.name = name; this.author = author; this.publisher = publisher; this.totalQuantity = totalQuantity; this.currentQuantity = currentQuantity; } // Getter and Setter... }Reader和BorrowRecord实体类请参照上述格式创建。注意BorrowRecord中的日期字段可以使用java.util.Date或java.time.LocalDateTime(推荐,JDK8+)。
4.2 工具类:数据库连接(Util)
这是一个关键工具类,负责创建和关闭数据库连接。务必注意资源关闭,否则会导致连接泄漏。
// 文件路径:src/main/java/com/yourdomain/library/util/DatabaseUtil.java package com.yourdomain.library.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DatabaseUtil { // 数据库连接信息,通常应该放在配置文件中 private static final String URL = "jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; // 你的数据库用户名 private static final String PASSWORD = "yourpassword"; // 你的数据库密码 static { try { // 加载驱动 (MySQL 8.0+ 可以省略,但显式加载更安全) Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException("找不到MySQL JDBC驱动!"); } } /** * 获取数据库连接 * @return Connection 对象 */ public static Connection getConnection() { try { return DriverManager.getConnection(URL, USER, PASSWORD); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("获取数据库连接失败!", e); } } /** * 关闭连接、Statement、ResultSet等资源 * 这是一个通用的关闭方法,可以安全地关闭多个资源。 */ public static void close(AutoCloseable... closeables) { for (AutoCloseable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { // 关闭资源时的异常通常只记录日志,不向上抛出 e.printStackTrace(); } } } } }4.3 数据访问层(DAO)
以BookDao为例,展示基础的CRUD操作。这里使用最基础的JDBC,以便理解原理。
// 文件路径:src/main/java/com/yourdomain/library/dao/BookDao.java package com.yourdomain.library.dao; import com.yourdomain.library.entity.Book; import com.yourdomain.library.util.DatabaseUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { /** * 新增图书 */ public boolean insert(Book book) { String sql = "INSERT INTO book (isbn, name, author, publisher, total_quantity, current_quantity) VALUES (?, ?, ?, ?, ?, ?)"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DatabaseUtil.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.setInt(5, book.getTotalQuantity()); pstmt.setInt(6, book.getCurrentQuantity()); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DatabaseUtil.close(pstmt, conn); // 务必在finally块中关闭资源 } } /** * 根据ID查询图书 */ public Book selectById(Integer id) { String sql = "SELECT * FROM book WHERE id = ?"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; Book book = null; try { conn = DatabaseUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); rs = pstmt.executeQuery(); if (rs.next()) { 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.setTotalQuantity(rs.getInt("total_quantity")); book.setCurrentQuantity(rs.getInt("current_quantity")); } } catch (SQLException e) { e.printStackTrace(); } finally { DatabaseUtil.close(rs, pstmt, conn); } return book; } /** * 查询所有图书 */ public List<Book> selectAll() { List<Book> bookList = new ArrayList<>(); String sql = "SELECT * FROM book"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DatabaseUtil.getConnection(); pstmt = conn.prepareStatement(sql); rs = pstmt.executeQuery(); while (rs.next()) { Book book = new Book(); // 同上,从ResultSet中提取数据并设置到book对象中 book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); book.setName(rs.getString("name")); // ... 设置其他属性 bookList.add(book); } } catch (SQLException e) { e.printStackTrace(); } finally { DatabaseUtil.close(rs, pstmt, conn); } return bookList; } /** * 更新图书信息 (例如,修改库存) */ public boolean update(Book book) { String sql = "UPDATE book SET isbn=?, name=?, author=?, publisher=?, total_quantity=?, current_quantity=? WHERE id=?"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DatabaseUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, book.getIsbn()); // ... 设置其他参数 pstmt.setInt(7, book.getId()); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DatabaseUtil.close(pstmt, conn); } } /** * 根据ID删除图书 */ public boolean deleteById(Integer id) { String sql = "DELETE FROM book WHERE id = ?"; Connection conn = null; PreparedStatement pstmt = null; try { conn = DatabaseUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DatabaseUtil.close(pstmt, conn); } } }ReaderDao和BorrowRecordDao的实现逻辑类似,主要区别在于SQL语句和实体类映射。
4.4 业务逻辑层(Service)
这是体现业务规则的地方。我们以“借书”这个核心业务为例,看看Service层如何协调多个DAO操作。
// 文件路径:src/main/java/com/yourdomain/library/service/BorrowService.java package com.yourdomain.library.service; import com.yourdomain.library.dao.BookDao; import com.yourdomain.library.dao.BorrowRecordDao; import com.yourdomain.library.dao.ReaderDao; import com.yourdomain.library.entity.Book; import com.yourdomain.library.entity.BorrowRecord; import com.yourdomain.library.entity.Reader; import java.time.LocalDateTime; import java.util.List; 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 成功与否的消息 */ public String borrowBook(Integer bookId, Integer readerId, Integer borrowDays) { // 1. 检查图书是否存在且可借 Book book = bookDao.selectById(bookId); if (book == null) { return "错误:图书不存在!"; } if (book.getCurrentQuantity() <= 0) { return "错误:该图书已无库存!"; } // 2. 检查读者是否存在 Reader reader = readerDao.selectById(readerId); if (reader == null) { return "错误:读者不存在!"; } // 3. (可选) 检查读者是否已超借。这里假设一个简单规则:最多借5本未还的书。 List<BorrowRecord> unreturnedRecords = borrowRecordDao.selectUnreturnedByReaderId(readerId); if (unreturnedRecords != null && unreturnedRecords.size() >= 5) { return "错误:该读者借书已达上限(5本)!"; } // 4. 执行借阅操作(更新库存 + 插入记录) // 注意:这里存在两个数据库操作,应该放在一个事务中。简化起见,我们先顺序执行。 // 在实际项目中,必须使用事务保证原子性。 book.setCurrentQuantity(book.getCurrentQuantity() - 1); boolean updateBookSuccess = bookDao.update(book); BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); LocalDateTime now = LocalDateTime.now(); record.setBorrowDate(now); record.setDueDate(now.plusDays(borrowDays)); // return_date 为 null boolean insertRecordSuccess = borrowRecordDao.insert(record); if (updateBookSuccess && insertRecordSuccess) { return "借书成功!"; } else { // 如果有一个操作失败,应该回滚。这里简化处理。 return "错误:借书操作失败,请稍后重试或联系管理员。"; } } /** * 还书业务逻辑 */ public String returnBook(Integer recordId) { // 1. 查询借阅记录 BorrowRecord record = borrowRecordDao.selectById(recordId); if (record == null) { return "错误:借阅记录不存在!"; } if (record.getReturnDate() != null) { return "错误:该书已归还!"; } // 2. 更新借阅记录(设置归还日期) record.setReturnDate(LocalDateTime.now()); boolean updateRecordSuccess = borrowRecordDao.update(record); // 3. 更新图书库存 Book book = bookDao.selectById(record.getBookId()); if (book != null) { book.setCurrentQuantity(book.getCurrentQuantity() + 1); boolean updateBookSuccess = bookDao.update(book); if (updateRecordSuccess && updateBookSuccess) { return "还书成功!"; } } return "错误:还书操作失败!"; } }Service层的核心价值:它把“借书”这个用户眼中的单一操作,拆解成了多个数据校验和DAO调用步骤,并封装了业务规则(库存检查、借阅上限)。UI层只需要调用borrowBook方法,无需关心内部细节。
4.5 表示层(UI)- 控制台菜单
这是用户交互的入口,结构清晰即可。
// 文件路径:src/main/java/com/yourdomain/library/ui/LibraryConsoleUI.java package com.yourdomain.library.ui; import com.yourdomain.library.service.BookService; import com.yourdomain.library.service.BorrowService; import com.yourdomain.library.service.ReaderService; import java.util.Scanner; public class LibraryConsoleUI { private Scanner scanner = new Scanner(System.in); private BookService bookService = new BookService(); private ReaderService readerService = new ReaderService(); private BorrowService borrowService = new BorrowService(); public void start() { while (true) { showMainMenu(); int choice = getIntInput("请选择操作:"); 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("感谢使用,再见!"); return; default: System.out.println("输入无效,请重新选择。"); } } } private 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 void manageBooks() { // 子菜单:新增、查询、修改、删除图书 System.out.println("\n--- 图书管理 ---"); // 这里调用 bookService 的方法 // 例如:bookService.addBook(...); } private void borrowBook() { System.out.println("\n--- 办理借书 ---"); Integer bookId = getIntInput("请输入图书ID:"); Integer readerId = getIntInput("请输入读者ID:"); Integer days = getIntInput("请输入借阅天数:"); String result = borrowService.borrowBook(bookId, readerId, days); System.out.println(result); } private void returnBook() { System.out.println("\n--- 办理还书 ---"); Integer recordId = getIntInput("请输入借阅记录ID:"); String result = borrowService.returnBook(recordId); System.out.println(result); } private void queryRecords() { // 调用 service 查询并展示记录 System.out.println("查询功能待实现..."); } private int getIntInput(String prompt) { System.out.print(prompt); while (!scanner.hasNextInt()) { System.out.println("输入错误,请输入一个整数!"); scanner.next(); // 消耗掉错误的输入 System.out.print(prompt); } int value = scanner.nextInt(); scanner.nextLine(); // 消耗掉行尾的换行符,避免影响下一次输入 return value; } // 主程序入口 public static void main(String[] args) { new LibraryConsoleUI().start(); } }5. 运行与测试
- 启动数据库:确保你的MySQL服务已经运行。
- 运行程序:在IDEA中,找到
LibraryConsoleUI类,右键点击main方法,选择Run 'LibraryConsoleUI.main()'。 - 功能测试:
- 系统启动后,会显示主菜单。
- 选择“3. 借书”,根据提示输入我们之前插入的测试图书ID(如1)和读者ID(如1),以及借阅天数(如30)。
- 如果一切正常,会看到“借书成功!”的提示。
- 此时,你可以去数据库查看
book表,对应图书的current_quantity应该减1,borrow_record表会新增一条记录。 - 选择“4. 还书”,输入刚才产生的借阅记录ID,完成还书操作。库存会恢复。
6. 常见问题与排查思路
在实现和运行过程中,你几乎一定会遇到下面这些问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
控制台报错:java.sql.SQLException: No suitable driver found | 1. MySQL驱动JAR包未正确引入。 2. JDBC URL格式错误。 | 1. 检查pom.xml依赖是否正确,Maven是否下载成功。2. 检查 DatabaseUtil中的URL字符串。 | 1. 在IDEA的Maven面板点击刷新。 2. 确保URL格式为 jdbc:mysql://主机:端口/数据库名?参数。 |
控制台报错:Access denied for user 'root'@'localhost' | 数据库用户名或密码错误。 | 核对DatabaseUtil中的USER和PASSWORD常量。 | 使用正确的数据库凭据。 |
控制台报错:Unknown database 'library_db' | 指定的数据库不存在。 | 检查SQL脚本是否成功执行,数据库名是否拼写正确。 | 回到第3步,手动执行建库SQL语句。 |
| 程序运行后,操作数据库没效果 | 1. 未提交事务(如果使用了事务)。 2. 代码逻辑错误, executeUpdate返回0。 | 1. 检查DAO层代码,确认执行了executeUpdate。2. 在关键步骤打印日志或调试。 | 1. 对于简单的JDBC,默认自动提交。如果手动管理事务,需要conn.commit()。2. 使用 System.out.println输出SQL语句和参数,去数据库客户端手动执行,看是否成功。 |
| 查询结果始终为null或空列表 | 1. SQL语句有误,特别是WHERE条件。 2. 实体类字段与ResultSet列名映射错误。 | 1. 打印出最终执行的SQL语句(带参数)。 2. 检查DAO中 rs.getXxx(“列名”)的列名是否与数据库表字段完全一致(注意大小写和下划线)。 | 1. 修正SQL语句。 2. 确保列名匹配,或使用 rs.getXxx(索引)。 |
| 输入非数字导致程序崩溃 | UI层未对用户输入进行验证。 | 检查getIntInput方法是否被正确调用和处理。 | 使用Scanner.hasNextInt()进行判断,如示例代码所示。 |
7. 从Demo到项目:最佳实践与进阶思考
现在你已经有了一个可运行的系统,但距离一个“健壮”的项目还有距离。以下是你需要继续思考和优化的方向,这也是面试中常被问到的“项目亮点”。
7.1 引入数据库连接池
目前的DatabaseUtil每次操作都新建连接,性能极差。生产环境必须使用连接池,如HikariCP、Druid。
<!-- 在pom.xml中添加HikariCP依赖 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>然后在DatabaseUtil中配置并提供一个从连接池获取连接的方法。
7.2 使用事务管理
当前的borrowBook方法中,更新库存和插入记录是两个独立操作。如果插入记录失败,库存却已经减少,会导致数据不一致。必须使用事务将它们捆绑。
Connection conn = null; try { conn = DatabaseUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 执行多个DAO操作... conn.commit(); // 提交事务 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); } finally { DatabaseUtil.close(conn); }7.3 使用DAO接口与实现分离
目前DAO是具体类,不利于单元测试和切换实现(比如从JDBC换到MyBatis)。应定义BookDao接口,然后提供BookDaoJdbcImpl实现。
7.4 日志记录
用System.out.println调试是临时手段。应集成日志框架,如SLF4J + Logback,记录系统运行信息、错误和操作日志。
7.5 输入验证与异常处理
UI层应对用户输入做更严格的验证(如书名不能为空,借阅天数必须为正数)。Service层应定义明确的业务异常(如BookNotFoundException,InsufficientStockException),而不是返回字符串,这样上层可以更灵活地处理。
7.6 迈向Web应用
这是最自然的进阶。你可以用Spring Boot快速重构本项目:
- 将实体类加上JPA注解(
@Entity,@Id)。 - 将DAO层改为Spring Data JPA的Repository接口。
- 将Service层改为Spring的
@Service,利用@Transactional管理事务。 - 将UI层改为Spring MVC的
@RestController或@Controller,提供RESTful API或Thymeleaf页面。
当你用Spring Boot重写一遍后,会对现代Java Web开发有截然不同的理解。
通过这个项目,你实践了从需求分析、数据库设计、三层架构搭建到基础功能实现的完整流程。它像一块敲门砖,帮你把散落的Java语法、SQL知识、面向对象概念砌成了一面墙。接下来,你可以沿着连接池、事务、日志、Web框架这些方向继续深挖,把这面墙筑成一座坚固的房子。真正的学习,始于你能独立解决每一个报错,并思考如何让代码变得更好的那一刻。建议你收藏本文,在实践每个步骤时,多问几个“为什么”,并尝试去实现第七部分提到的优化点。