简介:本资源是一套完整的基于Java Swing与MySQL开发的图书管理系统课程设计/毕业设计项目,面向计算机相关专业本科生及Java初学者,解决传统图书管理中人工操作效率低、信息检索不便、借还流程繁琐等实际问题。项目采用Swing构建图形化界面,实现用户注册登录、图书模糊搜索、借阅归还、后台数据管理(含图书、类别、用户、借阅记录)等核心功能,兼顾交互友好性与系统稳定性。压缩包共116个文件,包含20个核心Java源码、82个编译后Class文件(如LoginFrm、AdminMenuFrm、UserMenuFrm等典型窗体类)、9个XML配置文件(用于界面布局或数据映射)、4个依赖JAR包及1个IDEA工程配置文件,整体大小为2.94MB,结构清晰,模块划分明确,便于理解MVC分层逻辑与Swing事件驱动机制。目前已有330人学习下载,提供可直接运行的完整工程,涵盖从数据库建表脚本、GUI组件封装到业务逻辑实现的全流程代码,是掌握Java桌面应用开发与数据库集成实践的优质参考范例。
1. 项目概述与核心价值
最近在整理过去的项目代码,翻出来一个用Java Swing写的图书管理系统。这玩意儿虽然界面看起来有点“复古”,但麻雀虽小,五脏俱全,从图书的增删改查到借阅归还,再到用户管理,功能链是完整的。很多朋友在学Java桌面开发或者做课程设计时,都会选择这个方向,因为它能很好地串联起面向对象、数据库操作、事件驱动编程这些核心知识点。今天,我就把这个项目的里里外外拆解一遍,从设计思路到代码实现,再到那些年我踩过的坑,都分享出来。无论你是刚学完Java基础想找个项目练手,还是正在为课程设计发愁,这篇文章应该都能给你提供一条清晰的实现路径和不少实用的避坑指南。
这个系统本质上是一个典型的管理信息系统,核心目标就是把图书馆日常的手工操作数字化。我们得管理两类核心实体:书和人。书有它的基本信息,比如ISBN、书名、作者、出版社、库存数量;人主要指读者,有学号、姓名、联系方式等。然后,书和人之间通过“借阅”这个行为产生关联,这就引出了借阅记录,需要记录谁在什么时候借了哪本书,什么时候该还,是否超期,有没有罚款。把这些逻辑用代码实现,封装成一个带图形界面的应用,就是一个完整的图书管理系统了。选择Java Swing来实现,一是因为它纯Java,不依赖外部复杂环境,二是它能让你深刻理解事件监听、界面布局这些GUI编程的核心概念,这对理解现代前端框架的某些思想也有帮助。
2. 系统整体设计与架构思路
2.1 技术选型与架构模式
为什么用Swing?现在做桌面应用,Electron、JavaFX甚至Qt可能更流行。但对于一个以学习和演示为核心目的的项目,Swing有它的独特优势。首先,它是Java标准库的一部分,无需引入任何第三方依赖,环境搭建极其简单。其次,Swing的MVC思想虽然不如现代框架那么清晰,但其组件模型和事件监听机制是理解GUI编程的绝佳教材。最后,这个项目的数据量通常不会太大,Swing的性能完全足够。在架构上,我强烈推荐采用分层架构,哪怕项目不大,养成好习惯很重要。通常分为三层:表现层、业务逻辑层、数据访问层。
表现层就是Swing的那些窗口和组件。业务逻辑层负责处理核心业务规则,比如“借书时检查库存是否大于零”、“还书时计算是否超期”。数据访问层则负责和数据库打交道,执行增删改查。这里有一个关键决策:数据持久化方案。对于课程设计或小型应用,我建议直接用SQLite。它是一个文件数据库,无需安装独立的数据库服务,一个jar包引入项目,数据库就是一个.db文件,非常便于项目打包和移植。如果为了更贴近企业开发,也可以用MySQL。在代码组织上,对应每一层建立不同的包,例如com.library.view,com.library.service,com.library.dao,结构会非常清晰。
2.2 数据库表结构设计
数据库设计是整个系统的基石,设计得好,后面编码事半功倍。我们至少需要四张核心表:
- 图书表:存储图书的基本信息。
- 读者表:存储读者信息。
- 借阅记录表:核心中的核心,记录借阅行为。
- 管理员表:用于系统登录验证。
下面是一个简化的SQLite建表语句示例,我加上了字段说明和设计考量:
-- 图书表 CREATE TABLE `book` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键,自增 `isbn` VARCHAR(20) UNIQUE NOT NULL, -- ISBN号,唯一,用于精确检索 `name` VARCHAR(100) NOT NULL, -- 书名 `author` VARCHAR(50) NOT NULL, -- 作者 `publisher` VARCHAR(50), -- 出版社 `price` DECIMAL(10, 2), -- 价格,用十进制类型存储金额 `total_count` INTEGER NOT NULL DEFAULT 0, -- 总数量 `current_count` INTEGER NOT NULL DEFAULT 0, -- 当前可借数量 CONSTRAINT `chk_count` CHECK (current_count <= total_count AND current_count >= 0) -- 检查约束,确保库存逻辑正确 ); -- 读者表 CREATE TABLE `reader` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `reader_id` VARCHAR(20) UNIQUE NOT NULL, -- 读者证号/学号 `name` VARCHAR(50) NOT NULL, `gender` VARCHAR(2) CHECK(gender IN ('男', '女')), -- 使用检查约束限定性别输入 `phone` VARCHAR(15), `register_date` DATE DEFAULT (DATE('now')) -- 注册日期,默认为当前日期 ); -- 借阅记录表 (核心) CREATE TABLE `borrow_record` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `book_id` INTEGER NOT NULL, -- 外键,关联图书ID `reader_id` INTEGER NOT NULL, -- 外键,关联读者ID `borrow_date` DATE NOT NULL DEFAULT (DATE('now')), -- 借阅日期 `due_date` DATE NOT NULL, -- 应还日期 `return_date` DATE, -- 实际归还日期,为空表示未还 `status` VARCHAR(10) DEFAULT '借出' CHECK(status IN ('借出', '已还', '超期')), -- 状态标识 `fine` DECIMAL(10, 2) DEFAULT 0.00, -- 罚款金额 FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) ON DELETE CASCADE, -- 设置外键级联删除 FOREIGN KEY (`reader_id`) REFERENCES `reader`(`id`) ON DELETE CASCADE ); -- 管理员表 CREATE TABLE `admin` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(50) UNIQUE NOT NULL, `password` VARCHAR(100) NOT NULL -- 注意:实际存储应为加密后的密文 );设计心得:
borrow_record表的设计是精髓。return_date为空表示书还未还。status字段是一个冗余设计,但它能极大提升查询效率,比如直接查status='超期'的记录,而不用每次都计算日期。due_date(应还日期)应该在借书时由程序根据规则(如借期30天)自动计算并填入,而不是手动输入。
2.3 项目工程结构与依赖管理
对于这样一个项目,我推荐使用Maven进行依赖管理,即使你最终打包成可执行JAR,Maven也能让库的管理变得轻松。核心的依赖就是SQLite的JDBC驱动。你的pom.xml文件会非常简单:
<dependencies> <!-- SQLite JDBC 驱动 --> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.42.0.0</version> </dependency> </dependencies>项目目录结构应该清晰反映分层架构:
src/main/java/com/yourcompany/library/ ├── Main.java // 程序入口 ├── config/ │ └── DatabaseConfig.java // 数据库连接配置 ├── dao/ // 数据访问层 │ ├── BaseDao.java // 封装通用CRUD操作 │ ├── BookDao.java │ ├── ReaderDao.java │ ├── BorrowRecordDao.java │ └── AdminDao.java ├── service/ // 业务逻辑层 │ ├── BookService.java │ ├── ReaderService.java │ ├── BorrowService.java │ └── AuthService.java // 认证服务 ├── model/ // 实体类,与数据库表对应 │ ├── Book.java │ ├── Reader.java │ ├── BorrowRecord.java │ └── Admin.java └── view/ // 表现层,Swing界面 ├── LoginFrame.java // 登录窗口 ├── MainFrame.java // 主窗口 ├── panel/ // 主窗口内的各个功能面板 │ ├── BookManagePanel.java │ ├── ReaderManagePanel.java │ ├── BorrowReturnPanel.java │ └── DashboardPanel.java └── component/ // 自定义的可复用组件 └── BookTableModel.java // 自定义表格模型3. 核心模块实现与代码详解
3.1 数据库连接与通用DAO封装
第一步是建立可靠的数据库连接。我习惯用一个单独的配置类来管理:
// DatabaseConfig.java public class DatabaseConfig { private static final String DB_URL = "jdbc:sqlite:library.db"; private static Connection connection = null; public static Connection getConnection() { if (connection == null) { try { Class.forName("org.sqlite.JDBC"); connection = DriverManager.getConnection(DB_URL); // 启用外键约束(SQLite默认关闭) connection.createStatement().execute("PRAGMA foreign_keys = ON;"); System.out.println("数据库连接成功。"); } catch (Exception e) { JOptionPane.showMessageDialog(null, "数据库连接失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } return connection; } public static void closeConnection() { if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } }注意:这里用静态变量保存
Connection是一个简化做法,适用于单线程的桌面应用。在Web或多线程环境下,必须使用连接池。另外,PRAGMA foreign_keys = ON;这句对于SQLite的外键约束生效至关重要,务必加上。
有了连接,接下来封装一个BaseDao,把重复的JDBC操作(如获取连接、释放资源)抽离出来,让具体的XxxDao只关心SQL和参数。这是减少代码冗余、提高可维护性的关键。
// BaseDao.java public abstract class BaseDao { protected Connection conn = DatabaseConfig.getConnection(); // 通用的更新操作(增、删、改) protected int update(String sql, Object... params) { PreparedStatement pstmt = null; try { pstmt = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); } return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } finally { // 释放资源,注意PreparedStatement需要关闭,Connection由配置类统一管理 try { if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } } // 通用的查询操作(返回单个对象) protected <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... params) { PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); } rs = pstmt.executeQuery(); if (rs.next()) { return rowMapper.mapRow(rs); } return null; } catch (SQLException e) { e.printStackTrace(); return null; } finally { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } } // 通用的查询操作(返回列表) protected <T> List<T> queryForList(String sql, RowMapper<T> rowMapper, Object... params) { List<T> list = new ArrayList<>(); PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); } rs = pstmt.executeQuery(); while (rs.next()) { list.add(rowMapper.mapRow(rs)); } } catch (SQLException e) { e.printStackTrace(); } finally { // 释放资源... } return list; } // 行映射接口,由具体Dao实现 public interface RowMapper<T> { T mapRow(ResultSet rs) throws SQLException; } }3.2 实体类与具体DAO实现
以Book实体和BookDao为例:
// Book.java public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private BigDecimal price; private Integer totalCount; private Integer currentCount; // 省略构造方法、Getter/Setter、toString... }// BookDao.java public class BookDao extends BaseDao { // 新增图书 public int addBook(Book book) { String sql = "INSERT INTO book(isbn, name, author, publisher, price, total_count, current_count) VALUES(?,?,?,?,?,?,?)"; return update(sql, book.getIsbn(), book.getName(), book.getAuthor(), book.getPublisher(), book.getPrice(), book.getTotalCount(), book.getCurrentCount()); } // 根据ID删除图书 public int deleteBookById(Integer id) { String sql = "DELETE FROM book WHERE id=?"; return update(sql, id); } // 更新图书信息 public int updateBook(Book book) { String sql = "UPDATE book SET isbn=?, name=?, author=?, publisher=?, price=?, total_count=?, current_count=? WHERE id=?"; return update(sql, book.getIsbn(), book.getName(), book.getAuthor(), book.getPublisher(), book.getPrice(), book.getTotalCount(), book.getCurrentCount(), book.getId()); } // 根据ID查询图书 public Book findBookById(Integer id) { String sql = "SELECT * FROM book WHERE id=?"; return queryForObject(sql, new BookRowMapper(), id); } // 根据书名模糊查询 public List<Book> findBooksByName(String name) { String sql = "SELECT * FROM book WHERE name LIKE ?"; return queryForList(sql, new BookRowMapper(), "%" + name + "%"); } // 查询所有图书 public List<Book> findAllBooks() { String sql = "SELECT * FROM book"; return queryForList(sql, new BookRowMapper()); } // 内部类,负责将ResultSet的一行映射成Book对象 private static class BookRowMapper implements RowMapper<Book> { @Override public Book mapRow(ResultSet rs) throws SQLException { 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.setTotalCount(rs.getInt("total_count")); book.setCurrentCount(rs.getInt("current_count")); return book; } } }ReaderDao和BorrowRecordDao的实现逻辑类似,但BorrowRecordDao的查询会复杂一些,经常需要联表查询来获取图书名和读者名,而不是仅仅显示ID。
3.3 业务逻辑层:借阅与归还的核心算法
业务逻辑层是体现系统智能的地方。它调用DAO层的方法,并加入业务规则。BorrowService是最核心的服务类。
// BorrowService.java public class BorrowService { private BorrowRecordDao borrowRecordDao = new BorrowRecordDao(); private BookDao bookDao = new BookDao(); private ReaderDao readerDao = new ReaderDao(); // 借书操作 public String borrowBook(Integer bookId, Integer readerId, Integer borrowDays) { // 1. 检查图书是否存在且可借数量>0 Book book = bookDao.findBookById(bookId); if (book == null) { return "图书不存在!"; } if (book.getCurrentCount() <= 0) { return "该图书已全部借出!"; } // 2. 检查读者是否存在 Reader reader = readerDao.findReaderById(readerId); if (reader == null) { return "读者不存在!"; } // 3. 检查该读者是否已借阅此书且未归还(可选规则) List<BorrowRecord> records = borrowRecordDao.findBorrowingRecordsByReaderAndBook(readerId, bookId); if (records != null && !records.isEmpty()) { return "您已借阅此书,请先归还!"; } // 4. 检查读者借书数量是否超限(可选规则) int borrowingCount = borrowRecordDao.countBorrowingBooksByReader(readerId); if (borrowingCount >= 5) { // 假设上限是5本 return "您的借书数量已达上限!"; } // 5. 所有检查通过,执行借阅 // 5.1 创建借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(new Date()); // 借阅日期为当前日期 // 计算应还日期 Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); calendar.add(Calendar.DAY_OF_YEAR, borrowDays); record.setDueDate(calendar.getTime()); record.setStatus("借出"); int result = borrowRecordDao.addRecord(record); if (result <= 0) { return "借阅记录创建失败!"; } // 5.2 更新图书库存(当前可借数量减1) book.setCurrentCount(book.getCurrentCount() - 1); bookDao.updateBook(book); return "借阅成功!应还日期:" + new SimpleDateFormat("yyyy-MM-dd").format(record.getDueDate()); } // 还书操作 public String returnBook(Integer recordId) { // 1. 获取借阅记录 BorrowRecord record = borrowRecordDao.findRecordById(recordId); if (record == null || "已还".equals(record.getStatus())) { return "借阅记录无效或已归还!"; } // 2. 计算是否超期及罚款(假设超期一天罚款0.1元) Date returnDate = new Date(); BigDecimal fine = BigDecimal.ZERO; String status = "已还"; if (returnDate.after(record.getDueDate())) { status = "超期"; long overdueDays = (returnDate.getTime() - record.getDueDate().getTime()) / (1000 * 60 * 60 * 24); fine = new BigDecimal(overdueDays).multiply(new BigDecimal("0.1")); // 在实际系统中,罚款规则可能更复杂,这里只是示例 } // 3. 更新借阅记录 record.setReturnDate(returnDate); record.setStatus(status); record.setFine(fine); int updateResult = borrowRecordDao.updateRecord(record); // 4. 更新图书库存(当前可借数量加1) Book book = bookDao.findBookById(record.getBookId()); if (book != null) { book.setCurrentCount(book.getCurrentCount() + 1); bookDao.updateBook(book); } if (updateResult > 0) { String msg = "归还成功!"; if (fine.compareTo(BigDecimal.ZERO) > 0) { msg += "超期罚款:" + fine.setScale(2, RoundingMode.HALF_UP) + "元。"; } return msg; } else { return "归还失败!"; } } // 查询超期记录 public List<BorrowRecord> findOverdueRecords() { // 这里可以写一个复杂的SQL,直接查询应还日期小于当前日期且未归还的记录 // 或者调用DAO层方法,在内存中过滤。为了性能,建议在DAO层用SQL实现。 String sql = "SELECT * FROM borrow_record WHERE status='借出' AND due_date < DATE('now')"; return borrowRecordDao.queryForList(sql, new BorrowRecordDao.BorrowRecordRowMapper()); } }业务逻辑要点:
- 事务性:注意看,借书操作包含了“插入借阅记录”和“更新图书库存”两个数据库操作。这两个操作必须作为一个整体,要么都成功,要么都失败。上面的代码没有处理事务,这是一个严重的缺陷。在实际项目中,必须使用
Connection的setAutoCommit(false),commit(),rollback()来保证事务。这是面试常考的点,也是项目从“玩具”到“可用”的关键一步。- 并发问题:如果两个管理员同时为同一本最后一本书办理借阅,可能会发生“超借”。这需要通过数据库的锁机制(如
SELECT ... FOR UPDATE)或在应用层使用同步锁来处理。对于课程设计,可以暂时忽略,但要知道这是真实场景下的一个挑战。- 日期计算:使用
java.util.Calendar或Java 8+的java.time.LocalDate来进行日期加减,避免手动计算毫秒数。
3.4 表现层:Swing界面设计与事件处理
Swing界面开发的核心是理解事件驱动模型。用户点击按钮、输入文字、选择菜单,都会触发事件,我们需要编写监听器来处理这些事件。主界面通常采用JFrame,内部使用JTabbedPane来组织不同功能模块。
登录界面是起点,它需要验证用户名和密码。这里涉及一个安全要点:密码不能明文存储和比较。
// AuthService.java (业务逻辑层) public class AuthService { private AdminDao adminDao = new AdminDao(); public boolean login(String username, String password) { Admin admin = adminDao.findAdminByUsername(username); if (admin == null) { return false; } // 比较加密后的密码 String encryptedInput = encryptPassword(password); // 加密函数 return encryptedInput.equals(admin.getPassword()); } private String encryptPassword(String plain) { // 实际应用中应使用BCrypt、PBKDF2等强哈希算法,这里用MD5演示(已不安全,仅示意) try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(plain.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } }主界面的设计,我习惯用BorderLayout配合JTabbedPane。
// MainFrame.java public class MainFrame extends JFrame { private JTabbedPane tabbedPane; private BookManagePanel bookPanel; private ReaderManagePanel readerPanel; private BorrowReturnPanel borrowPanel; public MainFrame() { setTitle("图书管理系统 - 管理员"); setSize(1000, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 窗口居中 // 创建菜单栏 JMenuBar menuBar = new JMenuBar(); JMenu systemMenu = new JMenu("系统"); JMenuItem logoutItem = new JMenuItem("退出登录"); JMenuItem exitItem = new JMenuItem("退出系统"); systemMenu.add(logoutItem); systemMenu.addSeparator(); systemMenu.add(exitItem); menuBar.add(systemMenu); setJMenuBar(menuBar); // 创建选项卡面板 tabbedPane = new JTabbedPane(); bookPanel = new BookManagePanel(); readerPanel = new ReaderManagePanel(); borrowPanel = new BorrowReturnPanel(); tabbedPane.addTab("图书管理", bookPanel); tabbedPane.addTab("读者管理", readerPanel); tabbedPane.addTab("借阅/归还", borrowPanel); // 可以继续添加“数据统计”、“超期查询”等标签页 add(tabbedPane, BorderLayout.CENTER); // 事件监听 logoutItem.addActionListener(e -> { int confirm = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "提示", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { this.dispose(); // 关闭主窗口 new LoginFrame().setVisible(true); // 重新打开登录窗口 } }); exitItem.addActionListener(e -> { int confirm = JOptionPane.showConfirmDialog(this, "确定要退出系统吗?", "提示", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { System.exit(0); } }); } }图书管理面板是功能最集中的地方,需要展示表格、提供查询、新增、修改、删除按钮。
// BookManagePanel.java public class BookManagePanel extends JPanel { private JTable bookTable; private BookTableModel tableModel; // 自定义的表格模型 private JTextField searchField; private JButton searchBtn, addBtn, editBtn, deleteBtn, refreshBtn; private BookService bookService = new BookService(); public BookManagePanel() { setLayout(new BorderLayout()); // 顶部:搜索栏和按钮 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchField = new JTextField(20); searchBtn = new JButton("搜索"); addBtn = new JButton("新增"); editBtn = new JButton("修改"); deleteBtn = new JButton("删除"); refreshBtn = new JButton("刷新"); topPanel.add(new JLabel("书名:")); topPanel.add(searchField); topPanel.add(searchBtn); topPanel.add(addBtn); topPanel.add(editBtn); topPanel.add(deleteBtn); topPanel.add(refreshBtn); add(topPanel, BorderLayout.NORTH); // 中部:表格 tableModel = new BookTableModel(); bookTable = new JTable(tableModel); // 设置表格属性:单行选择、调整列宽 bookTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); bookTable.getColumnModel().getColumn(0).setPreferredWidth(50); // ID列窄一些 bookTable.getColumnModel().getColumn(1).setPreferredWidth(150); // ISBN列 JScrollPane scrollPane = new JScrollPane(bookTable); add(scrollPane, BorderLayout.CENTER); // 底部:状态栏(可选) JLabel statusLabel = new JLabel("双击表格行可查看详情"); add(statusLabel, BorderLayout.SOUTH); // 加载初始数据 loadBookData(null); // 绑定事件 searchBtn.addActionListener(e -> { String keyword = searchField.getText().trim(); loadBookData(keyword); }); addBtn.addActionListener(e -> { // 弹出一个对话框(JDialog)用于输入新图书信息 new BookEditDialog(null, this).setVisible(true); }); editBtn.addActionListener(e -> { int selectedRow = bookTable.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要修改的图书!", "提示", JOptionPane.WARNING_MESSAGE); return; } // 获取选中行对应的图书ID(隐藏列,通常放在第0列) Integer bookId = (Integer) tableModel.getValueAt(selectedRow, 0); Book book = bookService.findBookById(bookId); if (book != null) { new BookEditDialog(book, this).setVisible(true); } }); deleteBtn.addActionListener(e -> { int selectedRow = bookTable.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要删除的图书!", "提示", JOptionPane.WARNING_MESSAGE); return; } int confirm = JOptionPane.showConfirmDialog(this, "确定要删除这本图书吗?", "确认删除", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { Integer bookId = (Integer) tableModel.getValueAt(selectedRow, 0); boolean success = bookService.deleteBook(bookId); // Service层应处理关联记录(如是否有未还的借阅) if (success) { JOptionPane.showMessageDialog(this, "删除成功!"); loadBookData(null); } else { JOptionPane.showMessageDialog(this, "删除失败,该书可能存在关联的借阅记录!", "错误", JOptionPane.ERROR_MESSAGE); } } }); refreshBtn.addActionListener(e -> loadBookData(null)); // 双击行查看详情 bookTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { int row = bookTable.rowAtPoint(e.getPoint()); if (row >= 0) { Integer bookId = (Integer) tableModel.getValueAt(row, 0); Book book = bookService.findBookById(bookId); // 弹出一个显示详细信息的对话框 new BookDetailDialog(book).setVisible(true); } } } }); } // 加载数据到表格 public void loadBookData(String keyword) { List<Book> bookList; if (keyword == null || keyword.isEmpty()) { bookList = bookService.getAllBooks(); } else { bookList = bookService.searchBooksByName(keyword); } tableModel.setBooks(bookList); tableModel.fireTableDataChanged(); // 通知表格数据已更新 } }自定义表格模型BookTableModel是让JTable显示我们自定义数据对象的关键。它继承自AbstractTableModel。
// BookTableModel.java public class BookTableModel extends AbstractTableModel { private List<Book> books = new ArrayList<>(); private String[] columnNames = {"ID", "ISBN", "书名", "作者", "出版社", "价格", "总数", "可借数"}; public void setBooks(List<Book> books) { this.books = books; } @Override public int getRowCount() { return books.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Book book = books.get(rowIndex); switch (columnIndex) { case 0: return book.getId(); case 1: return book.getIsbn(); case 2: return book.getName(); case 3: return book.getAuthor(); case 4: return book.getPublisher(); case 5: return book.getPrice(); case 6: return book.getTotalCount(); case 7: return book.getCurrentCount(); default: return null; } } // 设置单元格不可编辑(默认是可编辑的,双击会进入编辑状态) @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } }BookEditDialog是一个自定义的编辑对话框,用于新增和修改图书信息。它的构造器可以接收一个Book对象,如果为null则是新增,否则是修改。
4. 项目打包、部署与进阶优化
4.1 使用Maven打包可执行JAR
开发完成后,我们需要将项目打包成一个可以独立运行的JAR文件。使用Maven的maven-assembly-plugin或maven-shade-plugin可以很方便地达成这个目标,它们能把所有依赖(包括SQLite的JDBC驱动)打包进一个“胖JAR”。
在pom.xml中添加插件配置:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <!-- 指定主类,即程序入口 --> <mainClass>com.yourcompany.library.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>然后在项目根目录下执行mvn clean package,Maven会在target目录下生成一个类似library-1.0-SNAPSHOT-shaded.jar的文件。双击这个JAR,或者在命令行执行java -jar library-1.0-SNAPSHOT-shaded.jar,你的图书管理系统就能运行了。
打包踩坑记录:
- 找不到主类:确保
<mainClass>配置的路径完全正确,包括包名和类名。- 数据库文件路径问题:在IDE中运行时,数据库文件
library.db通常位于项目根目录。但打包成JAR后,JAR文件是一个压缩包,程序无法直接往里面写数据。因此,数据库文件应该放在JAR文件外部。一种常见的做法是,在程序启动时,检查当前运行目录下是否存在数据库文件,如果不存在,则从JAR内的资源中复制一个初始的空数据库文件出来。这涉及到资源文件的读取和写入操作。- 中文乱码:如果界面或数据库中出现中文乱码,检查三处:a) Java源文件编码(应为UTF-8),b) 数据库连接字符串(SQLite可以加
?charset=utf8参数),c) 控制台输出编码(对于Swing的JOptionPane,一般没问题)。
4.2 常见问题排查与调试技巧
在开发过程中,你肯定会遇到各种问题。这里列几个典型的:
问题1:界面卡顿,特别是加载大量数据到JTable时。
- 原因:Swing的GUI更新必须在事件调度线程上进行。如果在后台线程中直接操作
JTable的数据模型,可能会导致界面无响应或数据不同步。 - 解决:使用
SwingWorker。SwingWorker允许你在后台线程执行耗时任务(如数据库查询),然后在任务完成后,在事件调度线程中安全地更新GUI。// 示例:使用SwingWorker异步加载数据 searchBtn.addActionListener(e -> { String keyword = searchField.getText().trim(); new SwingWorker<List<Book>, Void>() { @Override protected List<Book> doInBackground() throws Exception { // 在后台线程执行耗时查询 return bookService.searchBooksByName(keyword); } @Override protected void done() { try { List<Book> result = get(); // 获取doInBackground的返回结果 tableModel.setBooks(result); tableModel.fireTableDataChanged(); } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(BookManagePanel.this, "查询失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } }.execute(); });
问题2:数据库操作失败,但错误信息不明确。
- 解决:一定要打印或记录完整的异常堆栈信息。在DAO层捕获到
SQLException时,不要只是简单打印e.getMessage(),用e.printStackTrace()。更专业的做法是使用日志框架如Log4j2或SLF4J。另外,SQLite的错误信息通常很直接,比如“UNIQUE constraint failed”表示违反了唯一约束,“FOREIGN KEY constraint failed”表示外键约束失败。
问题3:界面布局错乱,在不同分辨率下显示不一致。
- 原因:绝对布局(
setBounds)是万恶之源。它虽然简单,但完全不具备适应性。 - 解决:坚持使用Swing的布局管理器。
BorderLayout、FlowLayout、GridBagLayout(功能强大但复杂)、MigLayout(第三方,非常强大)是你的好朋友。对于复杂的表单,可以嵌套使用多个JPanel,每个JPanel使用不同的布局管理器。花时间学习GridBagLayout或MigLayout是值得的。
问题4:程序第一次运行正常,关闭后再打开,数据没了。
- 原因:你可能把数据库文件放在了项目的
src/main/resources目录下,并且打包时被覆盖了。或者,你每次运行都重新初始化数据库。 - 解决:如4.1节所述,将数据库文件放在JAR外部。程序启动时检查外部文件是否存在,不存在则从资源中复制初始化模板。
4.3 项目进阶优化方向
如果你已经完成了基础功能,想让这个项目更出彩,可以考虑以下优化:
- 引入日志框架:用Log4j2替换
System.out.println,可以灵活控制日志级别,输出到文件或控制台。 - 使用连接池:虽然SQLite是文件数据库,但使用像HikariCP这样的轻量级连接池,可以规范连接管理,为将来切换MySQL等数据库做准备。
- 数据验证:在界面输入和业务逻辑层加强数据验证。例如,ISBN格式、电话号码格式、日期格式等。可以使用正则表达式。
- 报表功能:使用JFreeChart或直接通过SQL统计,生成简单的图表,如“每月借阅量趋势图”、“热门图书排行榜”。
- 数据导出:将图书列表、借阅记录导出为Excel或PDF文件。可以使用Apache POI或iText库。
- 更换UI外观:Swing默认的金属外观比较老旧。可以尝试设置
LookAndFeel,比如Nimbus:UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");。 - 模块化与依赖注入:引入Spring Framework(甚至只是Spring Core)来管理Bean和依赖,虽然对于小项目有点重,但这是学习主流框架的好机会。
- 单元测试:为Service层的关键方法编写JUnit测试,确保业务逻辑的正确性。
这个基于Java Swing的图书管理系统,虽然技术栈不算新潮,但它是一个绝佳的全栈练手项目。它强迫你从数据库设计、SQL编写,到后端业务逻辑、分层架构,再到前端事件处理、界面布局,走完一个完整应用的生命周期。过程中遇到的每一个问题,都是你成长的阶梯。希望这份超详细的拆解,能帮你少走弯路,更快地构建出属于自己的那个“管理系统”。
本文还有配套的精品资源,点击获取