news 2026/8/12 20:13:27

Java图书管理系统实战:从JDBC到三层架构的完整项目构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java图书管理系统实战:从JDBC到三层架构的完整项目构建

1. 项目概述:从零构建一个扎实的Java图书管理系统

最近在整理自己的项目仓库,翻出了这个几年前写的图书管理系统。它不是什么惊世骇俗的架构,也没有用到当下最时髦的微服务或云原生,但恰恰是这样一个“经典”的课程设计级项目,涵盖了从需求分析、数据库设计、后端逻辑到前端交互的完整闭环,是检验和巩固Java SE及Java Web核心技能的绝佳试金石。很多新手朋友在学完Java基础语法和JDBC后,常常会陷入“接下来该做什么”的迷茫,这个项目就是一个很好的答案。它模拟了一个小型图书馆的核心业务流程,包括图书的增删改查、读者的注册借阅、以及管理员的后台管理。今天,我就把这个项目的完整实现思路、关键代码以及我踩过的那些坑,毫无保留地分享出来,希望能给正在学习Java的你提供一个清晰、可落地的实战参考。

2. 系统核心需求与架构设计

2.1 业务需求拆解:我们到底要做一个什么系统?

在动手写第一行代码之前,我们必须把业务逻辑理清楚。一个最基本的图书管理系统,至少要满足三类用户(角色)的需求:

  1. 读者:核心诉求是“找书”和“借书”。他们需要能查询图书信息(按书名、作者、分类等),查看自己的借阅记录,以及进行图书的借阅与归还操作。
  2. 图书管理员:这是系统的核心操作者。他们需要对图书和读者信息进行全面的管理,包括:
    • 图书管理:新增图书入库、下架旧书、更新图书信息(如补货、修正信息)。
    • 读者管理:审核注册、注销读者账户、管理读者信息。
    • 借阅管理:处理所有借阅和归还请求,处理逾期、丢失等异常情况。
  3. 系统管理员:权限更高,负责管理“图书管理员”账户,进行系统层面的配置与数据维护。

基于以上,我们可以抽象出几个核心的业务实体Book(图书)、Reader(读者)、BorrowRecord(借阅记录)、Admin(管理员)。它们之间的关系是:一个Reader可以有多条BorrowRecord,一条BorrowRecord对应一本Book和一个ReaderAdmin独立于借阅流程之外,负责用户和图书的元数据管理。

2.2 技术栈选型与架构模式:为什么是这些技术?

确定了做什么,接下来就要决定怎么做。技术选型直接决定了开发效率和项目的可维护性。

  • 后端语言Java。这是项目的根基。我们主要使用Java SE的标准库,以及用于数据库连接的JDBC。选择Java是因为其严谨的面向对象特性、强大的生态和在企业级应用中的普遍性,非常适合用来理解业务逻辑的封装与分层。
  • 数据库MySQL。关系型数据库是这类管理系统的标准选择,它能清晰地表达图书、读者、借阅记录之间的关联关系。相比SQLite,MySQL更贴近生产环境;相比NoSQL,它的结构化查询对于复杂的统计报表(如借阅排行榜、逾期分析)更友好。
  • 数据访问层:原生JDBC。虽然MyBatis、Hibernate等ORM框架能极大简化开发,但在学习阶段,我强烈建议从原生JDBC开始。亲手编写SQL语句、处理ResultSet、管理连接池,能让你真正理解Java程序是如何与数据库对话的,这是后续学习任何框架的坚实基础。本项目将演示如何封装一个简单的DBUtil工具类来管理数据库连接和关闭资源,这是避免内存泄漏的关键。
  • 架构模式三层架构。这是经典且有效的分层方式。
    • 表示层:负责与用户交互。为了聚焦后端逻辑,本项目采用控制台界面实现。通过Scanner获取用户输入,打印菜单和结果。这能让我们抛开复杂的前端技术,专注于业务逻辑和数据处理。当然,你可以很容易地将其替换为Swing GUI、JavaFX或一个Spring Boot Web接口。
    • 业务逻辑层:系统的“大脑”。包含BookServiceReaderServiceBorrowService等类,负责处理具体的业务规则,例如“借书前检查读者是否已达借阅上限”、“还书时计算是否逾期并产生罚款”。
    • 数据访问层:系统的“手和脚”。包含BookDaoReaderDao等类,封装所有对数据库的增删改查操作,为业务逻辑层提供纯净的数据服务。

