Java SWT 桌面图书管理系统实战:从登录到借还的完整实现

发布时间:2026/11/2 17:43:31
Java SWT 桌面图书管理系统实战:从登录到借还的完整实现

简介:这份资源是一套基于SWT构建的Java图书管理系统图形化界面项目,面向已掌握Java基础、希望借助原生控件开发桌面应用的开发者与课程设计学生,可用于解决图书信息管理、借阅归还与查询等典型业务场景的界面搭建问题。压缩包共259个文件,约10.07MB,包含45个java源码、107个class编译文件、80个ico图标、12张jpg图片及7个jar依赖库,另附sql建库脚本、doc说明文档与exe可执行程序,覆盖源码、资源与部署产物。已有484人学习下载。项目按主窗口、图书与读者数据访问、增删改分组界面等职责拆分多个类,配合SWT布局管理、Table表格展示与对话框交互,读者可从中理解事件驱动编程、JDBC数据库操作与模块化界面组织方式,并参考资源加载与JAR打包思路,快速搭建可运行的桌面端图书管理原型。

1. 桌面端图书管理:用 SWT 把 Java 图形界面做成能交付的样子

接手一个图书管理系统的维护任务时,很多人第一反应是打开浏览器写 Vue,但真实场景里不少单位的内网机器根本不允许装现代浏览器,或者业务方就认一个双击即开的桌面程序。这个项目走的是另一条路:用 Java 加 SWT 做原生图形界面,把图书录入、查询、借还、读者管理这些日常操作塞进一个不依赖 Web 容器的桌面客户端里。SWT 的特点是直接调用操作系统的原生控件,窗口、表格、按钮长得跟系统自带程序一致,启动快、内存占用低,对老机器友好。适合谁?适合要交课程设计的学生、要给内网做轻量工具的开发,以及想找一个能跑起来的 Java 桌面 CRUD 骨架做二次开发的人。下面按「它长什么样、怎么跑、核心代码怎么写、哪里容易翻车、怎么改」的顺序拆。

2. 项目结构与技术选型:SWT 桌面客户端的骨架长什么样

2.1 为什么是 SWT 而不是 Swing

先把选型说清楚,因为这决定了后面所有代码的写法。Swing 是 Java 自绘控件,跨平台一致但观感偏旧;SWT 走的是 JNI 调用本地控件库,Windows 上就是 Win32 控件,Linux 上是 GTK,所以界面跟系统原生程序一个味道。代价是 SWT 依赖平台相关的本地库,打包分发时要把对应平台的 jar 带上,这是后面避坑章节要重点讲的地方。项目里常见的依赖就三块:SWT 本体、JDBC 驱动、以及可选的日志或工具库。用 Maven 的话,SWT 的坐标要带平台分类器,比如 Windows 64 位是org.eclipse.swt.win32.win32.x86_64,这一点新手最容易漏。

<!-- pom.xml 片段:SWT 依赖必须带平台分类器 --> <dependency> <groupId>org.eclipse.platform</groupId> <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId> <version>3.124.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

上面这段的关键在classifier隐含在 artifactId 里,不同平台换 artifactId 即可,版本号按你本地仓库实际有的填。JDBC 驱动版本要和数据库服务端匹配,MySQL 8 用 8.x 驱动,5.7 用 5.1.x,混用会报时区或认证插件错误。

2.2 目录结构:按职责分层,别把 SQL 写进按钮回调

一个能维护的桌面项目,目录不该是「一个 Main 类加一堆监听器」。这个项目合理的组织方式是按职责分层,界面层只负责收集输入和展示结果,业务逻辑和数据访问各自独立。下面是我拆这类项目时常见的结构,读者拿到源码可以对照着看:

