news 2026/9/2 17:49:28

Java图书管理系统实战:从三层架构到数据库设计完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java图书管理系统实战:从三层架构到数据库设计完整指南

很多Java初学者在学完基础语法后,都会陷入一个迷茫期:知道if-elsefor循环,也了解类和对象,但就是不知道如何把这些零散的知识点串联起来,做出一个“像样”的东西。网上的“图书管理系统”项目看似简单,但如果你只是跟着视频敲一遍代码,很可能只知其然,不知其所以然,遇到真实需求依然无从下手。

这篇文章要解决的,正是这个痛点。我们不只提供一个能运行的代码,而是要拆解一个图书管理系统从需求分析、技术选型、数据库设计到代码实现的完整思维过程。你会发现,这个经典项目背后,藏着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)

这是本项目采用的经典架构,目的是解耦,让每一层只关心自己的事。

  1. 数据访问层(DAO - Data Access Object)只负责和数据库打交道。它的方法名通常是insert,delete,update,selectById,selectAll等。这一层不关心业务规则(比如“读者最多借5本书”)。
  2. 业务逻辑层(Service)负责实现具体的业务规则和流程。例如,“借书”这个业务,Service层会先调用DAO层检查图书库存,再检查读者是否超借,最后才调用DAO层插入借阅记录并更新图书库存。它是连接表示层和DAO层的桥梁。
  3. 表示层(UI / Controller)负责与用户交互。在控制台项目中,就是那个打印菜单、接收用户输入、调用Service方法的main函数所在类。它不应该包含任何SQL或复杂的业务判断。

为什么不用所有代码都写在main方法里?那么做会导致代码高度耦合,修改数据库或业务规则时犹如牵一发而动全身,调试和扩展将是噩梦。三层架构是软件工程中应对复杂性的基本手段。

2.3 数据库表结构设计

良好的表设计是系统的基石。这里我们设计三张表:

book表

字段名类型说明
idINT PRIMARY KEY AUTO_INCREMENT主键,自增
isbnVARCHAR(20) UNIQUE国际标准书号,具有唯一性
nameVARCHAR(100) NOT NULL书名
authorVARCHAR(50)作者
publisherVARCHAR(50)出版社
total_quantityINT DEFAULT 0总数量
current_quantityINT DEFAULT 0当前可借数量

reader表

字段名类型说明
idINT PRIMARY KEY AUTO_INCREMENT主键,自增
card_numberVARCHAR(20) UNIQUE NOT NULL借书证号,唯一
nameVARCHAR(50) NOT NULL读者姓名
typeVARCHAR(10)读者类型(如:学生、教师)

borrow_record表

字段名类型说明
idINT PRIMARY KEY AUTO_INCREMENT主键,自增
book_idINT NOT NULL外键,关联book.id
reader_idINT NOT NULL外键,关联reader.id
borrow_dateDATETIME NOT NULL借出日期
due_dateDATETIME NOT NULL应还日期
return_dateDATETIME实际归还日期(为空表示未还)

设计要点

  • book表中的current_quantity(当前库存)是专门为查询效率设计的业务字段。如果每次都要通过总数量 - 已借出数量来计算,会涉及多表关联和聚合查询,性能较差。
  • borrow_record表通过book_idreader_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... }

ReaderBorrowRecord实体类请参照上述格式创建。注意BorrowRecord中的日期字段可以使用java.util.Datejava.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); } } }

ReaderDaoBorrowRecordDao的实现逻辑类似,主要区别在于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. 运行与测试

  1. 启动数据库:确保你的MySQL服务已经运行。
  2. 运行程序:在IDEA中,找到LibraryConsoleUI类,右键点击main方法,选择Run 'LibraryConsoleUI.main()'
  3. 功能测试
    • 系统启动后,会显示主菜单。
    • 选择“3. 借书”,根据提示输入我们之前插入的测试图书ID(如1)和读者ID(如1),以及借阅天数(如30)。
    • 如果一切正常,会看到“借书成功!”的提示。
    • 此时,你可以去数据库查看book表,对应图书的current_quantity应该减1,borrow_record表会新增一条记录。
    • 选择“4. 还书”,输入刚才产生的借阅记录ID,完成还书操作。库存会恢复。