注意:很多教学项目会把所有代码都写在main方法里,这是极其不利于理解和维护的。严格的三层架构分离,是迈向“工程化”开发的第一步。

3. 数据库设计与实现:一切的基础

3.1 数据表结构设计

数据库设计是项目的基石,设计得好,后续编码事半功倍。以下是核心表结构:

1. 图书表book

CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `isbn` VARCHAR(20) UNIQUE NOT NULL COMMENT '国际标准书号', `name` VARCHAR(100) NOT NULL COMMENT '书名', `author` VARCHAR(50) NOT NULL COMMENT '作者', `publisher` VARCHAR(50) COMMENT '出版社', `publish_date` DATE COMMENT '出版日期', `price` DECIMAL(10, 2) COMMENT '价格', `category` VARCHAR(30) COMMENT '分类', `stock` INT DEFAULT 0 COMMENT '库存数量(总册数)', `available_stock` INT DEFAULT 0 COMMENT '可借阅数量', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';

设计要点

  • isbn设为唯一索引,这是图书的唯一标识,避免重复录入。
  • 将库存拆分为stock(总册数)和available_stock(可借数)。当一本书被借出时,available_stock减1,归还时加1。stock只有在购入新书或旧书报废时才改变。这种设计便于快速查询可借状态,且逻辑清晰。
  • category字段预留,为后续按分类检索做准备。

2. 读者表reader

CREATE TABLE `reader` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `card_number` VARCHAR(20) UNIQUE NOT NULL COMMENT '借书证号', `name` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL COMMENT '加密后的密码', `gender` CHAR(1) COMMENT '性别:M/F', `phone` VARCHAR(20), `max_borrow_limit` INT DEFAULT 5 COMMENT '最大借阅册数', `status` TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-冻结', `register_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者信息表';

设计要点

  • password字段长度要足够,以便存储经过哈希加密(如BCrypt)后的密码字符串,切勿明文存储。
  • max_borrow_limit实现了业务规则,方便后续调整不同读者的借阅权限。
  • status字段用于账户状态管理,如冻结违规读者。

3. 借阅记录表borrow_record

CREATE TABLE `borrow_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `book_id` INT NOT NULL COMMENT '图书ID', `reader_id` INT NOT NULL COMMENT '读者ID', `borrow_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', `due_time` DATETIME NOT NULL COMMENT '应还时间', `actual_return_time` DATETIME COMMENT '实际归还时间', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-借出未还,2-已归还,3-逾期未还,4-丢失', `fine_amount` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '罚款金额', FOREIGN KEY (`book_id`) REFERENCES `book`(`id`), FOREIGN KEY (`reader_id`) REFERENCES `reader`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';

设计要点

  • 这是系统的核心事务表。通过book_idreader_id关联图书和读者。
  • due_time(应还时间)是关键,用于计算是否逾期。通常根据借阅规则(如30天)在借书时自动计算填入。
  • status字段跟踪借阅生命周期。actual_return_time为空表示未还。
  • 建立了外键约束,保证了数据的一致性:不能借不存在的书,也不能为不存在的读者创建记录。

3.2 数据库连接工具类封装

直接在每个方法里打开和关闭ConnectionStatementResultSet会导致代码冗余且容易忘记关闭,引发资源泄漏。我们必须封装一个工具类。