book-manager/ ├── src/main/java/ │ ├── app/ # 程序入口,Display/Shell 生命周期 │ │ └── MainApp.java │ ├── ui/ # 界面层:窗口、表格、对话框 │ │ ├── LoginWindow.java │ │ ├── MainWindow.java │ │ └── BookEditDialog.java │ ├── service/ # 业务层:借还规则、库存校验 │ │ ├── BookService.java │ │ └── BorrowService.java │ ├── dao/ # 数据访问层:JDBC 增删改查 │ │ ├── BookDao.java │ │ └── ReaderDao.java │ ├── model/ # 实体类:Book、Reader、BorrowRecord │ └── util/ # 连接池、日期格式化、弹窗封装 ├── src/main/resources/ │ └── db.properties # 数据库连接配置 └── lib/ # 非 Maven 方式下的 swt.jar、驱动 jar

这个分层不是摆设。界面层拿到用户输入后调 service,service 做校验再调 dao,dao 只认 SQL 和实体对象。好处是换数据库只动 dao,改界面只动 ui,测试 service 不用起窗口。很多学生项目把SELECT * FROM book直接写在按钮的addSelectionListener里,功能能跑,但一改需求就牵一发动全身,这是血泪经验。

2.3 入口与窗口生命周期

SWT 程序的骨架跟 Swing 不一样,它有一个Display代表与操作系统的连接,所有Shell(窗口)都挂在它下面,事件循环靠readAndDispatch驱动。入口类通常长这样:

