资讯详情

资讯详情

JavaFX与Spring整合实践:构建稳定高效的桌面应用架构

简介一套基于 JavaFX 与 SSMSpring SpringMVC Mybatis构建的完整 ERP 系统项目适合正在学习 Java 桌面应用开发或希望了解企业级框架前后端整合的开发者。压缩包内共包含 2 个文件1 个 7z 项目压缩包和 1 个 SQL 数据库脚本整体大小约 75.26MB7z 文件里装的是 JavaFX 界面代码、FXML 布局描述、CSS 样式以及 SSM 后端业务实现SQL 脚本则用于初始化 ERP 系统所需的数据库表结构和基础数据。目前已有 3413 人学习/下载项目在 UI 设计和框架集成方面都有不少值得借鉴的写法。通过这个项目可以深入理解 Spring 依赖注入与事务管理、SpringMVC 请求调度与控制器编写、MyBatis 持久层映射等后端技能也能掌握 JavaFX 数据绑定、TableView 表格展示、表单数据校验、FXML 界面布局和 CSS 美化等实战技巧。无论用于课程设计、毕业设计还是作为进入企业级开发前的练手项目这套 ERP 实现都具备很高的参考价值。 接手过一个内部数据管理工具的项目需求不复杂给业务部门做一个 Windows 桌面客户端用来查 MySQL 数据、做手工录入、导出 Excel 报表。当时团队里有人建议直接上 Web 管理系统但真到使用场景里就发现不对——内网离线环境、业务人员要批量导入 Excel、还得接本地扫码枪这些交互在浏览器里做起来始终差口气。于是最终把技术栈定成了 JavaFX 写界面层后端用 Spring SpringMVC Mybatis 做支撑。这三个词放到一起网上搜出来的大概率是 SSM 的 Web 整合教程但真正把 JavaFX 容器和 Spring 容器捏在一起、并且能稳定跑上两年的完整项目案例说实话很少。这篇就按我实际搭建这套架构的路线把最关键、最容易翻车的几个环节拆开讲透适合正在用 JavaFX 做桌面应用、又不想放弃 Spring 依赖注入和 Mybatis 数据访问能力的人参考。1. 桌面应用为什么要背这套重武器1.1 真实场景复盘一个内网数据管理工具先说说项目背景。这个工具的原始版本是纯 JavaFX 写的所有 Controller 里 new Service、Service 里 new Dao登录逻辑、数据校验、权限判断全揉在一个类里。做到第三个月一个 Controller 就有两千行每次加需求都要先花半天理清楚改哪里两个人维护都吃力。换成 Spring SpringMVC Mybatis 之后最直观的变化是对象创建不再由自己管了。Controller 里想要哪个 Service一个 Autowired 就进来想要谁的实例Spring 容器统一管理单例数据访问统一走 Mybatis 的 Mapper 接口不需要手工写一堆 JDBC 样板代码。再加上 Spring 的声明式事务代码量肉眼可见地降了下来。所以我的观点很明确如果你的 JavaFX 项目超过三个界面、有独立的数据库访问、有权限或用户体系就值得上这套架构。桌面工具不等于小工具逻辑复杂到一定程度控制反转和数据层框架带来的收益会远远大于初始搭建成本。1.2 SpringMVC 在这里扮演的角色不是 Web 框架很多人一听 SpringMVC 就以为是起 Web 服务其实放在 JavaFX 项目里它并不是用来接收 HTTP 请求的。我在这套架构里真正用到的是它的分层思想界面响应由 Controller 层负责业务规则写在 Service 层数据落在 Mapper 层。View 对应的 FXML 只做布局和样式控件的点击事件全部转发给 Controller。这个分层带来的好处是界面改动不影响业务逻辑。比如后来客户要求把查询结果表格从 TableView 换成自定义控件我只动了 FXML 和对应的 Controller 初始化代码Service 和 Mapper 一层没碰。如果你做过纯 JavaFX 开发应该能体会到这种改界面不动业务的奢侈感。2. 容器集成把 JavaFX 的生杀大权交给 Spring2.1 自定义 ControllerFactory 破解双容器问题JavaFX 和 Spring 整合最大的坎在于两个框架各自有一套对象创建机制。JavaFX 的 FXMLLoader 加载 FXML 时如果某个根节点或属性指定了 fx:controller它会通过反射调用无参构造器创建 Controller。Spring 则希望所有 Bean 都由 ApplicationContext 统一管理这样才能注入依赖。这两套机制不打通就会出现一个很经典的错误FXML 里声明的 Controller 是由 FXMLLoader 创建的其内部的Autowired字段全是 null。第一次跑的时候看到空指针我还以为是配置没加载排查了半天才发现根本不是生命周期的问题。解决办法是给 FXMLLoader 设置一个自定义的 ControllerFactorypublic class SpringFXMLLoader { private static final ApplicationContext CONTEXT SpringContextHolder.getContext(); public static Parent load(String fxmlPath) throws IOException { FXMLLoader loader new FXMLLoader(SpringFXMLLoader.class.getResource(fxmlPath)); loader.setControllerFactory(param - CONTEXT.getBean(param)); return loader.load(); } }这里的核心逻辑就一行loader.setControllerFactory(param - CONTEXT.getBean(param))。FXMLLoader 需要创建 Controller 时不再自己 new而是去 Spring 容器里找对应的 Bean。前提是这些 Controller 类要加上Controller注解并且让 Spring 扫描到比如通过ComponentScan指定包路径。这一步打通之后FXML 里声明的 Controller 就等于交给了 Spring 管理Autowired、Value、PostConstruct这些注解在里面全部生效。我建议所有 Controller 的初始化逻辑不要写在构造函数里统一放到PostConstruct或initialize()方法中。因为 Spring 创建 Bean 时依赖还没完全注入完构造函数里拿任何注入字段都可能拿到 null。2.2 ApplicationContext 的生命周期管理细节ApplicationContext 的创建时机也有讲究。JavaFX 启动入口是Application.launch()内部会依次调用init()、start()、stop()。最稳妥的做法是在init()里初始化 Spring 容器因为init()是在 JavaFX Application Thread 之外执行的做一些耗时初始化不会卡启动界面。public class MainApp extends Application { private ConfigurableApplicationContext springContext; Override public void init() { springContext new AnnotationConfigApplicationContext(AppConfig.class); SpringContextHolder.setContext(springContext); } Override public void start(Stage primaryStage) throws IOException { Parent root SpringFXMLLoader.load(/fxml/LoginView.fxml); Scene scene new Scene(root, 1200, 800); primaryStage.setScene(scene); primaryStage.show(); } Override public void stop() { springContext.close(); } }这里有一个很多人忽略的坑start()里如果直接写在主线程里加载 FXML界面还没显示用户会看到一个空白窗口挂在那。更规范的做法是把登录窗口显示出来、主界面延迟加载或者在start()之前做好所有 Bean 初始化。实际项目中我是先显示一个极简的启动界面后台异步初始化数据字典和权限缓存初始化完成再跳转主界面。这个后面讲启动优化时细说。stop()里必须手动springContext.close()否则非守护线程和数据库连接池不会被正常回收关掉窗口后 JVM 可能还在后台挂着。这个问题在开发时不容易发现一旦做成安装包部署到客户机器上就会出现关掉了但进程没结束的投诉。3. 界面层的 MVC 落地Controller 拆分与统一路由3.1 一个 FXML 对应一个 Controller 的边界感SpringMVC 的 Controller 是一个请求一个方法而在 JavaFX 项目里我的实践是一个窗口或者一个功能模块对应一个 FXML一个 FXML 对应一个 Controller 类。不要试图做一个万能 Controller 去处理所有页面那样和不用框架没有区别。以我的项目为例模块划分大概是这样的界面模块FXML 文件Controller 职责登录页LoginView.fxml账号密码校验、记住密码、跳转主界面工作台MainView.fxml左侧导航、顶部用户信息、内容区路由数据录入DataEntryView.fxml表单校验、保存、批量导入入口查询报表ReportView.fxml条件组合、分页查询、Excel 导出Controller 之间需要通信时不要直接互相拿着对方的实例调方法。我在项目里引入了一个简单的 EventBusSpring 事件机制界面跳转、数据刷新、登录状态变更都通过事件广播。比如数据录入完成后发布一个DataSavedEvent查询报表界面监听这个事件自动刷新列表。这样模块之间完全解耦新增一个界面不影响现有逻辑。3.2 统一拦截、异常弹窗与操作日志Web 的 SpringMVC 有拦截器和全局异常处理器JavaFX 里同样需要这套机制。我的做法是定义了一个所有 Controller 都要继承的基类BaseController里面封装了三个东西第一是权限检查。每次界面切换前基类里的navigateTo()方法会检查当前用户是否有权限访问目标界面没有权限就弹提示框而不是等界面渲染完才发现按钮应该禁用。这个和 Web 端的拦截器是一个思路只是拦的入口从 URL 变成了界面路由。第二是全局异常弹窗。JavaFX 里如果某个事件处理抛了异常默认只会打印到控制台用户看到的是界面没反应。我在基类里给所有按钮事件统一包了一层异常处理任何 Service 层抛出的业务异常都会被捕获转换成友好的中文提示。Service 层只抛异常不弹窗弹窗是界面层的事这个边界要守住。第三是操作日志。每个关键操作通过 AOP 切面记录操作人、时间、操作内容。这个用 Spring AOP 很好实现定义一个LogOperation注解切面里拿到方法参数和当前登录用户写入日志表。桌面应用没有 Web 那种成熟的过滤器链但 AOP 切面一样能做到统一埋点。4. Mybatis 数据层的线程边界与会话管理4.1 SqlSession 与 JavaFX 线程模型的冲突JavaFX 有一条硬性规矩UI 控件只能在 JavaFX Application Thread 中修改。所以耗时的数据库查询绝不能放在这个线程里否则界面直接卡死。但这就引出一个问题Mybatis 的 SqlSession 默认不是线程安全的每个线程都要有独立的 SqlSession。如果你用的是MapperScan扫描 Mapper 接口并且 Mybatis 与 Spring 整合Spring 会把 SqlSession 绑定到当前事务或线程上正常情况不会有问题。但我在实际项目里踩过一个坑在后台线程里执行查询时开启了 Spring 声明式事务的 Service 方法一切正常而一个没有加Transactional的查询方法偶尔会报SqlSessionException: SqlSession is not available。原因排查下来是 Mybatis-Spring 的 SqlSessionTemplate 在无事务场景下会从 SqlSessionUtils 获取一个自动提交的 SqlSession而 JavaFX 的并发模型导致某些自定义线程池里的线程没有正确绑定。解决办法有两个方向一是所有数据库操作都走Transactional标注的 Service 方法强制走事务管理器二是把自定义线程池交给 Spring 管理由TaskExecutor统一创建线程确保线程上下文完整。4.2 事务边界后台任务要单独开事务桌面应用的事务边界和 Web 有明显区别。Web 请求有明确的开始和结束事务跟着请求走JavaFX 里一个按钮点击就是一次请求但按钮点击之后可能还有异步回调事务边界要自己设计清楚。我的设计原则是一个用户可见的操作对应一个事务边界。比如导入 Excel整个导入过程包括解析文件、逐行校验、分批插入、统计结果这些逻辑全部放在一个Transactional的 Service 方法里任何一个步骤失败就整体回滚。但是导入过程要在后台线程执行不能阻塞 UI所以我会用一个进度条任务去调用这个 Service。这里有一个细节很值得说不要让 JavaFX 的 Task 直接调用 Service 方法后自己再做数据库操作。Task 只负责调度和更新 UI所有数据访问必须在 Service 层完成。否则事务注解是不会生效的——Spring AOP 基于代理内部方法调用无法触发事务。我们项目里不少同事写过这种代码// 错误写法Task 里直接调 mapper task.setOnSucceeded(e - orderMapper.updateStatus(order.getId())); // 正确写法Service 方法承载事务 Service public class OrderService { Transactional public void updateStatus(Long id, int status) { orderMapper.updateStatus(id, status); } }顺带提一下数据源配置。桌面应用我推荐用 HikariCP 搭配 MySQLmaximumPoolSize不要设置太大桌面端并发有限10 个连接足够用了。设置过大的池反而会让数据库端出现大量空闲连接加上connectionTimeout和idleTimeout的合理配置能避免长时间运行后连接池耗尽的问题。5. 踩过的坑、性能调优与打包发布5.1 三个印象深刻的翻车现场第一个是ClassNotFoundException: javafx.fxml.FXMLLoader。开发环境跑得好好的打成 jar 包在客户机器上就报这个错。这是因为 Java 8 之后 JavaFX 不再随 JDK 分发需要单独使用 jlink 或 jpackage 生成包含 JavaFX 模块的自定义运行时。如果你还在用传统的java -jar方式打包在没装 JavaFX 的机器上必然翻车。这里我直接采用了 jpackage 打安装包它会自动把 JavaFX 模块带进目标镜像里客户机器不需要预装任何环境。第二个是 FXML 里fx:controller与 Spring 容器双重管理导致的重复实例。有些同事图省事在 FXML 里写着 controller 类名又在代码里手动context.getBean()拿同样的类做其他用途结果界面上操作的和逻辑里用的是两个不同的对象。规范做法是任何 Controller 只能通过 FXMLLoader 的 ControllerFactory 获取代码里不允许再直接 getBean 同一个 Controller 类。第三个是 Mybatis 懒加载在 JavaFX 环境下的无效。aggressiveLazyLoading配置对普通 Web 项目没问题但在桌面端某些场景下延迟加载的代理对象被序列化或跨线程访问时会报错。我最终的配置是关闭懒加载关联查询全部写在 Mapper XML 里用 join 或嵌套查询一次性查出这样逻辑清晰而且不会踩代理的坑。5.2 启动加速与内存占用控制JavaFX Spring Mybatis 这套组合最大的槽点就是启动慢。我做了一次针对性优化把启动时间从 8 秒降到了 3 秒以内主要做了三件事第一Spring 容器扫描包路径收窄。不要用ComponentScan(basePackages com.company)扫整个项目精确到具体的 controller、service、mapper 包能省下不少类加载时间。第二延迟初始化非核心 Bean。配置类上加Lazy让一些预加载缓存、报表服务等不常用的 Bean 在真正使用时才初始化。登录之后用户第一眼看到的工作台用到的 Bean 全部提前初始化其余按需启动。第三FXML 资源预加载。用户停留登录页时后台线程把主界面的 FXML 和对应 Controller 提前加载并缓存登录成功后直接替换 stage 的 scene省去了等待时间。内存方面桌面应用要留意 TableView 大量数据渲染时的内存峰值我的做法是查询分页加虚拟化渲染一次最多取出 500 行而不是一次性装几千行数据到内存里。5.3 最终的项目目录建议如果你要搭一个类似的完整项目我建议按下面的包结构组织这个结构经过了两个项目的验证团队接手成本很低com.company.app ├── MainApp.java // JavaFX 启动入口 ├── config │ ├── AppConfig.java // Spring 配置类 │ ├── DataSourceConfig.java // 数据源与事务配置 │ └── MybatisConfig.java // SqlSessionFactory 与 Mapper 扫描 ├── controller // JavaFX 界面控制器 ├── service // 业务层接口 实现 ├── mapper // Mybatis Mapper 接口 ├── model // 实体与 DTO ├── fxml // 界面布局文件 ├── event // 自定义事件类 ├── common │ ├── SpringFXMLLoader.java // 接管 FXML 加载 │ ├── BaseController.java // 控制器基类 │ └── SpringContextHolder.java // 提供全局 ApplicationContext还有一个容易被忽略的点日志。桌面应用的日志不能只打到控制台因为客户机器上没有控制台。我把 logback 配置成了滚动文件输出按天归档保留最近 30 天。客户报软件出错了的时候让运维把日志目录发过来比对时间戳马上就能定位问题这个习惯帮我省了太多沟通成本。回到开头那个问题桌面应用到底值不值得背 Spring 全家桶这套重武器做了两个项目之后我的体会是值不值得取决于项目的生命周期和维护预期。如果你只是做一个三天交付的小工具纯 JavaFX 反而更快但只要是正经的业务系统有权限、有数据、有报表、有后续迭代Spring 的依赖注入、Mybatis 的数据访问能力和事务管理带来的收益是长期的。这套架构真正难的部分不在代码而在第一次把两个容器的边界理清楚。对照这篇的顺序把 ControllerFactory、事务边界、线程模型这几个点打通后面就是按部就班地堆功能了。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →