资讯详情 SWT + SQLite 打造Java图书管理系统:桌面应用架构与事务实践
📅 2026/10/21 5:08:50
简介基于SWT标准控件库构建的图书管理系统Java图形化界面项目主要面向正在学习Java桌面开发、数据库编程或是需要完成课程设计、毕业设计的开发者。项目围绕图书信息管理、借阅归还、读者管理等核心模块展开覆盖了JDBC数据访问、事件监听、布局管理、DAO分层等关键知识点从界面搭建到数据操作都提供了完整实现思路。压缩包内共259个文件包含45个Java源码、107个class编译结果、80个ico图标、12张jpg图片、7个jar依赖库以及SQL脚本、说明文档、可执行程序等整体约10.07MB目录结构清晰便于按模块对照学习和二次开发。目前已有483人学习下载借助源码和文档可以快速掌握SWT常用组件、表格展示、对话框交互等方法也可以直接导入IDE运行并在此基础上继续扩展借阅统计、权限管理等更多功能。1. 一个用 SWT 写的图书管理系统为什么还值得看一眼一个同学把课程设计从 Swing 换到 SWT 之后第一反应是“界面怎么这么难调”。但等他摸熟了 Display、Shell、Composite 这一套组件再回头看这个图书管理系统会发现 SWT 的价值不在于好看而在于那种由系统原生控件带来的稳定感。这个项目就是一个用 SWT 写 Java 图形化界面的完整示例包含登录、图书增删改查、读者管理、借书还书和简单的统计后端用 SQLite 存数据界面层和数据层拆得比较清楚适合想学桌面开发、又不想碰 Electron 的人拿来读源码和做二次开发。它不是生产级系统但作为 SWT 入门的参照物正好把“图形化界面到底怎么组织”这个问题讲透了。2. 架构与数据设计SWT 界面背后的三层结构2.1 技术栈与依赖SWT JDBC SQLite 怎么搭这个项目没有用 Maven 一揽子引依赖而是把 jar 直接放进 libs 目录。核心依赖就三个SWT 平台包、JFace 相关 jar、SQLite JDBC 驱动。SWT 是 Eclipse 基金会的图形库它不像 Swing 那样内置在 JDK 里必须根据操作系统下载对应版本Windows 用 win32.x86_64Linux 用 gtk.x86_64macOS 用 cocoa.x86_64。我跑这个项目时用的是 Windows 环境下载的 org.eclipse.swt.win32.win32.x86_64 压缩包解压后把里面的 swt.jar 拷到 libs 下再在 IDE 里 Build Path 添加外部 JAR 就能用了。SQLite JDBC 驱动选的是 xerial 的 sqlite-jdbc一个 jar 文件就包含了 native 库不需要额外安装 SQLite 服务。数据库连接串也就一行代码private static final String URL jdbc:sqlite:book.db;重点是 book.db 的路径。用相对路径时它被创建在System.getProperty(user.dir)指向的目录。从 IDE 直接运行时是项目根目录如果打成 jar 双击运行则可能是 jar 所在目录。这种差异很容易导致“代码一样换台电脑跑库却找不着”建议在 DBUtil 里用绝对路径拼接或者把数据库文件放到一个固定的 data 目录下。2.2 包结构与启动入口每行代码该放哪项目按界面、业务、数据访问分了三个层次。包结构是这类系统最常见的布局src/ ├── com/demo/book/ │ ├── MainApp.java // 程序入口创建 Display │ ├── ui/ │ │ ├── LoginWindow.java // 登录窗口 │ │ ├── MainWindow.java // 主界面 │ │ └── BookTable.java // 图书表格封装 │ ├── core/ │ │ ├── AuthService.java // 登录校验 │ │ └── BorrowService.java // 借阅归还业务 │ ├── dao/ │ │ ├── BookDao.java // 图书表访问 │ │ └── BorrowDao.java // 借阅记录访问 │ ├── model/ │ │ ├── Book.java │ │ ├── Reader.java │ │ └── BorrowRecord.java │ └── util/ │ └── DBUtil.java // 连接管理从这个结构能看出设计意图ui 包里的类只处理 Widget 和事件core 包处理业务规则dao 包负责 SQL。Model 是纯数据类不依赖任何 SWT 类型这样理论上业务逻辑是可以脱离界面做单元测试的。不过实际上这个项目早期版本里大量业务判断直接写在监听器里后来才逐步抽出来。对于课程设计或内部工具来说这种抽到什么程度看个人习惯。2.3 数据库表设计三张表支撑核心业务数据库表设计得很精简三张表搞定整个系统CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, isbn TEXT, publisher TEXT, total_count INTEGER DEFAULT 0, available_count INTEGER DEFAULT 0 ); CREATE TABLE reader ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT ); CREATE TABLE borrow ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER, reader_id INTEGER, borrow_date TEXT, return_date TEXT, status INTEGER DEFAULT 0 );status 字段这里有必要讲一下。我用 0 表示借出未还1 表示已归还。为什么不直接拿 return_date 是否为空来判断因为 SQLite 对 NULL 的判断如果没写熟练很容易写出where return_date null这种永远为假的 SQL。显式状态位反而更直观统计未还数量直接select count(*) from borrow where status 0就完了。外键约束我没有启用。SQLite 的外键默认是关闭的开起来需要每次连接都执行PRAGMA foreign_keys ON。这个项目的数据一致性完全靠 DAO 层的事务控制对单文件数据库来说够用省去了一堆级联操作的麻烦。3. 核心模块拆解从登录到借阅归还的完整链路3.1 程序入口Display 和 Shell 的启动时序SWT 应用的启动和 Swing 完全不同。Swing 里你 new 一个 JFramesetVisible(true) 就完事。SWT 必须自己维护一个事件循环否则窗口显示出来也没法响应鼠标键盘。MainApp 的核心代码只有十几行public static void main(String[] args) { Display display new Display(); Shell shell LoginWindow.createShell(display); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); }逻辑说明Display 代表操作系统显示资源Shell 是最顶层窗口。readAndDispatch()会把鼠标点击、键盘输入这类 Native 事件取出来分发给对应的 Widget如果当前没有事件就让线程sleep()而不是空转。当窗口被 dispose 后while 条件判断 shell 已被销毁循环退出最后释放 Display。这套时序写错了最常见的现象就是窗口看起来像死了一样其实是因为你没调用readAndDispatch()事件全堵在队列里。3.2 登录窗口用事件监听做用户校验登录窗口布局很朴素两个 Label、两个 Text、一个登录 Button。按钮点击事件里做校验这是整个系统里最容易看懂的一段代码loginBtn.addSelectionListener(new SelectionAdapter() { Override public void widgetSelected(SelectionEvent e) { String username usernameText.getText().trim(); String password passwordText.getText().trim(); if (username.isEmpty() || password.isEmpty()) { MessageBox box new MessageBox(shell, SWT.ICON_ERROR); box.setMessage(用户名和密码不能为空); box.open(); return; } if (AuthService.login(username, password)) { shell.dispose(); // 关闭登录窗口进入主界面 MainWindow.open(display, username); } else { MessageBox box new MessageBox(shell, SWT.ICON_ERROR); box.setMessage(用户名或密码错误); box.open(); } } });参数说明Text.getText()返回的是 Stringtrim()去掉首尾空格这个习惯能省掉很多“我看不到空格但比对不相等”的排查时间。SelectionAdapter是SelectionListener的适配器只重写widgetSelected一个方法就够了。登录成功后先shell.dispose()再打开主窗口避免两个窗口同时存在。登录背后的 SQL 使用的是预编译参数防止拼接字符串导致的注入风险public static boolean login(String username, String password) { String sql SELECT id FROM admin WHERE username? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); return rs.next(); } catch (SQLException ex) { ex.printStackTrace(); return false; } }3.3 图书列表TableViewer 与数据绑定的实现主界面左边是图书表格。用 JFace 的 TableViewer 而不是裸 Table好处是数据变化后不需要手动维护 TableItem。核心代码TableViewer viewer new TableViewer(parent, SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL); viewer.getTable().setLinesVisible(true); viewer.getTable().setHeaderVisible(true); String[] columns {书名, 作者, ISBN, 库存, 可借}; for (String column : columns) { TableColumn tableColumn new TableColumn(viewer.getTable(), SWT.LEFT); tableColumn.setText(column); tableColumn.setWidth(120); } viewer.setContentProvider(ArrayContentProvider.getInstance()); viewer.setLabelProvider(new BookLabelProvider()); viewer.setInput(BookDao.findAll());ArrayContentProvider会遍历传入的ListBook把每个 Book 对象交给BookLabelProvider渲染。后者按列索引返回字符串class BookLabelProvider extends LabelProvider implements ITableLabelProvider { Override public String getColumnText(Object element, int columnIndex) { Book book (Book) element; switch (columnIndex) { case 0: return book.getTitle(); case 1: return book.getAuthor(); case 2: return book.getIsbn(); case 3: return String.valueOf(book.getTotalCount()); case 4: return String.valueOf(book.getAvailableCount()); default: return ; } } }逻辑说明getColumnText接收的是列表里的某个对象而不是 Table 行本身。columnIndex 是创建 TableColumn 的顺序。这样表格列顺序调整时只需要改这里的分支顺序。如果只想刷新某一行数据调用viewer.refresh(book)如果整个列表变了调用viewer.setInput(newList)加上viewer.refresh()TableViewer 会帮你做 diff 和局部更新比手动删掉 TableItem 再重建快得多。3.4 借阅归还库存扣减与状态更新的关键逻辑借书操作涉及两步写操作插入借阅记录、扣减库存。这两步必须在一个事务里否则会出现“借阅记录有了但库存没减”的脏数据。BorrowService 里借书方法的代码是整条链路里最值得抄的一段public boolean borrow(int bookId, int readerId) { try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); PreparedStatement check conn.prepareStatement( SELECT available_count FROM book WHERE id?); check.setInt(1, bookId); ResultSet rs check.executeQuery(); if (!rs.next() || rs.getInt(available_count) 0) { conn.rollback(); return false; } PreparedStatement insert conn.prepareStatement( INSERT INTO borrow (book_id, reader_id, borrow_date, status) VALUES (?, ?, date(now), 0)); insert.setInt(1, bookId); insert.setInt(2, readerId); insert.executeUpdate(); PreparedStatement update conn.prepareStatement( UPDATE book SET available_count available_count - 1 WHERE id?); update.setInt(1, bookId); update.executeUpdate(); conn.commit(); return true; } catch (SQLException ex) { try { conn.rollback(); } catch (SQLException ignored) {} ex.printStackTrace(); return false; } }逻辑说明先查available_count如果小于等于 0 直接回滚并返回 false表示没库存。接着插入借阅记录然后扣减库存。setAutoCommit(false)之后任何一步失败都会在 catch 里调用rollback()把之前执行过的 SQL 全部撤销。finally 里如果没有恢复自动提交连接一旦复用就会出现诡异行为所以我习惯在关闭连接前把 autoCommit 改回 true。还书逻辑是镜像操作把 status 改成 1写 return_date再把 available_count 加一。这个流程里要特别判断重复还书比如已经归还的记录再次点还书SQL 的 update 条件要带上AND status 0这样更新影响的行数是 0就能在业务层给用户“这本书已归还”的提示而不是静默失败。4. 避坑与排查SWT 开发里最容易翻车的四个细节4.1 UI 线程卡死为什么点查询界面就白屏现象点击“查询”按钮后整个窗口拖不动、不重绘等好几秒才突然显示结果期间系统像死机一样。原因SWT 的事件监听器跑在 UI 线程里监听器内部直接执行了select * from book where title like ?上万条数据加载期间readAndDispatch()没有机会处理重绘事件界面自然卡死。解决把耗时查询放到后台线程查询完毕再用Display.asyncExec回到 UI 线程刷新表格。new Thread(() - { ListBook list BookDao.search(keyword); Display.getDefault().asyncExec(() - { viewer.setInput(list); viewer.refresh(); }); }).start();注意asyncExec里的代码只做控件更新不要再做查询——这里的 Runnable 依然是交给 UI 线程执行的放耗时操作等于白改。4.2 中文乱码与文件编码一套代码两个环境的结果不同现象源码注释是中文运行起来窗口标题、按钮文字全部变成乱码往 SQLite 里插入中文查询出来是问号。原因从两个方向排查。第一是源码文件的编码项目文件保存时用的是系统默认编码Windows 下是 GBK导入到另一个设置 UTF-8 的 IDE 后中文字节被错误解码。第二是 JDBC 连接串没指定编码SQLite 驱动按 JVM 默认字符集处理。解决IDE 里统一两端项目编码为 UTF-8源码文件重新用 UTF-8 保存数据库 URL 后面附带编码参数比如 MySQL 是?useUnicodetruecharacterEncodingutf-8SQLite 不需要这个参数但要确保 javac 编译时的-encoding UTF-8参数加上。4.3 布局被压缩GridData 设置不当的典型表现现象按钮和输入框挤成一团窗口拉伸时表格宽度不变按钮却跑到屏幕外面。原因SWT 的 GridLayout 不像 Swing 的 BorderLayout 有那么强的自动分配能力每个组件必须通过 GridData 声明“我要占用多少空间”。常见错误是只写了new GridData(SWT.FILL, SWT.FILL, false, false)——后两个布尔值表示是否抢占多余空间设为 false 时组件只保持初始大小。解决给表格和主面板设置正确的 GridDataGridData tableData new GridData(SWT.FILL, SWT.FILL, true, true); tableData.widthHint 600; tableData.heightHint 400; table.setLayoutData(tableData);true, true分别表示水平方向、垂直方向都跟随窗口拉伸扩展。设置完后窗口拉大表格会自动占满剩余空间。4.4 资源泄漏Image 和 Font 累计造成的卡顿现象程序运行两个小时以后越来越慢打开关闭窗口多次后内存占用只增不减最后界面甚至画不出来。原因SWT 的 Image、Font、Color 是操作系统句柄资源不调用dispose()就不会释放。我在表格的书名前面加了图标每次刷新都new Image()却没有在表格销毁时释放句柄被耗光之后所有控件都开始罢工。解决资源创建一次全局复用并在 Widget dispose 时统一清理table.addDisposeListener(e - { for (Image img : imageCache.values()) { img.dispose(); } });从那以后我给自己定了个规矩在一个界面类里 new 了 Image 或 Font必须立刻想清楚它什么时候 dispose而不是等 GC 来救。5. 本地运行与验证从环境配置到功能自测5.1 环境准备与导入步骤运行这个项目不需要多复杂的环境JDK 8 以上、一个支持 Java 的 IDE 就够。SWT 的 jar 包要按操作系统选我这里的做法是从 SWT 下载页选择对应操作系统的压缩包解压后把 swt.jar 放项目 libs 目录。把 sqlite-jdbc 驱动 jar 也放进 libs。IDE 里右键两个 jarBuild Path - Add to Build Path。检查项目编码设置为 UTF-8避免中文乱码。运行 MainApp.java等于启动整个应用。首次运行前DBUtil 的静态初始化块里会创建 book.db 文件和必要的表。这个初始化逻辑是幂等的用CREATE TABLE IF NOT EXISTS所以不用担心重复启动导致数据被清掉。初始管理员账号写在脚本里用户名 admin密码 123456。5.2 启动失败排查SWT 包不匹配的典型报错最常见的启动失败是运行时抛java.lang.UnsatisfiedLinkError: no swt-win32-xxxx in java.library.path或者NoClassDefFoundError: org/eclipse/swt/widgets/Display。前者说明 SWT 的 native 库没找到多半是 jar 里包含的 dll 与当前系统不匹配后者说明 classpath 里根本没有 swt.jar。解决方式就是核对 jar 的命名org.eclipse.swt.win32.win32.x86_64明确支持 64 位 Windows而win32.win32.x86是 32 位的。把 32 位 jar 扔到 64 位 JVM 里轻则报错重则直接崩溃。还有一种玄学情况同一个项目刚才还好好的重启后突然报Invalid thread access。这通常是你在非 UI 线程里创建了 Widget。SWT 规定所有控件都必须在创建它的那个 Display 线程上操作后台线程里 new 控件是踩红线。解决方法是把控件创建代码放进display.asyncExec或者用Display.getDefault().syncExec切回 UI 线程。5.3 功能验证清单怎么确认这套系统可用跑起来之后我习惯按一套清单过一遍确认不是“能启动就代表没问题”。登录验证分别输入空用户名、正确用户名加错误密码、正确账号。添加图书新增一本书表格刷新后能看到新行库存数字正确。借书选一本有库存的书点借出表格里可借数量减一。还书在借阅列表里选记录点还书可借数量加一借阅状态变更为已归还。统计标签主界面底部显示的总册数和借出数量与表格里的数字对得上。边界测试才是容易翻车的地方。我一般会额外测试连续快速点击两次借书按钮看库存会不会变成负数借出一本库存为 0 的书看有没有弹窗提示。这些用例不通过说明事务处理和库存检查有漏洞演示的时候会被现场打脸。6. 二次开发的一个实用技巧把业务从界面事件里解耦这个项目为了演示方便事件监听器里经常直接写整个流程先是数据库查询然后 MessageBox 弹窗最后刷新表格。这样写一时痛快但等到要加“借阅权限校验”“逾期次数限制”这些规则时监听器会膨胀到几百行。我重构时最常用的一个技巧是定义一个结果回调接口让业务服务完全不依赖 SWT 类型。比如借书按钮改造前的监听器里塞满了事务代码和弹窗逻辑。改造后业务方法接受一个 BorrowCallbackpublic interface BorrowCallback { void onSuccess(); void onFailure(String reason); } BorrowService.borrow(bookId, readerId, new BorrowCallback() { Override public void onSuccess() { viewer.setInput(BookDao.findAll()); viewer.refresh(); statusLabel.setText(借出成功); } Override public void onFailure(String reason) { MessageBox box new MessageBox(shell, SWT.ICON_ERROR); box.setMessage(reason); box.open(); } });这样 BorrowService 里不再出现 MessageBox、Button 这些 SWT 类可以单独用 JUnit 写测试用例。界面层只负责两件事把用户动作翻译成方法调用把回调结果翻译成界面更新。后续如果要给借书加一个“读者借阅数量上限”的规则只需要在 borrow 方法里先检查count(borrow where reader_id and status0) 5失败时调用callback.onFailure(该读者已借满 5 本)界面这边一行都不用改。从那次重构之后我每次在新界面上写业务逻辑都会先问自己一句这个逻辑脱离 Display 能不能跑能跑就放 Service不能跑才留在监听器。这个习惯让这个图书管理系统的维护成本降了很多。后来给它加分类筛选、逾期罚款功能时基本只需要新写 DAO 方法和界面组件老代码几乎没动。希望这个思路能帮你在自己的 SWT 项目里少走一段弯路。本文还有配套的精品资源点击获取