尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot+MyBatis+JavaFX:构建企业级桌面管理工具实战指南

Spring Boot+MyBatis+JavaFX:构建企业级桌面管理工具实战指南 简介SpringBootMyBatisJavaFX 综合项目源码包面向需要整合桌面客户端与服务端的 Java 开发者聚焦档案管理中的扫描功能。项目以 SpringBoot 的自动配置、起步依赖和内嵌式 Web 容器简化服务端搭建MyBatis 负责 SQL 映射、ORM 与事务控制JavaFX 借助组件库和事件监听构建交互界面并配合 Oracle 数据库实现档案数据的安全存储与复杂查询。包内共 130 个文件包含 24 个 Java 源文件、65 个 XML 映射/配置、4 个 FXML 界面布局、2 个 CSS 样式以及 properties、class 等编译产物压缩包仅 146KB结构紧凑。已有 4198 人学习下载适合对照源码快速梳理界面、业务与持久层的调用关系也可作为档案扫描类桌面应用的起步模板。 从“springbootmybatisjavafx”这三个词组合在一起很多人第一反应是“又是堆技术栈”。但如果你真在业务系统里摸爬滚打过会发现这个组合恰恰是当前很多企业内部工具、桌面管理和数据看板类项目的标准解法Spring Boot 负责把后端服务收拾利索MyBatis 把 SQL 控制权攥在手里JavaFX 则撑起一个跨平台的桌面界面三者各管一摊边界清晰。这篇内容我打算从一个实际做过资产台账管理客户端的角度把整个项目从架构设计、环境搭建、核心代码到打包部署的流程完整拆给你看也会把我在联调过程中踩过的坑、面试被问烂的几个点一并交代清楚。不管你是准备自己动手做毕设还是团队里要快速起一个带界面的内部工具这套思路应该都够你直接抄作业。1. 这个技术组合真正适合做什么1.1 三个技术栈的职责划分先说清楚这三样东西在项目里各干什么活不然框架一多就乱套。Spring Boot 是后端服务底座凡是和业务逻辑、权限控制、数据库打交道的事都放它这里。它解决的问题是让你不用再从零配置 Spring 容器、事务管理、Web 模块这些基础设施一个spring-boot-starter-web加进去内嵌 Tomcat 一启动REST 接口就能跑起来。MyBatis 是本项目的持久层框架它的定位很明确SQL 由你掌控。相比 Hibernate 或 Spring Data JPA 那种全自动 ORMMyBatis 更适合那些表结构复杂、SQL 需要手工调优的系统。动态 SQL 标签if、choose、foreach用熟了以后多条件组合查询写得非常顺手这也是很多人从 MyBatis 入门后就不太愿意换回 JPA 的原因之一。JavaFX 承担的是客户端界面。Oracle 早已把 JavaFX 从 JDK 中剥离现在通常用 OpenJFX 独立维护但它在桌面端的位置依然稳固。相比 Swing 的老态JavaFX 用 FXML 做界面布局、CSS 做样式、属性绑定机制做数据刷新写出来的界面要现代不少而且一套代码打好包后能跑在 Windows、macOS 和 Linux 上。1.2 适合落地的典型业务场景这个组合最适合的场景是那些“既要有服务端能力又需要一个桌面操作入口”的内部系统。举几个我实际接触过的例子企业内部设备资产管理系统管理人员用 JavaFX 客户端录入设备信息、变更记录、维修状态后端 Spring Boot 提供 REST APIMyBatis 操作 MySQL 里的设备表、部门表、操作日志表。药店或仓储进销存管理系统收银台、仓库管理端用 JavaFX 桌面程序数据汇总和服务端逻辑走 Spring Boot多个客户端可以连同一个后端。学校实验机房管理系统JavaFX 客户端做上机登记、课程排程展示后端处理并发请求和数据库事务。这类项目的共同特点是用户量不大几台到几十台机器、交互偏表单和表格、需要本地桌面体验同时后端服务可以被独立部署、方便后续扩展成小程序或 Web 端。用这套架构前端换成 Vue 或微信小程序都能直接对接现有接口。1.3 为什么不是 Web 前端 后端有同学会问都 Spring Boot 了为什么不直接做个 Vue 前端非要加 JavaFX 桌面端我的经验是在一些内网环境、老旧机房或保密性要求较高的场景里浏览器方案并不可靠。有的现场机器浏览器版本极低、插件限制严格、甚至不允许开外网访问前端静态资源但一个双击就能运行的 exe/jar 客户端就完全没有这些问题。JavaFX 程序可以直接通过 HTTP 访问内网里的 Spring Boot 服务部署简单、权限隔离也容易控制。另外就是面试或毕设的“可展示性”JavaFX 桌面界面的完成度比纯接口更能体现完整项目的开发能力这也是很多在校生热衷于这一组合的原因。2. Spring Boot MyBatis 后端搭建的核心环节2.1 依赖引入与版本对应关系项目用的是 Maven 做构建JDK 版本这里说下我的选择JavaFX 17 需要 JDK 17 起步Spring Boot 我用的是 2.7.x对应 Spring Framework 5.3.x因为这一系列的兼容性非常稳定MyBatis 官方 starter 对它支持也最好。如果你非要用 Spring Boot 3.x注意它基于 Jakarta EEMyBatis starter 也要换到mybatis-spring-boot-starter:3.0.x部分 API 包名不再兼容改起来麻烦不少。后端模块的pom.xml核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencymysql-connector-j是 MySQL 8.x 的新坐标老版本用mysql-connector-java也还能跑但新项目没必要再用旧坐标。Lombok 属于“用了就回不去”的库实体类里几十个字段Data一加getter/setter/toString 全出来了能省下大量重复代码。2.2 application.yml 里最值得关注的几项配置后端配置文件application.yml我通常会写成这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/asset_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.asset.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl几个配置项的含义和坑点mapper-locations指定 XML 文件位置。很多人的 Mapper 接口写好了但一直报Invalid bound statement (not found)多半是这里没配置对XML 没被扫描到。map-underscore-to-camel-case一定要打开。数据库字段是device_name实体属性是deviceName打开这个开关后 MyBatis 会自动做驼峰映射不需要一个个写resultMap去手动匹配。log-impl配StdOutImpl开发阶段可以在控制台直接看到完整的 SQL 和参数排查问题效率极高。生产环境记得去掉或用 Logback 的 Debug 级别控制。2.3 Mapper 接口与 XML 的规范写法Mapper 接口注入到 Service 里代码层面很薄Mapper public interface DeviceMapper { int insertDevice(Device device); ListDevice selectDeviceList(Param(keyword) String keyword, Param(category) String category); int countDeviceByCategory(Param(category) String category); }XML 里用动态 SQL 处理多条件查询select idselectDeviceList resultTypecom.example.asset.entity.Device SELECT id, device_name, model, category, purchase_date, status, create_time FROM device where if testkeyword ! null and keyword ! AND (device_name LIKE CONCAT(%, #{keyword}, %) OR model LIKE CONCAT(%, #{keyword}, %)) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY create_time DESC /select这里有两个面试官特别爱问的点我顺手讲清楚#{}是预编译占位符MyBatis 会把它替换成?由 JDBC 的PreparedStatement执行参数绑定能有效防止 SQL 注入。而${}是字符串拼接直接把值拼到 SQL 里有注入风险。除非是动态表名、列名这类非传值场景否则一律用#{}。where标签会自动处理首个条件前的AND也就是说你没传任何条件时它不会生成一个WHERE AND ...的残缺 SQL。这是 MyBatis 动态 SQL 里一个非常实用的细节。2.4 分页方案PageHelper 还是手写 LIMIT列表页几乎都会碰分页需求我比较推荐直接用 PageHelperdependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency用法极其简单先设置分页参数再调用查询PageHelper.startPage(pageNum, pageSize); ListDevice list deviceMapper.selectDeviceList(keyword, category); PageInfoDevice pageInfo new PageInfo(list);分页总记录数、总页数、当前页数据全都封装在PageInfo里后端把这个对象原样返回给客户端就知道怎么渲染表格了。PageHelper 的原理是基于 MyBatis 的拦截器在查询执行前动态拼接LIMIT ?所以使用时务必保证startPage和紧随其后的那一条 SQL 是一对一关系中间最好不要插入其他查询语句否则分页会作用到错误的 SQL 上。2.5 热搜词里的“表不存在自动建表”搜索热度里有个“springboot mybatis 当表不存在自动建表”的需求我猜是有人想在项目启动时省去手工建库建表的步骤。我提供一种可靠做法Spring Boot 支持spring.sql.init初始化脚本在application.yml里配置spring: sql: init: mode: always schema-locations: classpath:db/schema.sqlschema.sql里写CREATE TABLE IF NOT EXISTS device (...)这种幂等语句每次启动时执行一遍表不存在就建存在则跳过。这种方式适合开发环境、演示环境生产环境不建议依赖它还是交给正式的数据库迁移工具Flyway 或 Liquibase更稳妥。3. JavaFX 客户端设计与 REST API 对接3.1 客户端模块怎么搭骨架JavaFX 客户端我建议建成另一个 Maven 模块和后端隔离只依赖 Spring Boot 提供的接口地址。这样两端可以独立开发后端接口不稳定的情况下客户端也能用 Mock 数据先跑界面。客户端模块pom.xml的依赖dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version17.0.10/version /dependency dependency groupIdorg.openjfx/groupId artifactIdjavafx-fxml/artifactId version17.0.10/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.4/version /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependencyJavaFX 17 OkHttp Jackson 是我目前用得最顺手的一套客户端技术组合。OkHttp 处理 HTTP 请求简单直接Jackson 负责把后端返回的 JSON 序列化为 Java 对象。Java 自带的 HttpClient 也不是不行但 OkHttp 的连接池和超时控制用起来更灵活大家看习惯选就行。3.2 客户端里怎么解决 Spring 容器问题JavaFX 的 Controller 通常由 FXML 加载器 new 出来不归 Spring 管理所以直接Autowired是无效的。我的做法是写一个工具类提前把 Spring 上下文存下来public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; Override public void setApplicationContext(ApplicationContext applicationContext) { context applicationContext; } public static T T getBean(ClassT clazz) { return context.getBean(clazz); } }在客户端启动入口里手动 new 一个 Spring Boot 的应用上下文再把工具类注册进去public class MainApp extends Application { Override public void start(Stage primaryStage) throws Exception { ConfigurableApplicationContext context new SpringApplicationBuilder() .sources(ClientConfig.class) .web(WebApplicationType.NONE) .run(); SpringContextHolder.setContext(context); FXMLLoader loader new FXMLLoader(getClass().getResource(/fxml/LoginView.fxml)); Parent root loader.load(); primaryStage.setTitle(设备资产管理系统); primaryStage.setScene(new Scene(root, 1200, 800)); primaryStage.show(); } public static void main(String[] args) { launch(args); } }注意.web(WebApplicationType.NONE)因为客户端不需要内嵌 Tomcat这是不少新手容易忽略的坑不关掉的话启动时会去占用 8080 端口和后端冲突。3.3 网络请求与 JSON 序列化那点事客户端调用后端接口常见的做法是封装一个ApiClientpublic class ApiClient { private static final OkHttpClient CLIENT new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); private static final ObjectMapper MAPPER new ObjectMapper() .setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); public static T T get(String url, ClassT clazz) throws Exception { Request request new Request.Builder() .url(BASE_URL url) .header(Authorization, token) .build(); try (Response response CLIENT.newCall(request).execute()) { String body response.body().string(); if (!response.isSuccessful()) { throw new RuntimeException(HTTP response.code() : body); } return MAPPER.readValue(body, clazz); } } }这里最容易被坑的是时间字段。后端 LocalDateTime 默认序列化格式是一串带 T 的 ISO 字符串比如2024-06-01T10:30:00JavaFX 表格直接显示很难看。所以两边要约定好格式后端spring.jackson.date-format配yyyy-MM-dd HH:mm:ss客户端ObjectMapper用同样格式去解析就完成了对齐。3.4 TableView 数据绑定与界面刷新机制JavaFX 的表格刷新和 Web 前端有很大区别它的核心是ObservableList。当列表数据变化时视图会自动收到通知并刷新不需要手动table.refresh()一遍。我把全部设备加载到 TableView 的代码写出来FXML private TableViewDevice deviceTable; FXML private TableColumnDevice, String nameCol; FXML private TableColumnDevice, String modelCol; FXML private TableColumnDevice, String categoryCol; FXML private TableColumnDevice, Date purchaseDateCol; private ObservableListDevice deviceData FXCollections.observableArrayList(); FXML private void initialize() { nameCol.setCellValueFactory(new PropertyValueFactory(deviceName)); modelCol.setCellValueFactory(new PropertyValueFactory(model)); categoryCol.setCellValueFactory(new PropertyValueFactory(category)); purchaseDateCol.setCellValueFactory(new PropertyValueFactory(purchaseDate)); deviceTable.setItems(deviceData); loadData(); } private void loadData() { try { ListDevice list ApiClient.get(/device/list?keyword, new TypeReferenceListDevice() {}); deviceData.clear(); deviceData.addAll(list); } catch (Exception e) { showAlert(加载失败, e.getMessage()); } }注PropertyValueFactory会把列名“deviceName”自动映射到实体类的getDeviceName()方法上所以实体类必须要有一致的 getter 方法否则表格列会全部显示为空。3.5 线程模型的坑JavaFX 新手必看JavaFX 的 UI 操作必须在 JavaFX Application ThreadFXAT上执行一旦你在普通子线程里直接deviceData.addAll(...)程序分分钟抛出IllegalStateException: Not on FX application thread。我处理这个问题的标准姿势是使用Task结合Platform.runLaterTaskListDevice task new Task() { Override protected ListDevice call() throws Exception { return ApiClient.get(/device/list?keyword, new TypeReferenceListDevice() {}); } }; task.setOnSucceeded(event - { deviceData.clear(); deviceData.addAll(task.getValue()); }); new Thread(task).start();call()在子线程里执行网络请求setOnSucceeded回调由 JavaFX 在 UI 线程调用数据安全地更新到界面上。更复杂的场景还可以用javafx.concurrent.Service做请求的复用但我个人觉得小项目里 Task 已经足够清晰。4. 联调实录几个绕不开的坑4.1 接口跨域问题JavaFX 客户端本质上是另一个“浏览器同源”的客户端它发的 HTTP 请求和后端不在同一个 Origin 下后端如果在 Web 层加了跨域限制请求会被拦住。解决方式是在 Spring Boot 里配置全局跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(*) .allowedHeaders(*); } }不过说实话正式生产环境加了 Token 鉴权后跨域限制通常不会构成问题因为你用的是自定义请求头Authorization而allowedHeaders(*)已经放开了。这里只需要记住如果请求发出去了但控制台没任何报错或返回 403先查是不是 Origin 校验这层出问题。4.2 MySQL 方言和时区问题MySQL 8.x 连接串里的serverTimezoneAsia/Shanghai是必须的否则会报The server time zone value is unrecognized或时区错乱插入的时间比本地少 8 个小时。另外characterEncodingutf8要写成characterEncodingutf8MySQL 8 里utf8mb4才是完整的 4 字节编码支持如果表里要存 emoji 或生僻字URL 和表结构都要对齐utf8mb4。4.3 MyBatis 懒加载失效如果项目配置了fetchTypelazy之类的懒加载在 Spring Boot 分层调用里问题不大但在 JavaFX 客户端中如果你把实体对象返回后脱离了 MyBatis 的 SqlSession 生命周期再访问嵌套属性时可能触发LazyInitializationException。最省心的解决办法是在 Service 层就完成所有关联查询实体对象返回给 Controller 时已经完全加载好不要在 Controller/Servlet 线程里触发懒加载。这一点在面试描述项目时也很加分能体现你对 MyBatis 生命周期管理有概念。4.4 连接池如何配置才不倒退Spring Boot 2.x 默认数据源连接池是 HikariCP它在性能和稳定性上都很优秀我用的也是它。连接失败或连接池被耗尽多半和配置有关建议配置spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 3 connection-timeout: 30000 idle-timeout: 600000如果客户端同时打开多个窗口并频繁请求maximum-pool-size太小容易把连接池打满报Connection is not available, request timed out after 30000ms。我在这里吃过亏后来干脆把初始连接数调大一些问题就消失了。4.5 动态 SQL 的常见翻车现场多条件查询最容易出的问题条件都为空时where是安全的但如果 XML 里手写了WHERE再自己拼AND/OR那就麻烦了。老代码常见写法if testkeyword ! nullAND device_name LIKE .../if如果所有条件为空且你写了WHERE生成 SQL 就是SELECT * FROM device WHERE直接语法报错。所以最稳的方案就是多用whereif的组合让 MyBatis 替你处理首条AND。5. 打包部署与后续扩展5.1 后端打成 Docker 镜像Spring Boot 后端部署我通常用 Docker。首先制作一个DockerfileFROM eclipse-temurin:17-jdk-alpine WORKDIR /app COPY target/asset-server-1.0.0.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]构建命令mvn clean package -DskipTests docker build -t asset-server:v1 . docker run -d -p 8080:8080 --name asset-server asset-server:v1这里值得注意的一点是 JVM 参数中需要预留-XX:MaxRAMPercentage75.0之类的内容在容器环境里让 JVM 按宿主机资源自动适配避免容器内存设置过大超出限制被 OOMKill。不过在实际项目里这些参数可以放在JAVA_OPTS环境变量里启动命令长一点但控制灵活。5.2 JavaFX 客户端如何分发JavaFX 客户端打完包的常见形式是 fat jar 或 exe。Maven 里可以用maven-shade-plugin直接构建一个包含所有依赖的 fat jar然后打包发给用户用java -jar启动。但这种方法给终端用户使用门槛偏高如果要给现场的人双击就能运行推荐 JDK 自带的jpackage工具。jpackage会把 JavaFX 客户端和它的运行时一起打成本地安装包Windows 上生成 exe/msimacOS 上生成 dmg。基本命令jpackage --name AssetClient \ --input target/ \ --main-jar client-1.0.0.jar \ --main-class com.example.asset.MainApp \ --type msi \ --dest dist/这个工具比较吃配置图形界面的打包如果 FXML 路径、CSS、图片资源没处理好很容易打出来空白页或找不到类。我的经验是先把客户端本地跑通了再打非 GUI 处理模式的 jar 做验证最后才用jpackage封包每步都确认一遍资源路径。5.3 扩展成多端接入或者消息推送这套架构后端的接口设计是通用的所以后续扩展非常顺畅。比如同一个 Spring Boot 服务可再写一个 Vue 管理后台或者加一个 WebSocket 支持JavaFX 客户端收到操作提示后自动刷新列表。我在实际项目中就做过把扫码枪接收到的消息通过 WebSocket 推送到 JavaFX 客户端界面上的待办列表实时刷新体验比定时轮询好了不少。具体思路是后端引入spring-boot-starter-websocket客户端用 OkHttp 的 WebSocket 实现连上同一个端点收到服务端推送的 JSON 后用Platform.runLater再更新 TableView。这个方案适合实时性要求高的场景如果只是每天早上查一次数据的工具轮询就够了没必要引入额外复杂度。6. 我帮你的最后几个建议如果你准备照着这套架构自己搭一个项目我强烈建议不要一上来就把所有功能都堆上去而是先做一条完整的最小链路Spring Boot 启动能访问一个查询接口 → MyBatis 能从数据库查出数据 → JavaFX 能查询并展示列表 → 能写入数据并刷新。这条链路跑通后剩下的业务功能都是增量添加不会出现“开发了半个月发现底层架构跑不通”的尴尬。再提一个容易被忽视的细节前后端实体类别共用。后端返回富余字段或经过计算的字段时客户端如果严格要求字段完整解析会很容易出问题。我一般习惯在后端定义VO而不是直接把数据库实体怼给客户端或者至少保证返回 JSON 时用JsonInclude(JsonInclude.Include.NON_NULL)把空字段过滤掉客户端适配的容错空间会大得多。我见过太多同学一上来就在 JavaFX 里写RestTemplate或者HttpClient然后被线程问题折磨半天。按我上面给的Task Platform.runLater的模式应对绝大多数桌面端请求已经足够了。以后要是遇到性能瓶颈或者界面卡顿优先排查网络请求是否阻塞了 FX 线程再考虑加缓存或并发改造。这套组合不管是毕设、个人练手还是小团队内部工具都算得上一条稳扎稳打的路子。本文还有配套的精品资源点击获取
返回列表