资讯详情

Spring Boot与JavaFX集成双模式Demo:桌面端依赖注入实战

📅 2026/10/16 5:03:23 | 华诺云谱 👁 阅读
Spring Boot与JavaFX集成双模式Demo:桌面端依赖注入实战
简介面向希望把Spring Boot与JavaFX结合来开发桌面应用的Java工程师这套实例Demo从集成思路到可运行源码层层拆解覆盖自动配置引入、主类双框架启动、FXML加载、控制器注入等关键环节。压缩包共86个文件以41个Java源文件为核心体现启动类、控制器、业务逻辑分层12个FXML文件描述界面结构XML、YAML、Properties三类配置满足不同场景另有CSS样式与Maven包装脚本等辅助材料整体按part_1至part_8分模块组织便于对照学习。包体仅98KB内容精炼适合有Spring使用经验、想快速接手桌面端项目的开发者。目前已有1411人学习下载。通过研读源码可以直观理解Spring容器如何接管JavaFX生命周期并在桌面端复用依赖注入、配置管理等Spring Boot核心能力各模块目录与可运行示例也为后续扩展提供了一份清晰模板。1. Spring Boot JavaFX 集成实例 Demo 源码桌面端也能直接吃上 Spring 的福利Spring Boot 和 JavaFX 单独拿出来都不陌生但把它们塞进同一个工程里很多人的第一反应是“这俩怎么联动”。这个实例 Demo 源码反直觉的地方在于它没有把 JavaFX 当成一个纯 HTML 前端来打补丁而是用 Maven 多模块同时提供两条集成路线跨进程模式把 JavaFX 当客户端、Spring Boot 当独立服务两者走 REST进程内模式让 JavaFX 启动时直接拉起 Spring 容器Controller 里能 Autowired Service。整个 Demo 以用户登录和订单列表为主线代码量和目录结构都收敛到一个刚好的规模适合想用桌面端做后台管理、又不想丢掉 Spring Boot 依赖注入和事务能力的人。要是你正准备搭一个能跑起来的桌面客户端骨架这个源码完全可以当起点用。2. 技术选型与项目结构三个 Maven 模块怎么分工2.1 为什么把 Spring Boot 和 JavaFX 放在同一个仓库先说说这个组合为什么值得放在同一个项目里。JavaFX 的优势是 UI 渲染和交互劣势是缺少一套成熟的业务对象管理机制。很多桌面端项目写着写着就变成了 Controller 里 new Service、Service 里再 new DAO对象散落得到处都是等业务一复杂改一个字段要牵连十几个类这就是典型的“手动依赖注入翻车”。Spring Boot 这边恰恰相反它有完整的依赖注入容器、配置绑定、事务管理可它天生面向服务端和 Web没有桌面界面。于是这个 Demo 做的事情就非常直白把 Spring Boot 的容器和 JavaFX 的窗口放在同一个进程或同一个工程里让 JavaFX 的 Controller 像 Spring MVC 的 Controller 一样舒服地拿到 Service 和 Repository。这个项目里其实还藏着一个反模式对比如果只把 Spring Boot 当成一个薄薄的 API 壳JavaFX 侧所有业务逻辑裸写那壳没有意义如果反过来把 JavaFX 完全揉进 Spring Bean 里FXML 加载机制又要额外适配。Demo 给了一个中间方案进程内模式用 Spring 容器管理 Controller跨进程模式用统一的 ApiClient 封装 HTTP 调用两种模式共用同一套 DTO。这才是它值得下载下来看的原因。2.2 目录结构与模块职责整个工程没有采用单模块的堆积写法而是拆成三个 Maven 模块依赖方向非常清晰。先看目录结构demo-root/ ├── pom.xml ├── demo-common/ │ └── src/main/java/com/demo/common/ │ ├── dto/UserDTO.java │ ├── dto/OrderDTO.java │ └── result/Result.java ├── demo-server/ │ ├── pom.xml │ └── src/main/ │ ├── java/com/demo/server/ │ │ ├── DemoServerApplication.java │ │ ├── controller/LoginController.java │ │ ├── controller/OrderController.java │ │ ├── service/OrderService.java │ │ └── repository/OrderRepository.java │ └── resources/ │ ├── application.yml │ └── data.sql └── demo-client/ ├── pom.xml └── src/main/ ├── java/com/demo/client/ │ ├── Launcher.java │ ├── EmbeddedLauncher.java │ ├── config/ApiConfig.java │ ├── api/ApiClient.java │ └── controller/LoginController.java └── resources/ ├── fxml/login.fxml ├── fxml/order_list.fxml └── css/app.css三个模块的职责可以用一个很短的表格讲清模块定位关键依赖demo-common共享 DTO 与统一返回包装无第三方依赖demo-serverSpring Boot 后端提供 REST 接口starter-web、starter-data-jpa、h2demo-clientJavaFX 桌面端负责界面与交互javafx-controls、javafx-fxmldemo-common 是纯数据模块两端都要依赖所以放在整个 Maven reactor 的最前面。demo-server 是标准 Spring Boot 应用内置 Tomcat数据库用 H2 内存库数据初始化靠 data.sql。demo-client 是标准 JavaFX 工程入口类分两个Launcher 对应跨进程模式EmbeddedLauncher 对应进程内模式。这样拆的好处是二次开发时你可以单独替换前端而不动后端或者把后端独立部署到服务器客户端只留一个配置文件。2.3 进程内与跨进程两种集成方式的取舍这个 Demo 里两种模式同时存在看起来是重复劳动实际是故意为之。跨进程模式JavaFX 和 Spring Boot 各自独立启动中间通过 REST 接口通信进程内模式JavaFX 启动时把 Spring 容器拉进同一个 JVMController 直接注入 Service。两者各有适用场景取舍点在这里对比项跨进程模式进程内模式通信方式HTTP RESTJVM 内直接方法调用部署产物前端 后端分开打包单个桌面应用典型场景已有独立后端、前后端多人协作单机工具、快速原型主要成本DTO 序列化、接口联调内存占用、Spring 启动时间跨进程模式的价值是解耦JavaFX 这边换成一个 Web 前端也完全不影响后端坏处是每个接口都要写请求映射、处理序列化和异常。进程内模式的价值是开发体验Service 层拿过来就能注入事务和配置天然共用坏处是整个应用变重启动时要带着 Spring Boot 一起跑。默认情况下这个工程走的是跨进程模式进程内模式通过一个 Maven Profile 激活切换成本几乎为零。这一点很加分因为它让学习者在同一条代码里同时看到两种架构的写法而不是二选一。3. 环境准备与启动先跑后端再跑前端两条路线都过一遍3.1 环境要求与 Maven 配置这个 Demo 对环境的门槛很低。JDK 需要 11 及以上因为 JavaFX 从 JDK 11 开始被独立出来必须显式引入 OpenJFX 依赖Maven 建议 3.5 以上主要是为了保证 javafx-maven-plugin 和 spring-boot-maven-plugin 能正常解析。IDE 则无所谓IDEA、Eclipse 或 VS Code 都可以命令行的跑法是一致的。父 pom.xml 用 modules 做聚合构建顺序是 common 在最前modules moduledemo-common/module moduledemo-server/module moduledemo-client/module /modules这段配置决定了 Maven reactor 的构建顺序demo-client 依赖 demo-common 时必须保证 common 先进入本地仓库否则会报“找不到依赖”这种玄学错误。版本方面Spring Boot 相关依赖交给 spring-boot-starter-parent 统一管理OpenJFX 的版本单独用一个属性控制避免两个 framework 的传递依赖互相打架。demo-client 的 pom.xml 里必配 javafx-maven-plugin因为javafx:run能自动把 JavaFX 模块加载到 classpath省去手工拼写 module-path 的麻烦plugin groupIdorg.openjfx/groupId artifactIdjavafx-maven-plugin/artifactId configuration mainClasscom.demo.client.Launcher/mainClass /configuration /plugin这里 mainClass 决定mvn javafx:run启动的是哪个类。跨进程模式指向 Launcher进程内模式则用 Profile 把它改成 EmbeddedLauncher。插件版本和 javafx 版本都以仓库里父 pom 的属性值为准不建议直接改到最新容易和 JavaFX 运行时版本对不上。3.2 后端独立启动spring-boot:run 与接口验证先从后端开始跑。如果你只关心跨进程模式第一步就是启动 demo-server。命令如下mvn -pl demo-server -am spring-boot:run-pl demo-server表示只构建指定的 demo-server 模块-am会顺带构建它依赖的 demo-common第一次运行如果不加-am很可能因为本地仓库里没有 demo-common 的 jar 而失败。spring-boot:run是 Spring Boot 插件提供的目标等价于直接运行 DemoServerApplication 主类不需要先打 jar 包。启动成功后内置 Tomcat 默认监听 8080日志里会出现 “Started DemoServerApplication”。接着用 curl 验证登录接口curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}返回结果是一个统一包装对象 Resultcode 为 200 代表登录成功data 里携带用户 DTO。如果连不上先看三件事8080 端口有没有被别的进程占用H2 依赖有没有被打进 classpathJDK 版本是否满足要求。后端启动阶段是这套工程最容易踩坑的地方但也是最好排查的因为 Spring Boot 的报错信息通常已经把原因写在堆栈里。3.3 前端独立启动javafx:run 与配置文件后端跑起来之后另开一个终端启动前端。命令是mvn -pl demo-client javafx:runjavafx:run 会打开一个 JavaFX 窗口显示登录界面。此时前端并不依赖 Spring 容器它只是一个纯 JavaFX 应用通过 ApiClient 调用后端接口。后端的地址和超时时间集中在 demo-client 的src/main/resources/application.properties里server.urlhttp://localhost:8080 server.timeout5000第一个配置是接口根地址如果你把 demo-server 部署到了别的机器只需要改这里客户端代码不用动第二个配置是 HTTP 调用超时时间单位毫秒设置 5000 是为了防止后端死掉时前端按钮一直转圈。登录成功后页面会跳转到订单列表列表数据来自 H2 里 data.sql 预置的几条订单记录。如果窗口能打开但登录按钮点了没反应多半是 server.url 指向的端口和后端实际启动的端口不一致。先回后端控制台看端口号再检查 properties 文件这种问题基本都能秒解。3.4 进程内集成模式一条命令拉起两个端跨进程模式跑通后再看进程内模式就比较容易理解。进程内模式不需要单独启动后端前端启动时会自动把 Spring 容器带起来。命令变成了mvn -pl demo-client -Pembedded javafx:run-Pembedded激活的是 demo-client 里声明的 Maven Profile这个 Profile 会额外引入 demo-server 的依赖并把 javafx-maven-plugin 的 mainClass 改成 com.demo.client.EmbeddedLauncher。这样一来JavaFX 窗口打开的同时Spring Boot 的日志也会出现在控制台说明容器已经在同一个 JVM 里启动了。两种模式跑出来的界面完全一样区别只在依赖方向和启动类。跨进程模式下 ApiClient 走 HTTP进程内模式下 Controller 直接向 Spring 容器要 Bean。这也是这个 Demo 最值得抄的一个点通过 Profile 切架构而不需要维护两套 Controller 代码。4. 核心模块拆解一个登录请求从 JavaFX 到 Spring Boot 的完整链路4.1 服务端 REST 层Controller、Repository、统一返回先看服务端的订单接口。这个 Demo 的 Controller 写得比较克制没有把业务逻辑堆在接口层RestController RequestMapping(/api/order) public class OrderController { private final OrderRepository orderRepository; public OrderController(OrderRepository orderRepository) { this.orderRepository orderRepository; } GetMapping(/list) public ResultListOrder list(RequestParam(required false) String keyword) { ListOrder orders keyword null || keyword.isBlank() ? orderRepository.findAll() : orderRepository.findByTitleContaining(keyword); return Result.ok(orders); } }构造器注入是 Spring 官方推荐的写法字段变成 final依赖关系一目了然。返回类型统一用 Result 包装而不是裸的 List这样前端可以拿到 code、message、data 三段结构不用靠异常来猜状态。keyword 参数是可选的为空时返回全部数据不为空时走模糊查询。对应的 Repository 接口是另一个关键点public interface OrderRepository extends JpaRepositoryOrder, Long { ListOrder findByTitleContaining(String keyword); }这行接口方法没有写实现JPA 会自动根据方法名生成查询。方法是findByTitleContaining翻译成 SQL 就是where title like %keyword%。字段名 Title 必须和 Order 实体的属性名严格对应否则启动时就会报错。这是 Spring Data JPA 最基本的规则也最容易踩坑字段名拼错一个字母编译器不会提示启动时才炸。4.2 客户端 FXML 与 Controller界面和逻辑怎么绑定JavaFX 端的 FXML 里界面控件通过 fx:id 注入事件通过 onAction 绑定。这是 login.fxml 里的一段核心结构TextField fx:idusernameField promptText用户名/ PasswordField fx:idpasswordField promptText密码/ Label fx:idmessageLabel wrapTexttrue/ Button text登录 onAction#handleLogin/fx:id对应 Controller 里的 FXML 字段onAction#handleLogin对应 Controller 里名为 handleLogin 的方法。这套命名约定让 UI 和逻辑分离得很干净Controller 里拿到的字段类型必须和 FXML 里声明的控件类型一致否则 FXMLLoader 在 load 阶段会直接抛异常。LoginController 的逻辑也相当直白public class LoginController { FXML private TextField usernameField; FXML private PasswordField passwordField; FXML private Label messageLabel; public void handleLogin(ActionEvent event) { String username usernameField.getText().trim(); String password passwordField.getText(); LoginResponse response apiClient.login(username, password); if (response.isSuccess()) { messageLabel.setText(登录成功); // 这里跳转到订单列表页面 } else { messageLabel.setText(response.getMessage()); } } }Controller 里没有出现任何 new OrderService它只依赖一个 apiClient。跨进程模式下 apiClient 由外部传入或手动创建进程内模式下则由 Spring 容器注入。业务逻辑对一个桌面端 Controller 来说已经足够薄了这也正是集成 Spring 的意义所在。4.3 跨进程调用封装RestTemplate 的超时与错误处理JavaFX 侧调用后端不能每次请求都散着写所以这个 Demo 用一个 ApiClient 集中管理。它在配置阶段做了两件事设置超时时间和统一请求前缀Component public class ApiClient { private final RestTemplate restTemplate; private final String baseUrl; public ApiClient(RestTemplateBuilder builder, Value(${server.url}) String baseUrl) { this.restTemplate builder .setConnectTimeout(Duration.ofSeconds(3)) .setReadTimeout(Duration.ofSeconds(10)) .build(); this.baseUrl baseUrl; } public LoginResponse login(String username, String password) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityLoginRequest entity new HttpEntity(new LoginRequest(username, password), headers); ResponseEntityResultLoginResponse resp restTemplate.exchange( baseUrl /api/auth/login, HttpMethod.POST, entity, new ParameterizedTypeReferenceResultLoginResponse() {}); return resp.getBody().getData(); } }connectTimeout 是连接建立的超时readTimeout 是等待响应的超时这两个参数分别管“连不上”和“连上后不返回”两种情况。参数化类型这里必须用 ParameterizedTypeReference如果直接传Result.class泛型里的 LoginResponse 会被反序列化成 LinkedHashMap前端取值时就要多写一堆类型转换。这段封装在二次开发时可以直接复制换成 WebClient 也是同一个套路。进程内模式下ApiClient 仍然会被 Spring 容器管理只是它不再真正发起 HTTP 请求而是由调用方决定走容器还是走网络。这个 Demo 的巧妙之处在于ApiClient 本身成了两种模式的接缝处。4.4 进程内集成JavaFX 生命周期如何接住 Spring 容器进程内模式最核心的类是 EmbeddedLauncher。JavaFX 的 Application 生命周期是 init 再 start这个 Demo 把 Spring 容器的启动放在 init 里保证窗口打开之前所有 Bean 已经就绪public class EmbeddedLauncher extends Application { private ConfigurableApplicationContext context; Override public void init() { context new SpringApplicationBuilder(DemoServerApplication.class) .web(WebApplicationType.NONE) .run(); } Override public void start(Stage stage) throws IOException { FXMLLoader loader new FXMLLoader(getClass().getResource(/fxml/login.fxml)); Parent root loader.load(); context.getAutowireCapableBeanFactory().autowireBean(loader.getController()); stage.setScene(new Scene(root)); stage.show(); } Override public void stop() { context.close(); } }这一节代码是整个进程内模式的关键。web(WebApplicationType.NONE)表示不启动内置 Tomcat因为跨进程模式才会监听端口进程内模式如果再占一个端口就属于资源浪费。autowireBean(loader.getController())是大多数初学者最不容易想到的一步后面避坑章节会展开讲。stop 方法里调用 context.close() 也很重要否则 JavaFX 窗口关了后台 Spring 线程还挂着任务管理器里能看到 Java 进程一直不退出。这个细节属于那种不写就“越用越卡”的坑。5. 避坑记录集成调试中最容易翻车的五个节点5.1 JavaFX 线程阻塞耗时操作导致 UI 假死现象点击登录按钮后整个窗口变成白框鼠标挪动都卡等几秒后才恢复。原因JavaFX 的 UI 事件默认跑在 JavaFX Application Thread 上如果在这个线程里直接发起 HTTP 请求或者执行数据库查询线程被阻塞UI 刷新就失去响应。日志里不会有任何报错属于最典型的黑匣子问题。解决耗时操作必须放到后台线程再通过 Platform.runLater 回到 UI 线程更新控件new Thread(() - { LoginResponse response apiClient.login(username, password); Platform.runLater(() - messageLabel.setText(response.getMessage())); }).start();我一般会直接用 javafx.concurrent.Task 封装它自带状态回调和线程池管理比手写 Thread 更稳。判断依据很简单只要方法体里有网络或数据库调用就默认不能放在 Controller 方法里直接执行。5.2 FXML Controller 依赖注入为 null现象进程内模式下启动窗口Controller 里的 Autowired Service 一直是 null点按钮直接抛 NullPointerException。原因FXMLLoader 加载 FXML 时是通过反射直接 new 出一个 Controller 实例这个实例根本不是 Spring 容器里的 Bean所以 Autowired 注解不会被处理。解决在 FXML load 完成之后手动让 Spring 容器对 Controller 做一次依赖补齐FXMLLoader loader new FXMLLoader(getClass().getResource(/fxml/login.fxml)); Parent root loader.load(); context.getAutowireCapableBeanFactory().autowireBean(loader.getController());这行代码补的是“填坑式注入”不是把 Controller 放进容器而是让容器把已经存在的 Controller 的依赖字段填满。autowireBean 会检查 Autowired 字段并注入对应 Bean依赖不存在时依然会报错但至少错误信息会指明是哪个 Bean 缺失。5.3 Maven 打包后 FXML 资源路径消失现象IDE 里运行一切正常一用mvn package后 java -jar 运行窗口打不开报找不到/fxml/login.fxml。原因FXML 被打到 jar 包后路径变成绝对 classpath 起始形式。如果代码用了new File(src/main/resources/fxml/login.fxml)这在 IDE 运行时能蒙对打 jar 后文件根本不在那个目录自然加载不到。解决统一用 classpath 方式读取资源getClass().getResource(/fxml/login.fxml)写法上要注意开头必须有斜杠表示从 classpath 根开始找。打包后可以用jar tf或者直接打开 jar 包检查 target/classes 目录下是否出现了 fxml 和 css 文件。这一步检查做到位资源路径问题基本能全灭。5.4 进程内模式端口冲突现象先启动了跨进程模式的后端占住 8080再跑进程内模式工程直接报端口已占用并退出。原因进程内模式虽然不需要对外服务但 Spring Boot 默认还是会尝试启动 Web 容器监听端口两个进程抢同一个端口必然冲突。解决进程内模式把 Web 环境关掉或换随机端口。最直接的做法是server: port: 0server.port0表示随机分配端口进程内模式下根本没有外部调用所以对业务无影响。如果还需要在进程内模式里提供某些管理接口那就保留固定端口但要确保和其他模式错开。这个配置写在 application.yml 里用 Profile 区分生效场景更干净。5.5 H2 数据初始化不生效现象后端启动后查订单列表返回空数组但 data.sql 里明明写了 INSERT。原因JPA 默认情况下会顺带管理表结构如果 data.sql 在 JPA 建表之前执行表还不存在SQL 就会被跳过而且不报错。解决在 application.yml 里设置启动顺序spring: jpa: defer-datasource-initialization: true这个配置保证 Hibernate 先建表再执行 data.sql。如果项目用的是 2.x 版本这是官方推荐的做法用了这个还是没数据再检查 data.sql 的表名和实体类是否匹配。这类问题不报错但结果错属于开发时最容易忽略的一类。6. 二次开发从 Demo 到带 Token 的桌面客户端这个 Demo 的登录验证是明文账密数据库用的也是 H2真正要拿去做项目时第一件事不是加按钮而是把认证和数据层换掉。我的习惯是分三处落刀数据库从 H2 换成 MySQL登录从账密比对换成 JWT再给 RestTemplate 挂上 Token 拦截器。前两处改配置就行第三处值得看一下具体代码。一个很实用的改造是为 ApiClient 增加 Token 携带能力。登录成功后拿到 token把它存到一个拦截器里之后每个请求自动带上不用每个方法都写请求头Component public class TokenInterceptor implements ClientHttpRequestInterceptor { private String token; public void setToken(String token) { this.token token; } Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { if (token ! null !token.isBlank()) { request.getHeaders().setBearerAuth(token); } return execution.execute(request, body); } }使用时在 ApiClient 的构造阶段把拦截器注册进 RestTemplate登录接口成功后调用tokenInterceptor.setToken(...)后续所有请求就自动带着 Authorization 头。这个改动量不大但把原有 Demo 的登录逻辑升级成了会话保持模式前端代码完全不用感知 token 是怎么拼的。验证时只需要先登录再换一个没有 token 的客户端去访问订单接口后端返回 401就说明拦截链路已经生效。这套改造做完整个工程就从“能跑的 Demo”变成了“能接真实接口的客户端骨架”。我当初拆这个项目时最深的教训反而不是代码本身而是很多集成问题都出在“以为不报错就是对的”这种判断上。从那以后我每次拆这类前后端混合工程都会强制自己走一遍完整链路先验证端口、再验证数据初始化、最后验证类加载顺序全过一遍才开始改代码。希望这篇解析能帮你少绕一圈弯路把踩过的坑变成你看代码时的直觉。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