public class MainApp { public static void main(String[] args) { Display display = new Display(); // 建立与本地窗口系统的连接 Shell shell = new Shell(display); // 主窗口 shell.setText("图书管理系统"); shell.setSize(900, 600); // 这里挂载主界面内容,通常是 new MainWindow(shell).build(); shell.open(); while (!shell.isDisposed()) { // 事件循环:窗口没关就一直转 if (!display.readAndDispatch()) { display.sleep(); // 没事件时让出 CPU } } display.dispose(); // 释放本地资源 } }

Display和Shell必须成对释放,display.dispose()漏掉在 Windows 上可能看不出问题,但在 Linux 上会残留句柄。readAndDispatch返回 false 表示当前没有待处理事件,此时sleep让线程挂起,避免空转吃满一个核。这个循环是所有 SWT 程序的命脉,后面任何弹窗、表格刷新都跑在这个线程里,所以耗时操作(比如大批量导入)不能直接写在监听器里,否则界面直接假死,这是桌面开发跟 Web 最大的体感差异。

3. 核心模块拆解:从登录到借还的一条完整链路

3.1 登录窗口与数据库连接

登录是第一个能验证环境是否搭对的界面。它要做的事很朴素:拿用户名密码去reader或admin表里比对,成功就关掉登录窗、开主窗。SWT 里文本框用Text,密码框加SWT.PASSWORD样式,按钮用Button。关键代码在事件监听里:

Button loginBtn = new Button(shell, SWT.PUSH); loginBtn.setText("登录"); loginBtn.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { String user = userText.getText().trim(); String pwd = pwdText.getText(); if (user.isEmpty() || pwd.isEmpty()) { MessageBox box = new MessageBox(shell, SWT.ICON_WARNING); box.setMessage("用户名和密码不能为空"); box.open(); return; } // 交给 service 校验,界面层不碰 SQL boolean ok = new UserService().login(user, pwd); if (ok) { shell.dispose(); // 关闭登录窗 openMainWindow(); // 打开主界面 } else { MessageBox box = new MessageBox(shell, SWT.ICON_ERROR); box.setMessage("账号或密码错误"); box.open(); } } });

SelectionAdapter是 SWT 提供的适配器,只重写你关心的方法,比实现整个SelectionListener干净。MessageBox是原生弹窗,样式位SWT.ICON_WARNING、SWT.ICON_ERROR决定图标。注意shell.dispose()之后不能再操作这个 shell 的控件,否则抛SWTException: Widget is disposed,这是新手高频翻车点。数据库连接建议在util里封装一个DBUtil,从db.properties读 url、用户名、密码,用静态方法返回Connection,别在每个 dao 里重复写驱动加载。

3.2 图书列表:TableViewer 与数据绑定

主界面最核心的控件是图书列表。SWT 原生的Table要手动一行行TableItem塞数据,代码又臭又长;项目里更常见的做法是用 JFace 的TableViewer,它支持内容提供器和标签提供器,数据源直接给List<Book>,刷新时调viewer.setInput(list)即可。下面是一个最小可用的表格搭建:

TableViewer viewer = new TableViewer(parent, SWT.BORDER | SWT.FULL_SELECTION); TableViewerColumn colId = new TableViewerColumn(viewer, SWT.NONE); colId.getColumn().setText("编号"); colId.getColumn().setWidth(80); // 其余列:书名、作者、出版社、库存、状态,写法同上 viewer.setContentProvider(ArrayContentProvider.getInstance()); viewer.setLabelProvider(new ITableLabelProvider() { public String getColumnText(Object element, int index) { Book b = (Book) element; switch (index) { case 0: return String.valueOf(b.getId()); case 1: return b.getTitle(); case 2: return b.getAuthor(); case 3: return String.valueOf(b.getStock()); default: return ""; } } // getColumnImage 返回 null 即可 }); viewer.getTable().setHeaderVisible(true); viewer.getTable().setLinesVisible(true); viewer.setInput(new BookService().listAll()); // 首次加载

ArrayContentProvider把List当数组处理,ITableLabelProvider的getColumnText按列索引返回字符串,索引顺序必须和建列顺序严格一致,错一位整列数据就串了。setInput是刷新入口,增删改之后重新查库再setInput一次,表格就更新了。数据量上千条时这种全量刷新会卡,常见做法是加分页或改用LazyContentProvider,但课程设计规模下全量足够。

3.3 借还书业务:事务与库存校验

借书不是简单插一条记录,它要同时做两件事:写借阅记录、扣减库存,这两步必须在一个事务里,否则出现「记录写了库存没扣」的脏数据。service 层的写法:

public boolean borrow(int bookId, int readerId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 BookDao bookDao = new BookDao(conn); Book book = bookDao.findById(bookId); if (book == null || book.getStock() <= 0) { conn.rollback(); return false; // 库存不足,回滚 } bookDao.decreaseStock(bookId); // 库存 -1 new BorrowDao(conn).insert(bookId, readerId); // 写借阅记录 conn.commit(); // 两步都成功才提交 return true; } catch (SQLException ex) { try { if (conn != null) conn.rollback(); } catch (SQLException ignore) {} return false; } finally { DBUtil.close(conn); // 归还连接 } }

setAutoCommit(false)是事务开关,commit和rollback必须成对出现,finally里关连接保证异常路径也不泄漏。dao 构造函数接收Connection而不是自己新建,这样多个 dao 共享同一个事务连接,这是事务能生效的前提。还书逻辑对称:更新记录状态为已还、库存 +1,同样包在一个事务里。库存校验放在扣减之前,且最好用UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0这种带条件的 SQL,靠数据库行锁兜底,比先查后改更抗并发。

4. 跑起来与打包分发:环境、命令和踩坑排查

4.1 环境准备与启动步骤

跑这个项目需要 JDK(建议 8 或 11,SWT 新版本对高版本 JDK 也支持但要注意模块化限制)、数据库(MySQL 常见)、以及 SWT 本地库。步骤按顺序来:先建库建表,把项目里的schema.sql或建表语句执行一遍,至少要有book、reader、borrow_record三张表;再改db.properties里的连接信息;然后确认 SWT jar 的平台版本跟你的系统一致;最后从MainApp启动。用 Maven 的话一条命令编译运行:

# 编译并运行,注意 SWT 依赖已按平台引入 mvn clean compile mvn exec:java -Dexec.mainClass="app.MainApp"

不用 Maven 的老项目就把swt.jar和驱动 jar 放进lib/,编译时-cp "lib/*",运行时同样带上 classpath。启动后如果窗口一闪而过,多半是main里抛了异常但没打印,检查控制台堆栈。

4.2 常见问题排查:五条踩坑记录

现象一:启动报UnsatisfiedLinkError: no swt-win32-... in java.library.path。原因是用错了平台的 SWT jar,比如在 Windows 上放了 Linux 版。解决是换对应平台的 artifactId 或 jar 文件,64 位系统必须用 x86_64 版本,32 位 JDK 配 32 位 SWT,混用必炸。

现象二:界面能开,但点查询按钮整个窗口卡住不动。原因是把耗时查询或大批量导入直接写在了widgetSelected里,占住了 UI 线程。解决是把耗时逻辑丢到后台线程,查完用display.asyncExec回到 UI 线程更新控件,SWT 控件只能在创建它的线程里操作,跨线程直接改会抛Invalid thread access。

现象三:中文显示成方块或乱码。原因是源码编码、数据库连接编码、控件字体三者不一致。解决是编译加-encoding UTF-8,JDBC url 带useUnicode=true&characterEncoding=utf8,建库时字符集用 utf8mb4,三处对齐基本就正常了。

现象四:打包成 jar 后双击没反应,命令行运行报找不到 SWT 本地库。原因是 SWT 的本地 dll/so 在 jar 里,普通java -jar不会自动解压加载。解决是用mvn shade或assembly打 fat jar 时把本地库一并处理,或者干脆用jar-in-jar方式,更省事的做法是导出时把swt.jar放同级目录并用脚本指定 classpath。

现象五:借书成功但库存没变,或者库存变了记录没写。原因是两步操作没包在同一个事务里,或者 dao 各自拿了不同连接。解决是按 3.3 的写法共享一个Connection,setAutoCommit(false)后统一commit,异常路径rollback。

提示:SWT 的Widget is disposed和Invalid thread access是这类项目里出现频率最高的两个异常,前者查控件生命周期,后者查线程归属,基本能覆盖大半界面崩溃。

5. 二次开发改造:把课程设计级代码推到能用的程度

拿到这套代码想继续改,我一般从三个方向下手,按投入产出排序。第一是数据访问层换连接池,现在每个操作新建连接,并发一高就顶不住,引入 HikariCP 或 Druid,把DBUtil.getConnection改成从池里取,改动集中在一个类,风险低收益明显。第二是界面与业务解耦,把MainWindow里散落的校验逻辑抽到 service,界面只做展示,这样加「预约」「续借」这类新功能时不用动界面代码。第三是加一层简单的权限控制,登录时把用户角色放进会话对象,主界面按角色隐藏「图书维护」菜单,SWT 里就是menuItem.setEnabled(false)或直接不创建。

改造时验证方法要跟上。我的习惯是每改一个 dao 方法,先在main里写个临时调用跑一遍,确认 SQL 和事务没问题再接到界面上;界面改动则固定走「登录 → 查询 → 借书 → 还书 → 关闭」这条主链路手动过一遍,因为桌面程序没有浏览器那种刷新即恢复的后悔药,状态残留很容易骗过眼睛。下面这张表是我改这类项目时常用的对照,供参考:

改造点改动位置验证方式
连接池util/DBUtil连续借还 50 次看连接是否泄漏
业务抽离service 层主链路手动回归
权限菜单MainWindow不同角色登录对比菜单
分页查询BookDao + TableViewer造 2000 条数据看滚动流畅度

有个教训值得说:早期我图省事,在表格的setInput之后直接改TableItem的背景色标记逾期记录,结果每次刷新颜色错位,查了半天才发现TableViewer的刷新会重建 item,得用LabelProvider的update方法或者viewer.refresh()配合列标记来做。从那以后我每次动表格展示逻辑,都强制先造一批边界数据(空值、超长书名、逾期记录)跑一遍再提交。希望这些能帮你在拆这套 SWT 图书管理系统时少走点弯路。

本文还有配套的精品资源,点击获取