资讯动态

SpringBoot3整合JavaFx与MyBatisPlus:桌面端开发省事指南

发布时间:2026/10/18 16:55:12 来源:尧图企业网站定制
简介这份资源是面向Java后端与桌面应用开发者的SpringBoot3整合JavaFx与MyBatisPlus实战项目源码适合希望打通富客户端界面与数据持久层、构建跨平台企业级应用的中级开发者学习参考。压缩包共79个文件约110.76MB包含16个java业务源码、12个fxml界面布局、19个class编译产物、7个xml与2个yml配置、1个sql建表脚本以及png、jpg、ico等界面素材和mp4演示视频完整呈现从界面到数据访问的工程结构。项目以SpringBoot3为基座借助自动配置简化JavaFx与MyBatisPlus的集成前端用JavaFx构建现代化交互界面后端通过MyBatisPlus的通用CRUD、条件构造器与分页插件高效操作数据库。已有2422人学习下载读者可据此掌握依赖引入、场景启动器配置、数据源整合与前后端业务代码编写思路快速搭建可运行的整合脚手架。1. 桌面端不想写两套代码SpringBoot3 整合 JavaFx MyBatisPlus 到底能省多少事很多做后台管理系统的团队都遇到过这个场景业务逻辑已经用 SpringBoot3 写好了MyBatisPlus 把 CRUD 也封装得七七八八结果产品突然说“给我做个桌面客户端要能离线用还要直连本地数据库”。这时候如果重新用 Electron 或者 WPF 写一遍等于把 Service 层再抄一次后面改个字段两边不同步维护成本直接翻倍。SpringBoot3 整合 JavaFx MyBatisPlus 这个组合解决的就是这个问题——让 Spring 容器直接跑在 JavaFx 应用里Service、Mapper、事务全部复用桌面端只负责画界面。适合谁适合已经有一套 SpringBoot 后端、需要快速补一个桌面壳的团队或者想用 Java 一套技术栈同时覆盖服务端和客户端的独立开发者。它不是银弹但能把“写两套业务逻辑”这件事直接砍掉。2. 把 Spring 容器塞进 JavaFx启动顺序和线程模型先理清2.1 为什么不能直接在 JavaFx 的 start 方法里 new 一个 SpringApplicationJavaFx 的入口是Application.start(Stage)这个方法跑在 JavaFx Application Thread 上。如果你在这个线程里直接调SpringApplication.run()Spring 启动过程中的类加载、Bean 初始化、数据库连接池创建全部会阻塞 UI 线程表现就是窗口白屏几秒甚至十几秒用户以为程序卡死了。更麻烦的是Spring 默认的WebApplicationType如果是 SERVLET还会去起 Tomcat桌面端根本不需要。常见做法是在main方法里先启动 Spring 上下文拿到 Bean 之后再调Application.launch()。这样 Spring 的启动发生在 JavaFx 线程之外UI 线程只负责渲染。// MainLauncher.java public class MainLauncher { public static void main(String[] args) { // 先构建 Spring 上下文不启动 Web 容器 SpringApplication app new SpringApplication(AppConfig.class); app.setWebApplicationType(WebApplicationType.NONE); ConfigurableApplicationContext context app.run(args); // 把上下文挂到静态持有类供 JavaFx Controller 取 Bean SpringContextHolder.setContext(context); // 再启动 JavaFx此时容器已就绪 Application.launch(JavaFxApplication.class, args); } }这段代码的关键在WebApplicationType.NONE它告诉 SpringBoot3 不要初始化 Servlet 环境省掉 Tomcat 和 DispatcherServlet 的启动开销。SpringContextHolder是一个简单的静态类内部持有ConfigurableApplicationContext提供getBean(ClassT)方法。参数上唯一需要注意的是args要透传给Application.launch否则 JavaFx 的命令行参数会丢。2.2 JavaFx Controller 里怎么拿到 Service BeanJavaFx 的 FXML 加载器默认用无参构造器创建 Controller这意味着 Controller 本身不受 Spring 管理Autowired不会生效。有两种可靠做法一是把 Controller 也注册成 Spring Bean然后用FXMLLoader.setControllerFactory()指定从 Spring 容器取二是 Controller 里不写Autowired通过SpringContextHolder.getBean()手动取。第一种更干净推荐。// 在加载 FXML 时指定 Controller 工厂 FXMLLoader loader new FXMLLoader(getClass().getResource(/fxml/main.fxml)); loader.setControllerFactory(SpringContextHolder.getContext()::getBean); Parent root loader.load();setControllerFactory接收一个CallbackClass?, Object这里直接用 Spring 上下文的getBean方法引用。这样 FXML 里fx:controller指定的类会从 Spring 容器拿实例Autowired的 Service、Mapper 全部正常注入。注意 Controller 类上要加Component或Controller并且包路径要在ComponentScan范围内。2.3 线程模型数据库操作不能堵住 UI 线程即使 Spring 容器启动没问题运行时的数据库查询如果直接在 JavaFx 事件处理方法里调一样会卡界面。MyBatisPlus 的selectList走 JDBC 是阻塞的网络慢或者数据量大时 UI 直接无响应。标准做法是把耗时操作丢到后台线程用Task或CompletableFuture包装完成后通过Platform.runLater回 UI 线程更新控件。// 在 Controller 中异步查询并更新 TableView FXML private void onSearchButtonClick() { String keyword searchField.getText(); TaskListUserVO task new Task() { Override protected ListUserVO call() { // 这里跑在后台线程可以安全调 Service return userService.searchByKeyword(keyword); } }; task.setOnSucceeded(e - { // 回到 UI 线程更新表格 tableView.getItems().setAll(task.getValue()); }); task.setOnFailed(e - { Throwable ex task.getException(); alertService.showError(查询失败: ex.getMessage()); }); new Thread(task).start(); }Task的call方法在后台线程执行setOnSucceeded的回调默认在 JavaFx Application Thread 上执行所以可以直接操作tableView。参数上注意new Thread(task).start()这种方式每次查询起一个线程高频操作下建议换成有界线程池避免线程数失控。alertService是自定义的弹窗封装内部同样要用Platform.runLater包一层。3. MyBatisPlus 在桌面端的配置数据源、分页和事务边界3.1 桌面端数据源选型HikariCP 还是 SQLite 内嵌桌面端和服务器端最大的区别是数据库可能就在本机也可能是远程 MySQL。如果业务要求离线可用常见做法是 SQLite 作为本地库MyBatisPlus 照样能用只是分页插件要换成 SQLite 方言。如果还是连远程 MySQLHikariCP 的maximumPoolSize不要设太大桌面端并发低设 5 到 10 就够设大了反而浪费连接资源。# application.yml spring: datasource: url: jdbc:sqlite:./data/app.db driver-class-name: org.sqlite.JDBC hikari: maximum-pool-size: 5 minimum-idle: 1 connection-timeout: 3000 mybatis-plus: global-config: db-config: id-type: auto configuration: map-underscore-to-camel-case: trueSQLite 的 JDBC URL 是jdbc:sqlite:加文件路径./data/app.db表示程序运行目录下的 data 文件夹。maximum-pool-size: 5对桌面端足够connection-timeout: 3000让连接失败快速暴露而不是卡住 UI。map-underscore-to-camel-case开启后数据库的user_name会自动映射到实体类的userName省掉手动TableField注解。3.2 分页插件在 JavaFx 表格里的实际用法MyBatisPlus 的分页插件在 Web 端通常配合PageHelper或者自带IPage桌面端一样用IPage只是分页参数来自 JavaFx 的 Pagination 控件。注意分页插件必须注册成 Bean否则selectPage不会真正拼 LIMIT。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 根据数据库类型选方言SQLite 用 SQLiteDialect interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQLITE)); return interceptor; } }PaginationInnerInterceptor的构造参数DbType.SQLITE决定了生成的 LIMIT 语法。如果连的是 MySQL 就换DbType.MYSQL写错了分页 SQL 会报语法错误。注册完这个 Bean 之后Service 里调userMapper.selectPage(new Page(current, size), wrapper)才会返回带total和records的IPage对象。// 在 Controller 中对接 JavaFx Pagination 控件 FXML private Pagination pagination; private void loadPage(int pageIndex) { // JavaFx 的页码从 0 开始MyBatisPlus 从 1 开始 PageUserVO page new Page(pageIndex 1, 20); IPageUserVO result userService.page(page, null); tableView.getItems().setAll(result.getRecords()); pagination.setPageCount((int) result.getPages()); }这里有个容易翻车的点JavaFxPagination的pageIndex从 0 开始而 MyBatisPlus 的Page当前页从 1 开始所以必须pageIndex 1。忘了加这个 1第一页数据永远查的是第二页用户看到的就是“第一页少了一条”。setPageCount要在数据加载后调用否则分页控件不知道总页数。3.3 事务在桌面端要不要开怎么开桌面端如果只是单机 SQLite事务的意义主要是保证批量操作的原子性比如导入 Excel 时一批 insert 要么全成功要么全回滚。Spring 的Transactional在桌面端照样生效前提是方法调用经过 Spring 代理。也就是说Controller 里注入的 Service 是代理对象直接调this.saveBatch()这种自调用不会触发事务。Service public class UserService { Autowired private UserMapper userMapper; Transactional(rollbackFor Exception.class) public void importUsers(ListUserVO list) { for (UserVO vo : list) { userMapper.insert(vo); } // 如果某条插入失败整个方法回滚 } }rollbackFor Exception.class确保受检异常也回滚默认只回滚RuntimeException。桌面端 SQLite 的事务隔离级别默认是 SERIALIZABLE批量插入时性能会受影响如果数据量大可以考虑分批提交每 500 条一个事务。注意Transactional方法必须是public且不能是private或protected否则代理不生效。4. 避坑与排查桌面端整合最容易翻车的五个点4.1 现象启动时报 “NoClassDefFoundError: javafx/application/Application”原因Java 11 之后 JavaFx 从 JDK 里剥离了如果用的是 OpenJDK 或者没有单独引入 JavaFx 依赖运行时找不到类。解决在 Maven 里显式引入javafx-controls和javafx-fxml并且注意 classifier 要匹配操作系统。dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version21.0.2/version /dependency dependency groupIdorg.openjfx/groupId artifactIdjavafx-fxml/artifactId version21.0.2/version /dependency如果打包成 fat jar 后还是报错检查module-info.java是否存在SpringBoot3 和 JavaFx 的模块化混用容易出问题常见做法是干脆不用 module-info走 classpath 模式。4.2 现象界面能显示但所有Autowired都是 null原因FXML 加载时没有设置controllerFactoryJavaFx 自己 new 了 ControllerSpring 没参与。解决确认FXMLLoader调了setControllerFactory并且 Controller 类上有Component。另一个隐蔽原因是 Controller 类不在ComponentScan的包路径下比如主类在com.example.appController 在com.example.ui而SpringBootApplication默认只扫主类所在包及其子包com.example.ui不在范围内。解决是把主类往上提一层或者手动加ComponentScan(com.example)。4.3 现象SQLite 插入中文乱码原因SQLite 默认编码是 UTF-8但 JDBC 连接串没指定编码时某些驱动版本会用平台默认编码。解决在 URL 后加?encodingUTF-8或者确认 SQLite JDBC 驱动版本较新。另外 JavaFx 的TextField取出来的字符串本身是 Unicode问题一般出在 JDBC 层。4.4 现象打包后运行报 “Unable to open nested jar file”原因SpringBoot 的 fat jar 机制和 JavaFx 的 native library 加载冲突JavaFx 需要从 jar 里解压出 dll 或 so 文件但 SpringBoot 的LaunchedURLClassLoader不支持这种嵌套读取。解决用spring-boot-maven-plugin的layout设为ZIP或者改用javafx-maven-plugin单独打包。更稳妥的做法是用jlink生成自定义运行时镜像把 JavaFx 模块直接编进去彻底绕开 fat jar 的类加载问题。4.5 现象关闭窗口后进程不退出原因Spring 容器里的线程池、数据库连接池、定时任务没有随窗口关闭而销毁。JavaFx 的Platform.exit()只关 UI不关 Spring 上下文。解决在Application.stop()方法里调SpringContextHolder.getContext().close()并且确保 HikariCP 的shutdown被触发。如果用了Scheduled定时任务还要加PreDestroy或者实现DisposableBean来取消任务。Override public void stop() throws Exception { // 先关 Spring 上下文释放连接池和线程池 ConfigurableApplicationContext ctx SpringContextHolder.getContext(); if (ctx ! null ctx.isActive()) { ctx.close(); } super.stop(); }stop()是 JavaFxApplication的生命周期回调窗口关闭时触发。ctx.close()会依次销毁所有单例 Bean触发PreDestroyHikariCP 的close()会被调用连接释放。如果漏了这一步任务管理器里会残留 java 进程用户以为程序没关干净。5. 进阶用 FXML 加 CSS 做可维护的桌面界面以及一个验证整合是否成功的检查清单5.1 把界面逻辑从 Controller 里抽出去Controller 里堆太多if-else和控件操作后面改一个按钮位置都要动 Java 代码。更可维护的做法是 FXML 定义结构CSS 定义样式Controller 只处理事件绑定和数据填充。比如表格的列宽、字体、颜色全部写在 CSS 里Controller 里只保留tableView.getItems().setAll(...)这类数据操作。/* app.css */ .table-view .column-header { -fx-background-color: #2d2d2d; -fx-text-fill: #e0e0e0; } .table-row-cell:odd { -fx-background-color: #f5f5f5; }FXML 里通过stylesheets../css/app.css引入这样换主题只需要改 CSS 文件不用重新编译 Java。参数上注意 CSS 路径是相对于 FXML 文件的../css/app.css表示上一级目录下的 css 文件夹。5.2 验证整合是否成功的四个检查点第一个检查点Spring 上下文启动后context.getBean(UserService.class)能拿到实例且userService内部的userMapper不为 null。第二个检查点FXML 加载后Controller 里的Autowired字段有值可以通过在initialize()方法里打印userService ! null来确认。第三个检查点调一次selectPage看返回的IPage里total和records是否符合预期确认分页插件生效。第四个检查点关闭窗口后用jps命令看进程是否消失确认 Spring 上下文和连接池正确关闭。// 在 Controller 的 initialize 中做自检 FXML public void initialize() { // 检查 Spring 注入是否成功 if (userService null) { throw new IllegalStateException(UserService 未注入检查 ControllerFactory 配置); } // 检查分页插件是否生效 IPageUserVO testPage userService.page(new Page(1, 1), null); if (testPage.getTotal() 0) { throw new IllegalStateException(分页插件未生效检查 MybatisPlusInterceptor 注册); } }initialize()是 JavaFx Controller 的生命周期方法FXML 加载完成后自动调用。在这里做自检的好处是问题暴露在启动阶段而不是用户点按钮时才报错。testPage.getTotal()如果返回 -1 或者 0 但数据库明明有数据说明分页拦截器没注册或者方言配错了。5.3 一个我踩过的坑别在initialize里做耗时查询早期我把用户列表的首次加载直接写在initialize()里结果窗口显示后要等两三秒表格才出数据期间界面完全空白。后来改成initialize()只做控件初始化和事件绑定数据加载放到Platform.runLater或者Task里异步执行窗口秒开数据随后填充。这个习惯一直保留到现在initialize里只放不耗时的逻辑任何涉及 IO 的操作一律异步。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