简介:SpringBoot+MyBatis+JavaFX 综合项目源码包,面向需要整合桌面客户端与服务端的 Java 开发者,聚焦档案管理中的扫描功能。项目以 SpringBoot 的自动配置、起步依赖和内嵌式 Web 容器简化服务端搭建;MyBatis 负责 SQL 映射、ORM 与事务控制;JavaFX 借助组件库和事件监听构建交互界面,并配合 Oracle 数据库实现档案数据的安全存储与复杂查询。包内共 130 个文件,包含 24 个 Java 源文件、65 个 XML 映射/配置、4 个 FXML 界面布局、2 个 CSS 样式,以及 properties、class 等编译产物,压缩包仅 146KB,结构紧凑。已有 4198 人学习下载,适合对照源码快速梳理界面、业务与持久层的调用关系,也可作为档案扫描类桌面应用的起步模板。 从“springboot+mybatis+javafx”这三个词组合在一起,很多人第一反应是“又是堆技术栈”。但如果你真在业务系统里摸爬滚打过,会发现这个组合恰恰是当前很多企业内部工具、桌面管理和数据看板类项目的标准解法:Spring Boot 负责把后端服务收拾利索,MyBatis 把 SQL 控制权攥在手里,JavaFX 则撑起一个跨平台的桌面界面,三者各管一摊,边界清晰。这篇内容我打算从一个实际做过资产台账管理客户端的角度,把整个项目从架构设计、环境搭建、核心代码到打包部署的流程完整拆给你看,也会把我在联调过程中踩过的坑、面试被问烂的几个点一并交代清楚。不管你是准备自己动手做毕设,还是团队里要快速起一个带界面的内部工具,这套思路应该都够你直接抄作业。
1. 这个技术组合真正适合做什么
1.1 三个技术栈的职责划分
先说清楚这三样东西在项目里各干什么活,不然框架一多就乱套。
Spring Boot 是后端服务底座,凡是和业务逻辑、权限控制、数据库打交道的事都放它这里。它解决的问题是让你不用再从零配置 Spring 容器、事务管理、Web 模块这些基础设施,一个spring-boot-starter-web加进去,内嵌 Tomcat 一启动,REST 接口就能跑起来。
MyBatis 是本项目的持久层框架,它的定位很明确:SQL 由你掌控。相比 Hibernate 或 Spring Data JPA 那种全自动 ORM,MyBatis 更适合那些表结构复杂、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 API,MyBatis 操作 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 EE,MyBatis starter 也要换到mybatis-spring-boot-starter:3.0.x,部分 API 包名不再兼容,改起来麻烦不少。
后端模块的pom.xml核心依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>mysql-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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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); List<Device> selectDeviceList(@Param("keyword") String keyword, @Param("category") String category); int countDeviceByCategory(@Param("category") String category); }XML 里用动态 SQL 处理多条件查询:
<select id="selectDeviceList" resultType="com.example.asset.entity.Device"> SELECT id, device_name, model, category, purchase_date, status, create_time FROM device <where> <if test="keyword != null and keyword != ''"> AND (device_name LIKE CONCAT('%', #{keyword}, '%') OR model LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="category != 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
列表页几乎都会碰分页需求,我比较推荐直接用 PageHelper:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>用法极其简单,先设置分页参数,再调用查询:
PageHelper.startPage(pageNum, pageSize); List<Device> list = deviceMapper.selectDeviceList(keyword, category); PageInfo<Device> 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> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>17.0.10</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>17.0.10</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency>JavaFX 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(Class<T> 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 序列化那点事
客户端调用后端接口,常见的做法是封装一个ApiClient:
public 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, Class<T> 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:00,JavaFX 表格直接显示很难看。所以两边要约定好格式:后端spring.jackson.date-format配yyyy-MM-dd HH:mm:ss,客户端ObjectMapper用同样格式去解析,就完成了对齐。
3.4 TableView 数据绑定与界面刷新机制
JavaFX 的表格刷新和 Web 前端有很大区别,它的核心是ObservableList。当列表数据变化时,视图会自动收到通知并刷新,不需要手动table.refresh()一遍。我把全部设备加载到 TableView 的代码写出来:
@FXML private TableView<Device> deviceTable; @FXML private TableColumn<Device, String> nameCol; @FXML private TableColumn<Device, String> modelCol; @FXML private TableColumn<Device, String> categoryCol; @FXML private TableColumn<Device, Date> purchaseDateCol; private ObservableList<Device> 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 { List<Device> list = ApiClient.get("/device/list?keyword=", new TypeReference<List<Device>>() {}); deviceData.clear(); deviceData.addAll(list); } catch (Exception e) { showAlert("加载失败", e.getMessage()); } }注:PropertyValueFactory会把列名“deviceName”自动映射到实体类的getDeviceName()方法上,所以实体类必须要有一致的 getter 方法,否则表格列会全部显示为空。
3.5 线程模型的坑,JavaFX 新手必看
JavaFX 的 UI 操作必须在 JavaFX Application Thread(FXAT)上执行,一旦你在普通子线程里直接deviceData.addAll(...),程序分分钟抛出IllegalStateException: Not on FX application thread。
我处理这个问题的标准姿势是使用Task结合Platform.runLater:
Task<List<Device>> task = new Task<>() { @Override protected List<Device> call() throws Exception { return ApiClient.get("/device/list?keyword=", new TypeReference<List<Device>>() {}); } }; 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 连接串里的serverTimezone=Asia/Shanghai是必须的,否则会报The server time zone value is unrecognized或时区错乱,插入的时间比本地少 8 个小时。另外characterEncoding=utf8要写成characterEncoding=utf8,MySQL 8 里utf8mb4才是完整的 4 字节编码支持,如果表里要存 emoji 或生僻字,URL 和表结构都要对齐utf8mb4。
4.3 MyBatis 懒加载失效
如果项目配置了fetchType="lazy"之类的懒加载,在 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 test="keyword != null">AND device_name LIKE ...</if>,如果所有条件为空且你写了WHERE,生成 SQL 就是SELECT * FROM device WHERE,直接语法报错。所以最稳的方案就是多用<where>+<if>的组合,让 MyBatis 替你处理首条AND。
5. 打包部署与后续扩展
5.1 后端打成 Docker 镜像
Spring Boot 后端部署我通常用 Docker。首先制作一个Dockerfile:
FROM 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:MaxRAMPercentage=75.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/msi,macOS 上生成 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 线程,再考虑加缓存或并发改造。这套组合不管是毕设、个人练手还是小团队内部工具,都算得上一条稳扎稳打的路子。
本文还有配套的精品资源,点击获取