6. 常见问题与排查思路

在实现和运行过程中,你几乎一定会遇到下面这些问题。

问题现象可能原因排查方式解决方案
控制台报错:java.sql.SQLException: No suitable driver found1. 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中的USERPASSWORD常量。使用正确的数据库凭据。
控制台报错: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快速重构本项目:

  1. 将实体类加上JPA注解(@Entity,@Id)。
  2. 将DAO层改为Spring Data JPA的Repository接口。
  3. 将Service层改为Spring的@Service,利用@Transactional管理事务。
  4. 将UI层改为Spring MVC的@RestController@Controller,提供RESTful API或Thymeleaf页面。

当你用Spring Boot重写一遍后,会对现代Java Web开发有截然不同的理解。

通过这个项目,你实践了从需求分析、数据库设计、三层架构搭建到基础功能实现的完整流程。它像一块敲门砖,帮你把散落的Java语法、SQL知识、面向对象概念砌成了一面墙。接下来,你可以沿着连接池、事务、日志、Web框架这些方向继续深挖,把这面墙筑成一座坚固的房子。真正的学习,始于你能独立解决每一个报错,并思考如何让代码变得更好的那一刻。建议你收藏本文,在实践每个步骤时,多问几个“为什么”,并尝试去实现第七部分提到的优化点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 14:34:13

MCGS与OPC通讯实战:从DCOM配置到故障排查全解析

简介&#xff1a;本资源是面向工业自动化工程师、SCADA系统开发人员及高校控制类专业学习者的MCGS与OPC通信实战配置包&#xff0c;解决跨厂商设备数据集成与实时监控系统搭建中的核心通讯难题。压缩包共15个文件&#xff0c;涵盖MCGS工程文件&#xff08;.mcg&#xff09;、设…

作者头像 李华
网站建设 2026/9/1 10:52:49

Claude Code思考杠杆实战:用提示词控制模型推理深度

刚看完 Claude 官方关于“思考杠杆”的实战分享&#xff0c;又把自己手上几个用到 Claude Code 的项目重新调了一遍参数。最大的感受是&#xff1a;Claude 系列模型的推理能力确实强&#xff0c;但如果不会控制“思考的方式和深度”&#xff0c;很多时候只是把同样的低效思考重…

作者头像 李华
网站建设 2026/9/1 10:52:42

Kali Linux 安全入门:从零搭建虚拟渗透测试实验室

在网络安全领域&#xff0c;Kali Linux 是一个无法绕开的工具集。它预装了数百种用于渗透测试、安全审计和网络取证的工具&#xff0c;为安全研究人员和网络管理员提供了一个开箱即用的专业平台。然而&#xff0c;对于初学者而言&#xff0c;面对一个全新的操作系统和大量陌生的…

作者头像 李华
网站建设 2026/9/1 10:50:44

分布式锁实战指南:从Redis SETNX到Redisson的演进与最佳实践

在分布式系统架构成为主流的今天&#xff0c;你是否遇到过这样的场景&#xff1a;一个商品秒杀活动&#xff0c;明明库存显示充足&#xff0c;用户下单后却出现了超卖&#xff1b;或者一个重要的定时任务&#xff0c;在多台服务器上被重复执行&#xff0c;导致数据混乱。这些问…

作者头像 李华
网站建设 2026/9/1 10:49:39

第一个妙招怎么建?从一句常用指令开始

知道妙招好用&#xff0c;但一打开就卡住&#xff1a;建什么?很多人在这一步放弃了&#xff0c;觉得自己没有值得固化的流程。其实第一个妙招不需要设计&#xff0c;只需要回忆。1. 从最近一周找素材打开你的聊天记录&#xff0c;找这样一段内容&#xff1a;你在两三天里输入过…

作者头像 李华