首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Spring Boot + MyBatis + JavaFX三件套集成实践:从架构到避坑全解析
📅 2026/10/15 17:07:19
✍️ 爱科研究院
👁 阅读 3,247
简介这是一套将SpringBoot、MyBatis与JavaFX整合的档案管理扫描功能项目源码适合正在学习Java桌面端与后端服务整合、持久层映射的开发者作为综合实践参考。压缩包共含130个文件核心为65个xml配置与MyBatis映射文件、24个java业务及控制器源码、28个class编译产物另有FXML界面布局、CSS美化样式、Properties工程配置及少量依赖jar整体体积仅146KB下载与查阅都很方便。目前已有4207人学习下载内容精炼具有一定的参考价值。项目结构完整清晰展示了SpringBoot自动配置与内嵌服务器机制、MyBatis通过SQL映射解耦数据库操作、JavaFX构建响应式桌面GUI并借助Oracle数据库完成档案数据的存储、查询与事务管理。读者可以从中看到一个桌面应用从界面交互到数据库落地的完整链路尤其适合模仿其分层设计、配置组织方式以及前后端调用逻辑。1. 这个三件套组合到底在解决什么问题我接到过不少这样的需求客户手里有一套老旧的 Swing 桌面管理系统界面丑、报表逻辑和数据库访问全揉在一个类里改一个查询要重新打包发版。后来换过一条路用浏览器做前端后端挂 Spring Boot但客户是内部办公场景数据敏感、网络受限IT 部门明确要求数据不能出内网浏览器方案又涉及部署和维护浏览器服务。最后落地的方案就是 Spring Boot MyBatis JavaFXJavaFX 负责桌面端界面和交互Spring Boot 在同一个进程里掌管业务逻辑和对象装配MyBatis 负责 SQL 持久化。三件套的核心价值在于既保留桌面客户端的离线操作能力和部署简单性又能享受 Spring 生态的依赖注入、事务管理和 MyBatis 的 SQL 可控性适合中小型内部管理系统、运维工具、图像处理 Demo 这类重数据录入和查询的项目。这套组合的适用人群很明确已经会 Spring Boot 和 MyBatis、但没碰过 JavaFX 的后端开发或者是 Swing 老手想迁移到现代化 UI 框架。它的学习曲线不算陡真正磨人的是把 JavaFX 的 UI 生命周期和 Spring 的容器生命周期拧到一起以及最后打包时 classpath 和资源的处理。这篇笔记会把进程内组合的架构选型、最小可运行代码、MyBatis 接线方式、五个高频坑和最后验证方法一次讲完。2. 认清两种架构形态进程内还是进程外2.1 为什么我不推荐进程外方案最常见的一种做法是 JavaFX 只当纯客户端Spring Boot 单独起一个端口当后端服务两边通过 HTTP 通信。这个方案看似解耦实际上在桌面场景里很尴尬客户机器上要维护两个进程JavaFX 是桌面程序Spring Boot 是个控制台窗口用户一不留神就把后端关掉了内网环境下端口被安全策略拦掉也是常事数据交互全走 JSON 序列化查询列表大一点界面转圈转得明显。某公司的一个内部库存工具走了这条路上线后一半的工单都跟“服务没启动”有关。我一般倾向用进程内方案JavaFX 的 main 方法里直接拉起 Spring 容器整个项目打成带依赖的 fat jar 或者 jlink 镜像用户双击运行的就是一个完整应用。Spring Boot 内置的 Web 容器可以关掉业务逻辑作为普通的 Bean 被 JavaFX Controller 注入使用。这样部署件只有一个事务、连接池、MyBatis 全部在同一个 JVM 里工作桌面操作的数据延迟基本为零。2.2 进程内架构的分层与职责边界进程内方案虽然实现简单但分层反而要更克制。我建议保持三层JavaFX 层只做界面渲染和事件绑定不写 SQL、不直接 new ServiceSpring Service 层处理业务逻辑、事务边界MyBatis Mapper 层只做持久化。Controller 里禁止出现new SqlSessionFactory()之类的字眼也禁止把 Mapper 静态化。常见做法是让 JavaFX 的 Controller 类由 Spring 容器创建而不是由 FXML 加载器创建。这样才能把 Service 注入进 Controller。FXML 里的fx:controller属性依然要写类全路径但加载时要用 Spring 容器来实例化这个 Controller。具体做法在下一节讲。2.3 最小可运行骨架让 Spring 管理 JavaFX 生命周期先看启动入口。JavaFX 的Application类有独立的生命周期它在 main 线程里通过launch()启动而 Spring Boot 的SpringApplication.run()会阻塞当前线程。两者不能在同一个线程里顺序调用否则launch()之后 Spring 根本没机会启动。核心技巧是用SpringApplicationBuilder把 Spring 容器在init()阶段就拉起再在start()里加载 FXML。// App.java - 应用入口 public class App extends Application { private ConfigurableApplicationContext springContext; Override public void init() { // JavaFX 的 init() 运行在 JavaFX-Launcher 线程适合做容器初始化 springContext new SpringApplicationBuilder(SpringBootApp.class) .web(WebApplicationType.NONE) // 关闭 Web 容器纯桌面应用 .run(getParameters().getRaw().toArray(new String[0])); } Override public void start(Stage primaryStage) throws Exception { // 从 Spring 容器拿 JavaFX 的 Application 的子类实例而不是直接 new SpringFxmlLoader loader springContext.getBean(SpringFxmlLoader.class); Parent root loader.load(/fxml/MainView.fxml); primaryStage.setTitle(某跨平台系统); primaryStage.setScene(new Scene(root, 1200, 800)); primaryStage.show(); } Override public void stop() { // 让 Spring 容器跟着窗口关闭一起收尾否则 JVM 不退出 springContext.close(); } public static void main(String[] args) { launch(args); } }init()是 JavaFX 生命周期里最早且适合做重量级初始化动作的地方比放在main()里更好控制启动顺序。web(WebApplicationType.NONE)是这里的关键参数桌面程序不需要内嵌 Tomcat关掉它能让启动时间缩短不少也少了端口占用问题。最后的main()里只调launch(args)不要在这里写任何业务代码。注意这个配置里 Spring Boot 的启动类SpringBootApp只留一个SpringBootApplication注解不需要main()方法因为它不是手工启动的而是由SpringApplicationBuilder在init()里触发。2.4 SpringFxmlLoader 的写法把 Controller 交给容器// SpringFxmlLoader.java - 自定义 FXML 加载器 Component public class SpringFxmlLoader { private final ConfigurableApplicationContext springContext; public SpringFxmlLoader(ConfigurableApplicationContext springContext) { this.springContext springContext; } public Parent load(String fxmlPath) throws IOException { FXMLLoader loader new FXMLLoader(getClass().getResource(fxmlPath)); loader.setControllerFactory(springContext::getBean); return loader.load(); } }FXMLLoader默认用无参构造器反射创建 Controller但 Spring 管理的 Bean 有很多依赖需要注入直接反射会拿到一个所有字段都是 null 的壳子。setControllerFactory的作用就是告诉 FXML 加载器“别用反射了找 Spring 容器要 Bean”。springContext::getBean这个函数式写法会把 FXML 里声明的 Controller 类名当作 Bean 名去容器里查找。还有一个细节如果 FXML 里的fx:controller声明了类路径而 FXML 的属性文件也准备用 Spring 管理可以用setControllerFactory((Class? type) - springContext.getBean(type))这样所有 Controller 都能被 Spring 容器实例化和注入Service、Mapper 在 Controller 里用Autowired或构造器注入都正常生效。3. 数据层接入与事务边界MyBatis 在这套架构里的正确姿势3.1 Mapper 扫描与数据源配置MyBatis 在 Spring Boot 里一般用MapperScan指定 Mapper 接口的包路径配合mapper-locations指定 XML 文件位置。桌面应用和 Web 应用不同的点在于数据源高度依赖本地环境配置不能写死。# application.yml - 本地数据库连接配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/desktop_app?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: ${DB_USER:root} password: ${DB_PASSWORD:root} mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.domain configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必开否则数据库字段user_name映射到实体属性userName会全部为 null而且不报错只在运行时出现“查出来的数据全是空”的诡异现象。log-impl在开发阶段用 StdOutImpl方便在控制台直接看 SQL 和参数生产环境建议改成org.apache.ibatis.logging.slf4j.Slf4jImpl。加密和安全性上DB_USER和DB_PASSWORD用环境变量占位符是常见做法桌面应用不建议把密码明文写进 yml。如果有特殊合规要求可以考虑对密码字段做 jasypt 处理的方案不过那是额外的事了。3.2 一个完整的 Mapper 接口与 XML 实例// UserMapper.java - Mapper 接口 Mapper public interface UserMapper { User selectById(Param(id) Long id); ListUser selectPage(Param(offset) int offset, Param(limit) int limit); int insert(User user); int update(User user); }!-- UserMapper.xml - SQL 语句与结果映射 -- ?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper resultMap idUserMap typecom.example.domain.User id propertyid columnuser_id/ result propertyuserName columnuser_name/ result propertyemail columnemail/ /resultMap select idselectById resultMapUserMap SELECT user_id, user_name, email FROM t_user WHERE user_id #{id} /select select idselectPage resultMapUserMap SELECT user_id, user_name, email FROM t_user ORDER BY user_id LIMIT #{offset}, #{limit} /select insert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO t_user(user_name, email) VALUES(#{userName}, #{email}) /insert update idupdate UPDATE t_user SET user_name #{userName}, email #{email} WHERE user_id #{id} /update /mapperMapperScan放在 Spring Boot 启动类上指定接口包路径。XML 的 namespace 必须和接口全限定名一致这是 MyBatis 绑定接口和 SQL 的唯一依据写错一个字符启动时就报绑定异常。分页用的LIMIT #{offset}, #{limit}在 MySQL 下直接生效如果换成 PostgreSQL 或 SQLite语法需要调整。桌面系统如果数据量不大不建议引入 PageHelper 这类分页插件手写 LIMIT 更直白。3.3 桌面场景下 Service 层事务的边界设置事务在桌面应用里的边界和 Web 应用不一样。Web 层通常一个请求一个事务桌面应用里用户可能在一个界面里连续编辑多条记录最后点保存。这时候事务要放在 Service 方法上而不是 Controller 方法上。我见过有同事把Transactional写在 JavaFX 的 Controller 方法里结果代理对象不是由 Spring 管理的事务完全没生效update 一条成功一条失败数据处于半提交状态。// UserService.java - 事务设置在 Service 层 Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper userMapper; } Transactional(rollbackFor Exception.class) public void batchImport(ListUser users) { for (User user : users) { if (userMapper.selectById(user.getId()) ! null) { userMapper.update(user); } else { userMapper.insert(user); } } } }rollbackFor Exception.class在桌面场景极其重要。Spring 默认只对 RuntimeException 回滚检查性异常不会触发回滚而业务代码里捕获IOException后想整体回滚是常事不加这个参数就会静默提交。每一批导入几千条数据量并不大但事务一致性很重要——这是内部管理系统最喜欢埋雷的地方。3.4 连接池和 SQLite 的特殊处理如果系统是单机部署也可以选 SQLite 代替 MySQL。SQLite 在 MyBatis 里配置方式和常规数据库略有不同驱动用org.sqlite.JDBCURL 是jdbc:sqlite:data/desktop.db。连接池要注意HikariCP 对 SQLite 的兼容性一般可以利用maximumPoolSize1因为 SQLite 同一时间只允许一个写连接多连接并发写会直接报 database is locked。另一个大坑是 SQLite 对事务和并发读写的锁机制桌面应用会边查边写造成 UI 线程卡顿。通用的方案是让耗时查询走子线程JavaFX 的 UI 更新再通过Platform.runLater回主线程这个在避坑章节再展开讲。4. 集成高频踩坑记录现象、原因、解决办法4.1 启动后界面一闪而过进程直接消失现象双击启动窗口闪了一下就没了控制台也没报错或者报错被吞了。原因最常见的是FXMLLoader.load()在加载 FXML 时抛了异常而异常发生在start()方法里JavaFX 默认把异常打印到控制台如果用户是双击 jar 包启动没有控制台异常信息直接丢失看起来就像程序崩了。解决开发阶段一定用 IDEA 启动并看控制台打包后建议在start()最外层包一层try-catch把异常写进日志文件。还有一类可能是init()里 Spring 容器启动失败ConfigurableApplicationContext没初始化完getBean拿到 null同样表现为闪退。这两个问题排查方向不同先看日志再定位。4.2 Controller 里的 Service 是 null现象FXML 界面加载出来了一点按钮就抛NullPointerException报错的字段全是 Service。原因FXML 文件里的fx:controller被 FXMLLoader 自己反射创建了Spring 容器根本没有参与所以Autowired的 Service 全是 null。解决检查SpringFxmlLoader是否传给FXMLLoader了setControllerFactory另外确认SpringFxmlLoader的load()方法用的是同一个 FXMLLoader 实例不要某处直接new FXMLLoader()加载同一个 FXML那会绕过 Spring。4.3 fxml 里的参数、场所属性全是文字运行却中文乱码现象FXML 文件里写了中文运行时显示乱码。原因FXMLLoader按 UTF-8 读取 FXML 文件但如果文件本身是 GBK 编码Windows 上常见或者构建工具没有把编码统一就会乱码。解决所有.fxml、.properties文件在 IDEA 里统一设置为 UTF-8 编码Maven 项目的pom.xml里加project.build.sourceEncoding为 UTF-8如果实在遇到历史文件可以显式指定loader.setCharset(StandardCharsets.UTF_8)但治本还是统一编码。4.4 打包后要么没有界面要么连不上数据库现象本地 IDEA 跑得好好的打 fat jar 后运行要么界面加载不出 fxml要么提示找不到数据库驱动。原因资源路径问题。IDEA 运行时的 classpath 是target/classes能直接找到fxml目录而 fat jar 里资源路径在 jar 内部如果 FXML 路径写的是fxml/MainView.fxml少了/前缀会找不到资源。数据库驱动问题通常是pom.xml里依赖标了runtime或providedscope打包时没带上。解决FXML 路径统一以/开头镜像打包配置里让spring-boot-maven-plugin的includes包含所有 runtime 依赖数据库驱动放宽 scope 为默认 compile。打了包建议先用java -jar app.jar测试再看窗口是否正常。4.5 窗口关闭后 JVM 进程不退出任务管理器里一堆残留现象点窗口右上角的 X界面关了但 JVM 进程一直在内存里还占着端口或数据库连接。原因JavaFX 关闭窗口时默认只调用Platform.exit()但 Spring 容器的非守护线程比如 HikariCP 的 HouseKeeper 线程还在运行JVM 不会自动退出。解决在Application类的stop()方法里调用springContext.close()还会顺带关闭数据源连接池如果某些自定义线程还挂着建议在实现DisposableBean或PreDestroy时做好收尾。5. 进阶技巧这玩意儿怎样做得顺手又稳定JavaFX 的界面线程模型和 Swing 一样是单线程的。所有 UI 操作必须在 Application 线程上执行但数据库查询会在 MyBatis 里做磁盘 IO直接放 UI 线程会卡界面。常见的做法是给 Service 方法包一层任务让它跑在子线程里查询完成后再切回 UI 线程更新控件。// 查询按钮的事件处理方法 FXML private void onSearchClick() { // 参数从界面取DatePicker 的值要转成字符串再传 String keyword keywordInput.getText(); TaskListUser queryTask new Task() { Override protected ListUser call() { // 这段代码在子线程执行Service 里查数据库不会卡界面 return userService.search(keyword); } }; queryTask.setOnSucceeded(event - { ListUser result queryTask.getValue(); // 这里回到 UI 线程可以安全操作 TableView userTable.getItems().setAll(result); }); Thread thread new Thread(queryTask); thread.setDaemon(true); thread.start(); }这里有个边界问题new Thread()每次查询都新建线程高频操作时开销大。更精细的做法是交给通用的线程池JavaFX 也提供了ExecutorService的集成方式。setDaemon(true)也很要紧否则关闭窗口后子线程还会阻止 JVM 退出spring 容器已经关了但线程还在跑问题甚至比stop()没调用更隐蔽。验证这套项目是否健康我习惯做三件事第一启动后看控制台 SQL 日志确认 Mapper 位置和参数绑定正确第二用jstack pid看线程状态确认查询子线程没有泄漏、UI 线程没有被数据库操作占住第三连续 50 次保存操作每次启动后都执行配置事务回滚测试确认rollbackFor正常。有回滚的场景可以在 Service 里故意抛个异常看看数据是否真的回滚这是测试事务边界最实用的手段。回想我第一次把三件套集成起来的那次在ControllerFactory和stop()上各耗掉一天最后发现全是生命周期和线程边界的问题。这个组合本身不难难在理解两条生命周期线的交汇点以及资源统一由 Spring 管理的纪律性。如果用这套方案做事建议先把最小的骨架跑通再谈界面美化和业务扩展。希望帮到你。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/15 17:07:19
3Dmol.js 分子可视化:从压缩包到浏览器 3D 模型实战
2026/10/15 17:07:19
ESP8266 NONOS SDK三大致命错误排查指南
2026/10/15 17:07:19
并行区一进就停在 libiomp5md.dll,Intel OpenMP 运行时怎么对安装包
2026/10/15 17:57:41
mycroft-core 自然语言格式化模块 mycroft.util.format 完全指南:数字、时间与日期的人性化输出
2026/10/15 17:57:41
CLRS《算法导论》10.4 节习题精讲:有根树的表示与 O(n) 遍历全解析
2026/10/15 17:57:41
人脸图像base64编解码全链路实战:从拍照到AI可用File对象
2026/10/15 17:57:41
均衡器源码实战:双二阶滤波器系数与频段编排避坑指南
2026/10/15 17:57:40
招聘这件事,已经从“人找简历”变成“简历找人”
2026/10/15 17:52:40
openEuler移植RK3399实战:内核编译与资源镜像制作指南
2026/10/15 0:02:59
AI Coding时代的柔性之美:从静态软件到自演化软件,软件研发正在重新进化
2026/10/15 0:02:59
亨得利客户服务中心官方网点分布一览:2026年全国多城门店地址与最新联系电话公布
2026/10/15 0:02:59
防关联浏览器哪个好?三款产品实测差异解析
2026/10/13 12:25:09
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/14 18:35:19
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/14 14:52:59
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/15 1:07:55
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/15 5:07:59
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/15 8:10:01
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)