// DBUtil.java import java.io.InputStream; import java.sql.*; 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("jdbc.url"); user = props.getProperty("jdbc.user"); password = props.getProperty("jdbc.password"); // 加载驱动(MySQL 6.0+ 可以省略,但写上更稳妥) Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new RuntimeException("初始化数据库连接池失败", e); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 关闭资源(重载方法,应对不同场景) public static void close(Connection conn, Statement stmt, ResultSet rs) { close(rs); close(stmt); close(conn); } public static void close(Connection conn, Statement stmt) { close(stmt); close(conn); } private static void close(AutoCloseable ac) { if (ac != null) { try { ac.close(); } catch (Exception e) { // 日志记录,生产环境不要直接打印 e.printStackTrace(); } } } }

对应的db.properties配置文件:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.user=root jdbc.password=your_password

实操心得:这里我强烈建议使用try-with-resources语法(Java 7+),它可以自动关闭实现了AutoCloseable接口的资源,代码更简洁安全。但在Dao层的方法内部,我们仍然需要调用这个close方法,因为Connection等资源是在方法内部获取的,需要在方法结束前关闭。另外,serverTimezone参数对于避免时区导致的日期时间问题至关重要。

4. 核心业务逻辑层实现详解

三层架构中,业务逻辑层是体现系统“智慧”的地方。它调用数据访问层获取数据,并按照业务规则进行处理。

4.1 图书管理服务实现

我们以BookService为例,看看如何将数据访问和业务规则结合。

// BookService.java public class BookService { private BookDao bookDao = new BookDao(); // 新增图书 public boolean addBook(Book book) { // 业务校验:ISBN不能重复 if (bookDao.findByIsbn(book.getIsbn()) != null) { System.out.println("错误:ISBN已存在!"); return false; } // 库存逻辑:新书入库,总库存和可借库存同时增加 book.setAvailableStock(book.getStock()); return bookDao.insert(book) > 0; } // 借出一本书 public boolean borrowBook(int bookId, int readerId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查图书是否存在且可借 Book book = bookDao.findById(bookId, conn); if (book == null || book.getAvailableStock() <= 0) { System.out.println("图书不存在或已无库存可借"); return false; } // 2. 检查读者状态和借阅上限(这里需要ReaderService,暂简化) // ... // 3. 更新图书可借库存 book.setAvailableStock(book.getAvailableStock() - 1); if (bookDao.update(book, conn) <= 0) { conn.rollback(); return false; } // 4. 创建借阅记录(需要BorrowRecordDao) BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setDueTime(LocalDateTime.now().plusDays(30)); // 假设借期30天 // borrowRecordDao.insert(record, conn); conn.commit(); // 提交事务 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } } // 多条件查询图书 public List<Book> searchBooks(String name, String author, String category) { // 构建动态SQL查询条件 StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (name != null && !name.trim().isEmpty()) { sql.append(" AND name LIKE ? "); params.add("%" + name + "%"); } if (author != null && !author.trim().isEmpty()) { sql.append(" AND author LIKE ? "); params.add("%" + author + "%"); } if (category != null && !category.trim().isEmpty()) { sql.append(" AND category = ? "); params.add(category); } return bookDao.queryByCondition(sql.toString(), params.toArray()); } }

关键点解析

  1. 事务管理borrowBook方法是一个典型的事务场景。借书涉及更新图书库存和插入借阅记录,这两个操作必须同时成功或失败。我们通过conn.setAutoCommit(false)conn.commit()conn.rollback()手动管理事务。这是保证数据一致性的核心。
  2. 业务校验:在addBook中,我们先检查ISBN是否重复,这是业务规则,不属于数据库唯一约束的简单替代,因为我们需要给用户友好的提示。
  3. 动态查询searchBooks展示了如何根据前端传入的不定条件构建SQL。使用WHERE 1=1是一个小技巧,可以方便地拼接AND条件,避免判断第一个条件时是否要加WHERE的麻烦。

4.2 数据访问层模式:DAO与POJO

业务逻辑层操作的是BookReader这样的Java对象,而数据访问层负责将这些对象与数据库表映射。我们采用经典的DAO模式

首先定义实体类(POJO):

// Book.java public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private LocalDate publishDate; // 使用Java 8的日期API private BigDecimal price; private String category; private Integer stock; private Integer availableStock; private LocalDateTime createTime; // 省略getter/setter和toString方法 }

然后是对应的BookDao

// BookDao.java public class BookDao { // 插入数据 public int insert(Book book) { String sql = "INSERT INTO book(isbn, name, author, publisher, publish_date, price, category, stock, available_stock) VALUES(?,?,?,?,?,?,?,?,?)"; // 使用try-with-resources,简化资源管理 try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, book.getIsbn()); pstmt.setString(2, book.getName()); // ... 设置其他参数 pstmt.setInt(8, book.getStock()); pstmt.setInt(9, book.getAvailableStock()); int affectedRows = pstmt.executeUpdate(); // 获取自增主键 if (affectedRows > 0) { try (ResultSet rs = pstmt.getGeneratedKeys()) { if (rs.next()) { book.setId(rs.getInt(1)); } } } return affectedRows; } catch (SQLException e) { e.printStackTrace(); return 0; } } // 根据ID查询 public Book findById(int id, Connection conn) throws SQLException { String sql = "SELECT * FROM book WHERE id = ?"; // 注意:这里传入的conn由Service层的事务管理,不能在此关闭 PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { return convertResultSetToBook(rs); } DBUtil.close(null, pstmt, rs); // 只关闭Statement和ResultSet return null; } // 将ResultSet转换为Book对象 private Book convertResultSetToBook(ResultSet rs) throws SQLException { Book book = new Book(); book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); // ... 设置其他属性 // 处理可能为NULL的日期字段 Date sqlDate = rs.getDate("publish_date"); book.setPublishDate(sqlDate != null ? sqlDate.toLocalDate() : null); Timestamp timestamp = rs.getTimestamp("create_time"); book.setCreateTime(timestamp != null ? timestamp.toLocalDateTime() : null); return book; } // 条件查询 public List<Book> queryByCondition(String sql, Object... params) { List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); } ResultSet rs = pstmt.executeQuery(); while (rs.next()) { list.add(convertResultSetToBook(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; } }

踩坑记录findById方法有一个重载版本接收外部传入的Connection。这是因为在事务方法中(如borrowBook),多个数据库操作必须使用同一个连接,否则它们就不在同一个事务里了。这是新手极易忽略的一点。同时,对于这类方法,资源(Connection)的关闭责任在调用方(Service层),Dao层只负责关闭自己创建的StatementResultSet

5. 控制台表示层与用户交互

由于没有图形界面,我们需要设计清晰的菜单和交互流程来驱动整个系统。

// LibraryConsoleApp.java import java.util.Scanner; public class LibraryConsoleApp { private static Scanner scanner = new Scanner(System.in); private static BookService bookService = new BookService(); private static ReaderService readerService = new ReaderService(); // 假设当前登录的用户 private static Reader currentReader = null; private static Admin currentAdmin = null; public static void main(String[] args) { while (true) { if (currentReader == null && currentAdmin == null) { showMainMenu(); } else if (currentReader != null) { showReaderMenu(); } else { showAdminMenu(); } } } private static void showMainMenu() { System.out.println("\n===== 图书管理系统 ====="); System.out.println("1. 读者登录"); System.out.println("2. 读者注册"); System.out.println("3. 管理员登录"); System.out.println("0. 退出系统"); System.out.print("请选择: "); int choice = scanner.nextInt(); scanner.nextLine(); // 消耗换行符 switch (choice) { case 1: readerLogin(); break; case 2: readerRegister(); break; case 3: adminLogin(); break; case 0: System.out.println("感谢使用,再见!"); System.exit(0); default: System.out.println("输入无效,请重新选择。"); } } private static void showReaderMenu() { System.out.println("\n===== 读者功能菜单 ====="); System.out.println("欢迎," + currentReader.getName()); 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.print("请选择: "); // ... 处理读者功能选择 } private static void readerLogin() { System.out.print("请输入借书证号: "); String cardNumber = scanner.nextLine(); System.out.print("请输入密码: "); String password = scanner.nextLine(); // 实际应加密后比对 Reader reader = readerService.login(cardNumber, password); if (reader != null && reader.getStatus() == 1) { currentReader = reader; System.out.println("登录成功!"); } else { System.out.println("登录失败,证号或密码错误,或账户已被冻结。"); } } private static void searchBooks() { System.out.println("\n--- 图书查询 ---"); System.out.print("请输入书名(直接回车跳过): "); String name = scanner.nextLine(); System.out.print("请输入作者(直接回车跳过): "); String author = scanner.nextLine(); System.out.print("请输入分类(直接回车跳过): "); String category = scanner.nextLine(); List<Book> books = bookService.searchBooks( name.isEmpty() ? null : name, author.isEmpty() ? null : author, category.isEmpty() ? null : category ); if (books.isEmpty()) { System.out.println("未找到相关图书。"); } else { System.out.printf("%-5s %-15s %-30s %-15s %-10s %-10s\n", "ID", "ISBN", "书名", "作者", "库存", "可借"); for (Book b : books) { System.out.printf("%-5d %-15s %-30s %-15s %-10d %-10d\n", b.getId(), b.getIsbn(), b.getName(), b.getAuthor(), b.getStock(), b.getAvailableStock()); } } } // ... 其他菜单方法 }

