尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

Java图书管理系统实战:从JDBC到三层架构的完整项目构建 1. 项目概述从零构建一个扎实的Java图书管理系统最近在整理自己的项目仓库翻出了这个几年前写的图书管理系统。它不是什么惊世骇俗的架构也没有用到当下最时髦的微服务或云原生但恰恰是这样一个“经典”的课程设计级项目涵盖了从需求分析、数据库设计、后端逻辑到前端交互的完整闭环是检验和巩固Java SE及Java Web核心技能的绝佳试金石。很多新手朋友在学完Java基础语法和JDBC后常常会陷入“接下来该做什么”的迷茫这个项目就是一个很好的答案。它模拟了一个小型图书馆的核心业务流程包括图书的增删改查、读者的注册借阅、以及管理员的后台管理。今天我就把这个项目的完整实现思路、关键代码以及我踩过的那些坑毫无保留地分享出来希望能给正在学习Java的你提供一个清晰、可落地的实战参考。2. 系统核心需求与架构设计2.1 业务需求拆解我们到底要做一个什么系统在动手写第一行代码之前我们必须把业务逻辑理清楚。一个最基本的图书管理系统至少要满足三类用户角色的需求读者核心诉求是“找书”和“借书”。他们需要能查询图书信息按书名、作者、分类等查看自己的借阅记录以及进行图书的借阅与归还操作。图书管理员这是系统的核心操作者。他们需要对图书和读者信息进行全面的管理包括图书管理新增图书入库、下架旧书、更新图书信息如补货、修正信息。读者管理审核注册、注销读者账户、管理读者信息。借阅管理处理所有借阅和归还请求处理逾期、丢失等异常情况。系统管理员权限更高负责管理“图书管理员”账户进行系统层面的配置与数据维护。基于以上我们可以抽象出几个核心的业务实体Book图书、Reader读者、BorrowRecord借阅记录、Admin管理员。它们之间的关系是一个Reader可以有多条BorrowRecord一条BorrowRecord对应一本Book和一个Reader。Admin独立于借阅流程之外负责用户和图书的元数据管理。2.2 技术栈选型与架构模式为什么是这些技术确定了做什么接下来就要决定怎么做。技术选型直接决定了开发效率和项目的可维护性。后端语言Java。这是项目的根基。我们主要使用Java SE的标准库以及用于数据库连接的JDBC。选择Java是因为其严谨的面向对象特性、强大的生态和在企业级应用中的普遍性非常适合用来理解业务逻辑的封装与分层。数据库MySQL。关系型数据库是这类管理系统的标准选择它能清晰地表达图书、读者、借阅记录之间的关联关系。相比SQLiteMySQL更贴近生产环境相比NoSQL它的结构化查询对于复杂的统计报表如借阅排行榜、逾期分析更友好。数据访问层原生JDBC。虽然MyBatis、Hibernate等ORM框架能极大简化开发但在学习阶段我强烈建议从原生JDBC开始。亲手编写SQL语句、处理ResultSet、管理连接池能让你真正理解Java程序是如何与数据库对话的这是后续学习任何框架的坚实基础。本项目将演示如何封装一个简单的DBUtil工具类来管理数据库连接和关闭资源这是避免内存泄漏的关键。架构模式三层架构。这是经典且有效的分层方式。表示层负责与用户交互。为了聚焦后端逻辑本项目采用控制台界面实现。通过Scanner获取用户输入打印菜单和结果。这能让我们抛开复杂的前端技术专注于业务逻辑和数据处理。当然你可以很容易地将其替换为Swing GUI、JavaFX或一个Spring Boot Web接口。业务逻辑层系统的“大脑”。包含BookService、ReaderService、BorrowService等类负责处理具体的业务规则例如“借书前检查读者是否已达借阅上限”、“还书时计算是否逾期并产生罚款”。数据访问层系统的“手和脚”。包含BookDao、ReaderDao等类封装所有对数据库的增删改查操作为业务逻辑层提供纯净的数据服务。注意很多教学项目会把所有代码都写在main方法里这是极其不利于理解和维护的。严格的三层架构分离是迈向“工程化”开发的第一步。3. 数据库设计与实现一切的基础3.1 数据表结构设计数据库设计是项目的基石设计得好后续编码事半功倍。以下是核心表结构1. 图书表bookCREATE 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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书信息表;设计要点isbn设为唯一索引这是图书的唯一标识避免重复录入。将库存拆分为stock总册数和available_stock可借数。当一本书被借出时available_stock减1归还时加1。stock只有在购入新书或旧书报废时才改变。这种设计便于快速查询可借状态且逻辑清晰。category字段预留为后续按分类检索做准备。2. 读者表readerCREATE 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者信息表;设计要点password字段长度要足够以便存储经过哈希加密如BCrypt后的密码字符串切勿明文存储。max_borrow_limit实现了业务规则方便后续调整不同读者的借阅权限。status字段用于账户状态管理如冻结违规读者。3. 借阅记录表borrow_recordCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;设计要点这是系统的核心事务表。通过book_id和reader_id关联图书和读者。due_time应还时间是关键用于计算是否逾期。通常根据借阅规则如30天在借书时自动计算填入。status字段跟踪借阅生命周期。actual_return_time为空表示未还。建立了外键约束保证了数据的一致性不能借不存在的书也不能为不存在的读者创建记录。3.2 数据库连接工具类封装直接在每个方法里打开和关闭Connection、Statement、ResultSet会导致代码冗余且容易忘记关闭引发资源泄漏。我们必须封装一个工具类。// 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.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.userroot jdbc.passwordyour_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 ListBook searchBooks(String name, String author, String category) { // 构建动态SQL查询条件 StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11 ); ListObject 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()); } }关键点解析事务管理borrowBook方法是一个典型的事务场景。借书涉及更新图书库存和插入借阅记录这两个操作必须同时成功或失败。我们通过conn.setAutoCommit(false)、conn.commit()和conn.rollback()手动管理事务。这是保证数据一致性的核心。业务校验在addBook中我们先检查ISBN是否重复这是业务规则不属于数据库唯一约束的简单替代因为我们需要给用户友好的提示。动态查询searchBooks展示了如何根据前端传入的不定条件构建SQL。使用WHERE 11是一个小技巧可以方便地拼接AND条件避免判断第一个条件时是否要加WHERE的麻烦。4.2 数据访问层模式DAO与POJO业务逻辑层操作的是Book、Reader这样的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 ListBook queryByCondition(String sql, Object... params) { ListBook 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层只负责关闭自己创建的Statement和ResultSet。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(); ListBook 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()); } } } // ... 其他菜单方法 }交互设计要点状态管理通过currentReader和currentAdmin两个变量来记录登录状态从而动态显示不同的菜单。输入处理使用Scanner时混合使用nextInt()和nextLine()要格外小心。nextInt()只读取数字不读取行尾的换行符紧接着的nextLine()会立刻读到空行。所以在nextInt()后通常需要跟一个nextLine()来“消耗”掉这个换行符。用户友好查询功能允许用户跳过某些条件这通过判断输入是否为空字符串来实现并传递给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 ListBook queryByConditionWithPage(String baseSql, ListObject params, int pageNum, int pageSize) { ListBook 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层将接收pageNum和pageSize参数并返回一个包含ListBook和总页数信息的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后日志可以输出到控制台和文件并区分INFO、WARN、ERROR等级别便于问题追踪和系统监控。6.4 从控制台到Web的迁移如果你想将这个系统升级为B/S架构技术栈可以平滑演进后端引入Spring Boot框架。将现有的BookService、BookDao等类转换为Spring的Service、Repository组件。利用Spring的依赖注入和事务管理Transactional替代手写的事务代码更加简洁优雅。数据访问引入MyBatis或Spring Data JPA。它们能极大简化JDBC的模板代码。你只需要定义Mapper接口或JpaRepository接口并编写XML映射文件或使用注解框架会自动生成实现。但务必理解其底层仍是JDBC。前端可以使用简单的JSP、Thymeleaf模板或者前后端分离用Vue/React等框架构建独立前端项目通过RESTful API与后端交互。接口设计为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后添加参数。对于时区问题加serverTimezoneAsia/Shanghai对于新版本MySQL身份验证问题可能需要加allowPublicKeyRetrievaltrue注意安全风险或更新驱动版本。问题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而数据库是DATETIME或TIMESTAMP。ResultSet的getTimestamp方法可以正确转换。解决在convertResultSetToBook方法中使用rs.getTimestamp(“create_time”)并判断是否为null后再转换为LocalDateTime。同时确保数据库连接URL中设置了正确的服务器时区。问题4事务不起作用部分操作失败后数据没有回滚。排查检查数据库引擎是否为InnoDB支持事务MyISAM不支持。检查Service层多个Dao操作是否使用的是同一个Connection对象。检查是否在操作前正确设置了conn.setAutoCommit(false)并在最后执行了conn.commit()。检查是否在catch块中执行了conn.rollback()。解决确保满足以上所有条件。一个可靠的做法是定义一个“事务模板”方法或者直接使用Spring的Transactional注解。问题5控制台菜单循环打印错乱或输入后没反应。排查通常是Scanner的用法问题。nextInt()、nextDouble()等不会读取分隔符如换行符。解决在读取数字后多加一行scanner.nextLine()来消耗掉输入缓冲区中的换行符。或者统一使用nextLine()读取所有输入然后对于需要数字的地方使用Integer.parseInt()进行转换这样更可控。这个图书管理系统项目就像一把钥匙帮你打开了Java企业应用开发的大门。从JDBC到三层架构从事务管理到异常处理每一个环节都是后端开发的基本功。我建议你不要止步于此尝试着去实现上面提到的每一个进阶功能甚至自己增加一些新特性比如邮件提醒逾期、图书热度排行榜等。真正的成长就藏在把这些想法一行行变成代码的过程里。完整的项目代码我已经整理好你可以根据这个思路去搭建和调试遇到问题欢迎随时交流。
返回列表