ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaFX整合SpringBoot与MyBatis,桌面应用数据持久化工程化实践

JavaFX整合SpringBoot与MyBatis,桌面应用数据持久化工程化实践 简介SpringBootMyBatisJavaFX整合项目围绕档案管理中的扫描业务展开适合具备一定Java基础、想通过完整工程理解桌面端与后端数据交互的开发者学习。项目以SpringBoot作为服务端基座借助自动配置、起步依赖与内嵌Web容器免去繁琐XML配置即可快速搭建运行环境MyBatis作为持久层框架通过SQL映射文件实现数据库操作与Java代码解耦并利用Oracle数据库支持复杂查询、事务管理与权限安全完整保存档案扫描结果JavaFX承担客户端界面基于布局、事件监听和跨平台特性构建响应式操作页面。资源包共130个文件其中Java源文件定义实体与业务方法XML文件承载MyBatis映射与工程配置FXML文件规划界面结构Class文件为编译结果另有properties属性文件管理数据源参数整体压缩后仅146KB小巧便于下载研读。已有4198人学习目录结构清晰分层涵盖实体类、工具类、控制器、应用入口等模块。通过阅读该项目可以掌握SpringBoot自动配置原理、MyBatis动态SQL写法、JavaFX控制器与界面绑定方式还能理解Oracle数据库在桌面应用中的接入思路是一份适合课程设计或中小型档案管理系统的参考模板。 做Java桌面应用最怕什么不是界面丑而是数据这块完全没有工程化连接池要自己手写、SQL散落在各个按钮的监听器里、每次发版前手动改数据库脚本客户机器上装错一个MySQL版本能折腾一整天。我过去几年用JavaFX做了好几个管理类项目后来把SpringBoot和MyBatis整套搬进了桌面端这个组合一用就回不去了。这套东西解决的问题其实很明确JavaFX负责界面交互SpringBoot不再启动Web容器而是作为轻量级IoC容器负责Bean管理和自动配置MyBatis负责SQL映射和数据库操作。三者合在一起之后桌面应用也能享受到Web后端那种分层清晰、依赖注入、声明式事务的待遇。如果你是JavaFX开发者或者正在为桌面项目的数据持久化发愁又或者刚学完SpringBoot想找个非Web的落地场景这篇内容应该能帮你省下不少弯路。1. 为什么要把SpringBoot塞进JavaFX桌面应用1.1 桌面应用里最容易被忽视的工程化问题我见过不少JavaFX项目界面写得很精细但打开数据访问代码就是另一种画风DriverManager.getConnection()到处调用每个按钮的点击事件里直接拼SQL字符串事务要手动commit和rollback漏写一个finally关连接就是连接泄漏。小项目靠自觉还能撑住一旦业务逻辑复杂起来改一个字段名要全局搜索替换排查问题得在十几个类之间来回跳。这其实不是Java开发者的水平问题而是JavaFX生态默认的路子太原始了。Oracle官方案例教的是纯JDBCDemo可以这么写但真实业务系统必须有连接池、有SQL统一管理、有事务边界、有配置外部化。这些能力Spring生态早就成熟了只不过长期以来大家默认它们是Web后端专属没人把它搬回桌面场景。1.2 组合之后各自的角色划分在这个架构里三个组件的边界要划清楚否则代码会越写越乱组件职责类比JavaFX界面渲染、用户交互、场景切换餐厅的前厅负责接待SpringBootBean管理、配置加载、事务控制、依赖注入餐厅的后厨调度台MyBatisSQL编写、结果映射、参数绑定采购台账管好每笔数据进出SpringBoot在这里完全不启动Tomcat它只被当作一个轻量的应用上下文使用。spring-boot-starter这个基础依赖提供了自动配置能力MyBatis的SqlSessionFactory、DataSource事务管理器都能自动装配好。开发者拿到的是一个已经组装完毕的容器只需要关注业务逻辑本身。这套结构最适合三类场景给公司内部做的管理系统客户端、需要本地数据库支撑的单机业务工具、以及不想搞Web前端但又想用后端工程化思路的Java桌面项目。如果只是做个一次性小工具、数据量几十条不落库都行那就没必要引入这套东西农业社会不需要上重型工业设备。2. 搭建工程骨架时的版本与依赖取舍2.1 版本组合怎么选SpringBoot、JavaFX、MyBatis三个生态各自迭代版本选错是最早遇到的坑。我实测下来比较稳的组合是组合方案SpringBootJavaFXmybatis-spring-boot-starterJDK保守方案2.7.1817.0.82.3.28/11/17新特性方案3.2.x21.0.23.0.317如果项目已经上Java 17直接走SpringBoot 3.x JavaFX 21这套新组合没有历史包袱。如果团队还停留在JDK 8或者担心兼容性SpringBoot 2.7.18至今仍在维护搭配JavaFX 17非常稳。注意SpringBoot 3.x的javax包全部换成了jakarta代码习惯要跟着变桌面端项目遇到ClassNotFoundException十有八九是这个原因。2.2 pom.xml依赖清单pom.xml里要引入的核心依赖如下dependencies !-- SpringBoot基础能力不带Web容器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency !-- MyBatis整合包 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- JavaFX界面包 -- dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version17.0.8/version /dependency dependency groupIdorg.openjfx/groupId artifactIdjavafx-fxml/artifactId version17.0.8/version /dependency !-- 数据库驱动按实际选型 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies不需要引spring-boot-starter-web一旦引了内嵌Tomcat就会启动端口还会占用8005之类纯属多余。如果之前从Web项目改过来要记得把它从依赖列表里删干净包括传递依赖里可能带上的Web组件。2.3 包结构划分建议项目骨架我习惯这样分com.example.demo ├── Launcher.java // JavaFX程序入口带main方法 ├── DemoApplication.java // SpringBootApplication配置类 ├── config/ // JavaFX的Stage初始化、全局配置 ├── controller/ // FXML对应的控制器界面层 ├── service/ // 业务逻辑层 ├── mapper/ // MyBatis的Mapper接口 ├── entity/ // 数据库实体类 └── resources/ ├── application.yml // 数据源、MyBatis等配置 ├── mapper/ // Mapper XML文件目录 └── fxml/ // JavaFX界面文件这个结构和Web项目最大的区别是没有Controller接收HTTP请求的概念这里的controller是JavaFX的FXML控制器作用是把按钮点击、表格刷新这些UI事件转成业务调用。很多人刚入手时会混着来把Service写在控制器里最后还是返工。3. 核心难点让Spring容器接管JavaFX的应用生命周期3.1 别照搬Web项目的启动写法初学整合时最容易犯的错是直接在SpringBootApplication类里写main方法先用SpringApplication.run()启动Spring容器再手动调用Application.launch()启动JavaFX。两种启动方式在工作线程模型上完全不同SpringBoot会占用主线程做上下文刷新JavaFX的launch()要求在主线程调用两者撞车会导致界面启动卡顿甚至根本弹不出来。正确的打法是改造JavaFX本身的生命周期。javafx.application.Application类已经预留了三个钩子方法init()、start(Stage stage)、stop()天然适合承载Spring容器的初始化、界面的创建和资源的释放。3.2 Launcher类的完整写法public class Launcher extends Application { private ConfigurableApplicationContext context; public static void main(String[] args) { // 标准JavaFX入口 launch(args); } Override public void init() { // 在JavaFX线程启动前创建Spring容器 SpringApplicationBuilder builder new SpringApplicationBuilder(DemoApplication.class); builder.web(WebApplicationType.NONE); // 强制不启动Web容器 context builder.run(getParameters().getRaw().toArray(new String[0])); } Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader new FXMLLoader(getClass().getResource(/fxml/MainView.fxml)); // 关键让FXML中的控制器使用Spring管理的Bean loader.setControllerFactory(context::getBean); Parent root loader.load(); Scene scene new Scene(root); primaryStage.setScene(scene); primaryStage.setTitle(桌面管理工具); primaryStage.show(); } Override public void stop() { // 先关闭Spring容器再退出JavaFX context.close(); Platform.exit(); } }init()阶段创建Spring容器是刻意为之的。JavaFX的init()运行在非FX线程上Spring容器初始化中的数据库连接、连接池预热不会阻塞界面线程等到start()执行时所有Bean已准备完毕直接取用就行。WebApplicationType.NONE这个配置值得单独说一说它告诉SpringBoot不需要内嵌任何Servlet容器否则即使你没引starter-web某些自动配置还是可能触发Web环境初始化。3.3 Controller的依赖注入打通FXMLLoader.setControllerFactory(context::getBean)这行代码是整个整合的钥匙。FXML默认用反射创建Controller对象这样创建的Controller是孤儿Spring的依赖注入对它完全无效。设置了这个工厂方法之后fx:controller里声明的类名会被当作Bean名称去容器里查找。Controller public class MainController { Autowired private UserService userService; FXML private TableViewUser userTable; FXML private void handleQueryAction() { userTable.getItems().setAll(userService.queryAll()); } }注意这里的Controller是org.springframework.stereotype.Controller不是Web层那个注解它只是把控制器交给Spring管理从而让Autowired生效。FXML文件里照常写fx:controllercom.example.demo.controller.MainController加载时Spring容器会接管实例化过程。4. MyBatis在桌面应用里的落地配置4.1 数据源和连接池参数要收敛桌面应用不像Web服务有高并发压力连接池参数如果照搬服务器配置纯属浪费资源。application.yml里我一般这样配spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/desk_app?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root hikari: # 桌面端预留5个连接足够 maximum-pool-size: 5 minimum-idle: 1 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: trueminimum-idle: 1保证只有一个空闲连接常驻maximum-pool-size: 5是考虑到界面操作可能有多个窗口同时查询。这两个参数在单机场景下不用贪大实测用默认10也不会出问题但连接池维护开销明显桌面程序不划算。还有个实际体验问题HikariCP默认控制台不会打印SQL执行信息。开发期间可以在application-dev.yml里追加下面这段把Mapper接口的日志级别降到debugSQL和参数值就会出现在控制台logging: level: com.example.demo.mapper: debug线上或交付给客户时记得改回info否则控制台会被SQL刷屏日志文件也会迅速膨胀。4.2 SQLite还是MySQL这里有个分叉口桌面应用的数据存储选型是个值得单独聊的话题。如果软件给外部客户部署一台机器装一个MySQL实例运维成本立刻上去了升级、备份、密码管理全是麻烦。这时候SQLite是更好的选择它本身就是嵌入式数据库驱动替换一下就行spring: datasource: driver-class-name: org.sqlite.JDBC url: jdbc:sqlite:./data/app.dbMyBatis对SQL的差异主要在方言上SQLite不太支持复杂的分页和函数但CRUD是没问题的。我的习惯是内部自用工具用MySQL方便排查问题要对外交付的桌面产品用SQLite配合MyBatis的XML写一份通用SQL。这套架构好在切换数据源的成本极低配置改一下、SQL调一调其他代码不用动。4.3 声明式事务在桌面端依然好用SpringBoot自动配置了DataSourceTransactionManager所以Transactional在Service层照常生效Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void createOrder(Order order) { orderMapper.insert(order); stockMapper.deduct(order.getProductId(), order.getQuantity()); } }这段逻辑两个操作要么同时成功要么同时回滚。用纯JDBC写这个至少要三四行try-catch还不一定能保证异常类型覆盖完整。桌面场景的数据操作量大都是单事务、短事务声明式事务已经覆盖了95%的场景。有个容易被忽略的点Transactional只对Spring管理的Bean代理生效。如果你的调用链是Controller直接调另一个Mapper里的方法没有经过Service层事务是不会开启的。所以不管业务多简单数据操作统一走Service层这是一个纪律问题。5. 数据层和界面层的分工边界5.1 FXML控制器里不要写SQLJavaFX开发有个天然倾向——因为FXML控制器要管界面事件写着写着就把业务逻辑也塞进去了。我接手过一个项目一个Controller两千多行里面十几个方法直接操作JDBC改需求时没人敢动那个文件。SpringBootMyBatis这套组合天然逼着你分好层。控制器只负责两件事响应界面事件、把结果显示到界面组件上。查询和写入交给Service层Service层再调Mapper。这样Controller可以保持轻量每个方法就是一两行业务调用加上界面刷新逻辑。5.2 异步加载避免界面卡死数据库查询在桌面端同样要注意线程模型。JavaFX的FX Application Thread负责所有界面绘制和事件分发如果在普通事件处理方法里直接执行一个耗时SQL界面会整个冻结Windows标题栏上还能看到无响应。解决手段是TaskFXML private void handleQueryAction() { TaskListUser task new Task() { Override protected ListUser call() { // 在子线程执行数据库查询 return userService.queryAll(); } }; task.setOnSucceeded(event - { userTable.getItems().setAll(task.getValue()); }); Thread thread new Thread(task); thread.setDaemon(true); thread.start(); }call()在后台线程运行setOnSucceeded回调回到FX线程中刷新表格。这个模式做多了会发现需要同步等待结果的操作其实很少大多数据加载都可以做成异步的。JavaScript的前端开发者看到这套模型应该很眼熟它本质上就是事件驱动回调。6. 实测中遇到的高频坑与完整排查记录6.1 MapperScan没生效接口找不到实现症状是启动日志里打印了UserMapper接口存在但注入时报No qualifying bean of type UserMapper。排查路径是主配置类DemoApplication上没有加MapperScan注解或者加的位置不对。MapperScan的扫描路径必须覆盖到实际Mapper所在的包SpringBootApplication MapperScan(com.example.demo.mapper) public class DemoApplication { }另一个隐藏坑是启动类在com.example.demo但Mapper放在com.example.demo.sub.mapperMapperScan(com.example.demo.mapper)就扫不到了。如果项目分层深直接在扫描注解里写basePackages指定精确路径别图省事用通配符。6.2 启动时端口占用日志报Port 8080 was already in use很多从Web项目迁移过来的人会保留spring-boot-starter-web依赖。SpringBoot识别到Web环境就会尝试启动内嵌Tomcat即使没有Controller默认端口也会被监听。解决方式很直接去掉Web依赖或者在Launcher里明确设置WebApplicationType.NONE。两者我都试过后者更保险因为某些传递依赖可能悄悄引入Web组件。6.3 打包后运行报NoClassDefFoundError: javafx/application/Application这个坑集中在Maven打包环节。SpringBoot的spring-boot-maven-plugin默认会把Main-Class改成org.springframework.boot.loader.JarLauncher而JavaFX需要的是直接启动Application子类。解决方式是在插件里显式指定启动类plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.demo.Launcher/mainClass /configuration /plugin另外JavaFX的native库比如libglass.dylib、glass.dll这些依赖外部模块打fat jar时容易漏。稳妥的做法是用jpackage打原生安装包或者至少把JavaFX的modules一起带上。这块没处理好开发环境跑得再顺换台机器就是崩。6.4 关闭程序时Hikari连接池线程还挂着点击关闭窗口后进程不结束任务管理器里一直有Java进程。根因是Spring容器的关闭和JavaFX生命周期断开。必须保证stop()方法里先把context.close()调了让HikariCP的线程池正常销毁再Platform.exit()。顺序反了会出现线程残留看似退出实际卡死。最后再分享一个我自己的体会这套组合在开发效率和运行效率上的平衡点同类技术栈里确实少见。JavaFX本身是个成熟的桌面框架SpringBoot补齐了它的架构短板MyBatis让SQL维护变得有条理。后续如果想扩展还可以给Spring容器加上定时任务或者暴露一个本地HTTP端口供其他程序调用这都是在现有骨架上做加法的事底层不用再重构。用这套东西做了四五个工具之后我已经不想回到手写JDBC的时代了。本文还有配套的精品资源点击获取
返回列表