交互设计要点

  1. 状态管理:通过currentReadercurrentAdmin两个变量来记录登录状态,从而动态显示不同的菜单。
  2. 输入处理:使用Scanner时,混合使用nextInt()nextLine()要格外小心。nextInt()只读取数字,不读取行尾的换行符,紧接着的nextLine()会立刻读到空行。所以在nextInt()后通常需要跟一个nextLine()来“消耗”掉这个换行符。
  3. 用户友好:查询功能允许用户跳过某些条件,这通过判断输入是否为空字符串来实现,并传递给Service层处理为null

6. 项目进阶与优化思考

一个基础版本完成后,我们可以从多个维度对其进行扩展和优化,这能极大提升项目的技术深度。

6.1 引入密码加密存储

明文存储密码是严重的安全漏洞。我们必须对密码进行哈希加密。

// 使用BCrypt加密(需要引入org.mindrot:jbcrypt依赖) import org.mindrot.jbcrypt.BCrypt; public class PasswordUtil { // 生成加密后的密码 public static String hashPassword(String plainPassword) { // gensalt()会自动生成盐,并混入哈希结果中 return BCrypt.hashpw(plainPassword, BCrypt.gensalt()); } // 验证密码 public static boolean checkPassword(String plainPassword, String hashedPassword) { return BCrypt.checkpw(plainPassword, hashedPassword); } }

在用户注册时,调用hashPassword存储密文;在登录验证时,调用checkPassword进行比对。BCrypt是当前公认的安全的密码哈希算法,它自动加盐并可以调整计算成本以对抗暴力破解。

6.2 实现分页查询

当图书数据量很大时,一次性查询所有数据效率低下且体验不好。需要在Dao层实现分页。

// 在BookDao中添加分页查询方法 public List<Book> queryByConditionWithPage(String baseSql, List<Object> params, int pageNum, int pageSize) { List<Book> list = new ArrayList<>(); String sql = baseSql + " LIMIT ?, ?"; // 计算偏移量 int offset = (pageNum - 1) * pageSize; params.add(offset); params.add(pageSize); try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { for (int i = 0; i < params.size(); i++) { pstmt.setObject(i + 1, params.get(i)); } ResultSet rs = pstmt.executeQuery(); while (rs.next()) { list.add(convertResultSetToBook(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; }

同时,还需要一个查询总记录数的方法来计算总页数。Service层将接收pageNumpageSize参数,并返回一个包含List<Book>和总页数信息的PageResult对象。

6.3 日志记录与异常处理

使用try-catch打印e.printStackTrace()仅适用于调试。生产环境需要引入日志框架,如SLF4J + Logback。

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BookService { private static final Logger LOGGER = LoggerFactory.getLogger(BookService.class); private BookDao bookDao = new BookDao(); public boolean addBook(Book book) { try { if (bookDao.findByIsbn(book.getIsbn()) != null) { LOGGER.warn("尝试添加重复ISBN的图书: {}", book.getIsbn()); return false; } boolean success = bookDao.insert(book) > 0; if (success) { LOGGER.info("成功添加图书: {} (ISBN: {})", book.getName(), book.getIsbn()); } return success; } catch (Exception e) { LOGGER.error("添加图书时发生异常,图书信息: {}", book, e); return false; } } }

配置好logback.xml后,日志可以输出到控制台和文件,并区分INFOWARNERROR等级别,便于问题追踪和系统监控。

6.4 从控制台到Web的迁移

如果你想将这个系统升级为B/S架构,技术栈可以平滑演进:

  1. 后端:引入Spring Boot框架。将现有的BookServiceBookDao等类转换为Spring的@Service@Repository组件。利用Spring的依赖注入和事务管理(@Transactional)替代手写的事务代码,更加简洁优雅。
  2. 数据访问:引入MyBatisSpring Data JPA。它们能极大简化JDBC的模板代码。你只需要定义Mapper接口或JpaRepository接口,并编写XML映射文件或使用注解,框架会自动生成实现。但务必理解其底层仍是JDBC。
  3. 前端:可以使用简单的JSP、Thymeleaf模板,或者前后端分离,用Vue/React等框架构建独立前端项目,通过RESTful API与后端交互。
  4. 接口设计:为BookController设计RESTful接口,如:
    • GET /api/books查询图书列表
    • POST /api/books新增图书
    • PUT /api/books/{id}更新图书
    • DELETE /api/books/{id}删除图书

这个迁移过程,正是从一个“课程设计”项目迈向“企业级”应用的关键一步。

7. 常见问题与调试技巧实录

在开发过程中,我遇到了不少典型问题,这里总结出来,希望能帮你绕过这些坑。

问题1:数据库连接失败,报错“Public Key Retrieval is not allowed”或时区错误。

  • 排查:这是MySQL Connector/J的常见问题。检查JDBC连接URL。
  • 解决:在db.properties的url后添加参数。对于时区问题加&serverTimezone=Asia/Shanghai;对于新版本MySQL身份验证问题,可能需要加&allowPublicKeyRetrieval=true(注意安全风险)或更新驱动版本。

问题2:执行插入操作后,获取不到自增主键ID。

  • 排查:检查PreparedStatement的创建方式。
  • 解决:创建Statement时,需要传入Statement.RETURN_GENERATED_KEYS标志。
    PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); int affectedRows = pstmt.executeUpdate(); if (affectedRows > 0) { try (ResultSet rs = pstmt.getGeneratedKeys()) { if (rs.next()) { int id = rs.getInt(1); // 获取生成的主键 book.setId(id); } } }

问题3:查询结果中,日期时间字段显示不正确或为null。

  • 排查:Java对象中使用的是LocalDateTime,而数据库是DATETIMETIMESTAMPResultSetgetTimestamp方法可以正确转换。
  • 解决:在convertResultSetToBook方法中,使用rs.getTimestamp(“create_time”),并判断是否为null后再转换为LocalDateTime。同时确保数据库连接URL中设置了正确的服务器时区。

问题4:事务不起作用,部分操作失败后数据没有回滚。

  • 排查
    1. 检查数据库引擎是否为InnoDB(支持事务),MyISAM不支持。
    2. 检查Service层多个Dao操作是否使用的是同一个Connection对象。
    3. 检查是否在操作前正确设置了conn.setAutoCommit(false),并在最后执行了conn.commit()
    4. 检查是否在catch块中执行了conn.rollback()
  • 解决:确保满足以上所有条件。一个可靠的做法是定义一个“事务模板”方法,或者直接使用Spring的@Transactional注解。

问题5:控制台菜单循环打印错乱,或输入后没反应。

  • 排查:通常是Scanner的用法问题。nextInt()nextDouble()等不会读取分隔符(如换行符)。
  • 解决:在读取数字后,多加一行scanner.nextLine()来消耗掉输入缓冲区中的换行符。或者,统一使用nextLine()读取所有输入,然后对于需要数字的地方,使用Integer.parseInt()进行转换,这样更可控。

这个图书管理系统项目,就像一把钥匙,帮你打开了Java企业应用开发的大门。从JDBC到三层架构,从事务管理到异常处理,每一个环节都是后端开发的基本功。我建议你不要止步于此,尝试着去实现上面提到的每一个进阶功能,甚至自己增加一些新特性,比如邮件提醒逾期、图书热度排行榜等。真正的成长,就藏在把这些想法一行行变成代码的过程里。完整的项目代码我已经整理好,你可以根据这个思路去搭建和调试,遇到问题,欢迎随时交流。

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

MAF快速入门(12)主工作流+子工作流

目录 简介 1 子工作流模式介绍 2 主工作流子工作流实验案例 2.1 关键依赖包引入 2.2 定义数据传输模型 2.3 定义产品质量处理子工作流 2.4 定义物流问题处理子工作流 2.5 构建主工作流 2.6 测试工作流 3 小结 4 示例源码 简介 大家好&#xff0c;我是Edison。 上一…

作者头像 李华
网站建设 2026/8/12 20:06:14

CentOS下安装MySQL8.0完整指南

一、安装方式对比 安装方式适用场景优点缺点包管理器安装 (apt/yum/dnf)快速部署、追求稳定性、新手友好自动处理依赖、配置简单、易于升级维护版本可能较旧、定制化程度低手动编译安装需要特定版本、深度定制、学习原理版本可控、性能优化灵活、功能模块可选步骤繁琐、依赖管…

作者头像 李华
网站建设 2026/8/12 20:05:46

HarmonyOS7 颜色 Token 让主题更稳:ArkUI/ArkTS 实战拆解

文章目录前言为什么这个问题经常被写乱场景&#xff1a;品牌页接入深色模式常用颜色语义实操步骤先把页面目标想清楚完整示例&#xff1a;语义颜色驱动品牌页把关键代码一段段拆开容易踩坑的点优化建议颜色 Token 要先分清语义写在最后前言 颜色 token 的重点不是把色值藏起来…

作者头像 李华
网站建设 2026/8/12 20:05:09

桌面端 AI 自动化体验,OpenClaw 从安装到任务测试全流程

&#x1f4a1;OpenClaw 桌面 AI 智能体&#xff5c;Windows/macOS 完整部署与问题排查 适配系统&#xff1a;Windows10/11 64 位、macOS 12 及以上✨ 当前版本&#xff1a;Windows v2.9.3 /macOS v2.7.9 现在很多人都想体验可以直接操作桌面文件、软件的 AI 智能体&#xff0…

作者头像 李华
网站建设 2026/8/12 20:04:25

JavaScript无限循环导致浏览器卡死的调试与解决方案

1. 从一次真实的“浏览器卡死”事故说起 那天下午&#xff0c;我正在为一个复杂的表单验证逻辑编写一个 while 循环。本意是遍历一个动态生成的数组&#xff0c;检查每个元素的状态。我自信满满地写下了 while (true) { ... } &#xff0c;心想里面肯定有 break 条件。结…

作者头像 李华
网站建设 2026/8/12 20:03:49

Apache Doris BitMap去重实战:高基数场景下替代COUNT(DISTINCT)的性能优化方案

1. 从“计数不准”到“精准去重”的实战需求 在数据仓库和实时分析领域&#xff0c;我们经常遇到一个看似简单却暗藏玄机的问题&#xff1a;如何精确地统计一个集合中不重复元素的数量&#xff1f;尤其是在处理海量用户ID、设备ID、订单号等场景时&#xff0c;传统的 COUNT(DI…

作者头像 李华