游乐游手机版
首页/前端开发/文章详情

从零开始用Spring Boot+Vue+Fuzio构建现代Java桌面应用的完整教程

时间:2026-07-17 07:00
利用SpringBoot内嵌Web服务器承载业务逻辑与数据,Vue构建前端界面,Fuzio将Web界面嵌入Java桌面窗口并连接后端,实现分层清晰、依赖注入与配置管理完善的现代桌面应用,兼具Web开发灵活性与桌面应用原生体验。

Spring 传统上被视为服务器端专属框架,但这一认知已逐渐转变。当桌面项目趋于复杂,需要清晰的分层架构、依赖注入、配置管理与数据访问支持时,将 Spring 引入客户端已经成为一种经过验证的最佳实践——尤其在企业级桌面软件开发中。更进一步,Spring Boot 自带的嵌入式 Web 服务器,使桌面客户端能够内置完整的本地后端,从而借助 Web 技术构建用户界面。

这一思路的具体实现方案为:用 Spring Boot 承载业务逻辑与数据,并内置本地 HTTP 服务;用 Vue 构建前端界面;再用 Fuzio 将 Web 界面嵌入桌面窗口并与 Spring 后端连接。接下来,我们将从零开始逐步搭建。

为什么在桌面应用里引入 Spring

在桌面端引入 Spring 的核心驱动力在于:项目一旦复杂化,原生工具包在应用结构管理上的局限性就会显现。Swing 和 JavaFX 擅长界面呈现,但缺乏对依赖、配置和对象生命周期的统一管理——开发者不得不手动 new 服务、自行维护单例、将配置逻辑分散到各个控制器中。项目规模较小时尚可应付,随着应用增长,代码很快变得纠缠且难以维护。这也是为何在稍具规模的项目、尤其是企业级桌面软件中,使用 Spring 来组织桌面应用已成为一种成熟模式

将 Spring 引入桌面,可直接复用整套后端能力:

  • 依赖注入与 Bean 生命周期
  • 配置与 Profile
  • 数据访问
  • 生态组件
  • 内嵌 Web 服务器

一旦客户端内置了 HTTP 服务器,界面层便有了全新选择:不必再局限于原生控件,而是可以用 Web 技术来构建。这恰好弥补了 Swing/JavaFX 在现代视觉效果和跨平台一致性上的短板。

由此引出接下来要构建的架构:用 Spring Boot 作为内嵌后端,用 Web 技术实现界面,再通过 Fuzio 将两者整合进一个 Java 桌面窗口。 下面逐步实现。

我们要构建什么

一个单一可执行的 Java 桌面应用,内部由三部分组成:

  • Spring Boot:随桌面应用一同启动,负责业务逻辑、配置管理与数据访问。
  • Vue 前端:标准的 Vue 3 + Vite 工程,负责界面布局、交互与图表展示。
  • Fuzio:在 Java 桌面窗口中嵌入 Chromium,将 Vue 界面渲染为原生桌面应用的一部分。

最终效果是一个深色风格的后台管理界面,运行在原生桌面窗口中:

项目结构如下:

 复制代码spring-fuzio-desktop-demo/
├── pom.xml                          # Maven 构建配置
├── src/main/
│   ├── java/tech/fuzio/demo/
│   │   ├── DesktopApplication.java  # 入口:启动 Spring Boot + Swing 窗口 + Fuzio
│   │   ├── dashboard/               # 方案 A:REST 接口(Controller + Service)
│   │   └── user/                    # 方案 B:用户业务
│   └── resources/
│       ├── application.yml          # 内嵌服务器配置(端口 / 监听地址)
│       └── static/                  # 前端构建产物(由 Spring Boot 托管)
└── web-app/                         # Vue 前端
    ├── package.json
    ├── vite.config.ts               
    └── src/
        ├── pages/                   # Dashboard / 用户管理页
        ├── components/              # 图表等复用组件
        └── api/                     # 两套通信封装:HTTP 与 JS 桥

针对 Vue 页面如何获取 Java 后端的数据,这里提供两种方案:

  • 方案 A:内嵌服务器 + REST 接口:Spring Boot 内嵌 Tomcat,既托管前端,又以 REST 接口提供数据,前端通过标准 HTTP 调用。
  • 方案 B:自定义协议 + Java↔JS 桥:不启动本地 HTTP 服务器,Vue 资源由 Fuzio 自定义协议从应用内部加载,前后端通过 Java↔JS 桥直接通信。

两套方案如何选择,后面会详细讨论。下面先从两者共同的基础——让 Spring Boot 与 Swing 共存于同一进程开始。

第一步:为桌面应用启动 Spring Boot 后端

首先解决最基础的问题:一个 Spring Boot 应用如何同时弹出一个 Swing 窗口。

与任何 Spring Boot 应用一样,入口类使用 @SpringBootApplication 注解修饰,该注解启用 Spring Boot 服务。

应用本身通过 SpringApplicationBuilder 创建,这样可以在启动前调整启动参数。其中关键的一项调整是关闭无头模式:默认情况下,Spring Boot 以“无头模式”(headless)启动,该模式适用于服务器环境,不提供图形界面支持。而在这种模式下创建 JFrameJDialog 等需要图形环境的组件时,会抛出 HeadlessException。因此启动时需显式设置 headless(false)

 复制代码import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;// @SpringBootApplication 启用 Spring Boot 的自动配置与组件扫描。
@SpringBootApplication
public class DesktopApplication {    public static void main(String[] args) {
        // 使用 SpringApplicationBuilder 创建应用,并在启动前关闭无头模式。
        // headless(false) 是桌面应用的必备设置,否则创建 Swing 窗口会抛 HeadlessException。
        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(DesktopApplication.class)
//                        .web(WebApplicationType.NONE)
                        .headless(false)
                        .run(args);        // ... 后续创建窗口
    }
}

这里是否需要保留内嵌 Web 服务器,取决于您选择哪套方案:

  • 方案 A(第三步,用内嵌服务器托管前端 + REST),则保持默认——不要添加 web(WebApplicationType.NONE),让 Spring Boot 启动内嵌 Tomcat。
  • 方案 B(第四步,自定义协议 + JS 桥),无需 HTTP,可以显式设置 web(WebApplicationType.NONE),将 Spring 作为纯 DI 容器使用。

本文先按方案 A 推进。为此配置 application.yml,让内嵌 Tomcat 使用随机端口,并仅监听本地回环地址:

 复制代码server:
  # 0 = 随机端口,启动后由代码读取实际端口,避免端口冲突。
  port: 0
  # 仅监听本地回环地址,避免内嵌服务被局域网访问。
  address: 127.0.0.1

使用 port: 0 让系统分配一个空闲端口,并在启动后回读真实端口——这样多个实例或被占用端口都不会冲突。读取端口的方式:

 复制代码import org.springframework.boot.web.context.WebServerApplicationContext;int port = ((WebServerApplicationContext) context).getWebServer().getPort();
String appUrl = "https://127.0.0.1:" + port + "/";

至此,Spring Boot 已能在桌面环境下启动。但还没有窗口——接下来交给 Fuzio。

第二步:用 Fuzio 在桌面窗口中渲染 Vue 界面

Fuzio 是一个 Java 嵌入式 Chromium 浏览器组件,可将基于 Chromium 的 Web 视图集成进 Swing、JavaFX、SWT、Compose Desktop 应用。此处使用 Swing 的 JFrame 作为主窗口,并在窗口内部嵌入 Fuzio 的 BrowserView。

先添加 Fuzio 依赖。下面分别给出 Maven 和 Gradle 的配置方式。

Mavenpom.xml):

 复制代码<repositories>
    <repository>
        <id>tech.fuzioid>
        <url>https://maven.fuzio.tech/releasesurl>
    repository>
repositories><dependencies>
    
    <dependency>
        <groupId>tech.fuziogroupId>
        <artifactId>fuzio-cross-platformartifactId>
        <version>2026.3.0version>
        <type>pomtype>
    dependency>    
    <dependency>
        <groupId>tech.fuziogroupId>
        <artifactId>fuzio-swingartifactId>
        <version>2026.3.0version>
    dependency>
dependencies>

Gradlebuild.gradle,使用官方 Fuzio Gradle 插件):

 复制代码plugins {
    id 'tech.fuzio.gradle' version '1.0.0'
}fuzio {
    // Fuzio 版本(必需)
    version = '2026.3.0'
}dependencies {
    // 检测当前平台并添加对应的 Chromium 二进制文件
    implementation fuzio.currentPlatform    // Fuzio:Swing UI 工具包集成
    implementation fuzio.swing
}

应用主窗口基于 Java Swing 的 JFrame 实现——作为 Java 内置组件,它轻量且易用。窗口内部嵌入 Fuzio 提供的 Chromium 内核 Web 视图(BrowserView),直接在桌面窗口中渲染 Vue.js 界面,并支持所有现代浏览器特性:

 复制代码var engine = Engine.newInstance(HARDWARE_ACCELERATED);
var browser = engine.newBrowser();SwingUtilities.invokeLater(() -> {
    var view = BrowserView.newInstance(browser);
    var frame = new JFrame("Spring + Fuzio 桌面应用");
    frame.addWindowListener(new WindowAdapter() {
        @Override
        public void windowClosing(WindowEvent e) {
            engine.close();
        }
    });
    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    frame.add(view, BorderLayout.CENTER);
    frame.setSize(1280, 800);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);    // 加载由内嵌 Spring Boot 托管的本地前端
    browser.na vigation().loadUrl(appUrl);
});

这里的 appUrl 指向内嵌 Spring Boot 的本地地址(https://127.0.0.1:{port}/)。

这套架构分工明确:Spring 后端负责核心业务逻辑(数据读写、文件操作等),Vue.js 前端专注界面布局与用户交互,各司其职、高效协同。

另外,Fuzio 采用多进程架构。Chromium 在独立进程中运行,通过 IPC 与 Java 进程通信。这样即使某个网页崩溃,承载 Spring 后端的 Java 进程依然存活,对需要保护用户数据的桌面应用尤为关键。

第三步:方案 A——内嵌服务器托管前端 + REST 接口

第一套方案最为直接:让 Spring Boot 启动内嵌 Tomcat,既托管 Vue 构建产物,又提供 REST 接口。这一步编写一个普通的 @RestController,前端通过标准 HTTP 调用它:

 复制代码@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {    private final DashboardService dashboardService; // 普通 Spring Bean,构造器注入    public DashboardController(DashboardService dashboardService) {
        this.dashboardService = dashboardService;
    }    @GetMapping("/summary")
    public Map summary() {
        return Map.of(
                "statCards", dashboardService.statCards(),
                "revenueChart", dashboardService.revenueChart());
    }
}

DashboardService 是一个普通 Spring Bean,项目中可以接入 Spring Data、MyBatis、本地数据库或企业内部系统。关键在于:桌面应用仍然可以沿用 Spring Boot 熟悉的分层方式组织业务代码。

前端(Vue)这边就是常规的 fetch

 复制代码// 前端通过标准 fetch 调用内嵌 Spring Boot 的 REST 接口。
export async function fetchSummary(): Promise<DashboardSummary> {
  const res = await fetch('/api/dashboard/summary')
  if (!res.ok) throw new Error('加载仪表盘数据失败:' + res.status)
  return res.json()
}

前端的构建产物也由这个内嵌 Spring Boot 托管。Vue 工程使用 Vite,将构建输出目录指向 Spring Boot 的静态资源目录:

 复制代码// vite.config.ts
export default defineConfig({
  plugins: [vue()],
  base: './',
  build: {
    // 构建产物直接输出到 Spring Boot 的 static 目录,由内嵌 Tomcat 托管。
    outDir: fileURLToPath(new URL('../src/main/resources/static', import.meta.url)),
    emptyOutDir: true,
  },
})

这样一来,Spring Boot 启动后,Fuzio 加载 https://127.0.0.1:{port}/ 就能显示完整的 Vue 页面,而前端的 fetch('/api/dashboard/summary') 也落在同一个内嵌服务上。

第四步:方案 B——自定义协议 + Java↔JS 桥

第三步的“内嵌服务器 + HTTP”方案已能运行完整应用。但它隐含了一个前提:您需要在本地启动一个 HTTP 服务器。有些场景并不需要——例如只想使用 Spring 的依赖注入、配置、数据访问,而不打算对外暴露任何 HTTP 接口;或出于安全考虑,不希望前端代码和接口能被本地浏览器直接访问。

这种情况下,可以走一条完全不依赖本地服务器的路线:前端资源通过 Fuzio 的自定义协议从应用内部加载,前后端通信使用 Java↔JS 桥。这里换用这套方案实现用户管理页。

用自定义协议托管前端

第三步是将 Vue 构建产物交给内嵌 Tomcat,通过 https://127.0.0.1:{port}/ 提供。这里换一种方式:将 Vue 应用文件直接打包进 Java 资源目录,通过 Fuzio 的自定义协议拦截 API,从类路径中直接加载资源。

为了允许请求拦截器处理 Web 资源请求,将其分配给一个自定义协议:

 复制代码var engine = Engine.newInstance(
        EngineOptions.newBuilder(HARDWARE_ACCELERATED)
                .addScheme(Scheme.of("app"), interceptor)
                .build());

每当浏览器尝试加载 app:// URL 时,拦截器就会处理该请求,并从应用的资源目录中返回对应的 HTML、CSS 或 JavaScript 文件。

通过这种方式,所有 Web 资源的加载都发生在应用内部,外部无法拦截,也无法访问这些资源。同时,使用自定义协议可确保常规 HTTPS/XHR 请求(例如身份验证、API 调用)能够顺利通过。

之后让浏览器加载 app://my-app.com 即可,不再需要本地端口:

 复制代码browser.na vigation().loadUrl("app://my-app.com");

这样一来,所有 HTML、CSS、JavaScript 都从应用 JAR 内读取,不经过本地 HTTP 服务器,源码也不会暴露在某个本地地址上

用 Java↔JS 桥进行通信

前端不再使用 fetch 发起 HTTP 请求,而是直接调用注入到页面里的 Java 对象。出于安全考虑,Fuzio 只允许标注了 @JsAccessible 的公共方法被 JavaScript 访问。定义一个桥对象,注入所需的 Spring Bean,将方法暴露给前端:

 复制代码@Component
@JsAccessible
public class SpringBridge {    private final UserService userService;     public SpringBridge(UserService userService) {
        this.userService = userService;
    }    public String listUsers() {
        return new Gson().toJson(userService.findAll());
    }
}

然后使用 Fuzio 的 InjectJsCallback,将从 Spring 容器取出的桥对象挂到 window 上:

 复制代码SpringBridge bridge = context.getBean(SpringBridge.class);
browser.set(InjectJsCallback.class, params -> {
    params.frame().executeJa vaScript("window").putProperty("springBridge", bridge);
    return InjectJsCallback.Response.proceed();
});

前端只需声明类型,即可像调用本地方法一样调用后端:

 复制代码declare const springBridge: {
  listUsers(): string
}const users = JSON.parse(springBridge.listUsers())

用户管理页运行效果如下:

来源:https://juejin.cn/post/7661983059006128154
上一篇TinyVue BaseSelect 基础选择器实战:高效表单筛选利器详细教程 下一篇Vue-i18n 与 TypeScript 集成实现自动补全与类型提示完整指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
JavaScript动态加载与执行脚本的完整使用方法详解
前端开发 · 2026-07-17

JavaScript动态加载与执行脚本的完整使用方法详解

动态加载JavaScript脚本主要有三种方式:通过document createElement插入标签兼容性最好;import()支持按需加载ES模块并返回Promise;eval或Function构造器适用于执行代码字符串但风险较高。使用时需注意避免重复加载、处理超时及服务端渲染环境。

箭头函数多行逻辑的书写优化
前端开发 · 2026-07-17

箭头函数多行逻辑的书写优化

箭头函数多行逻辑需用花括号包裹并显式return,否则返回undefined;单行仅限一个表达式自动返回;返回对象字面量必须加小括号;复杂逻辑应拆分为独立函数,避免硬塞导致代码混乱。

如何设计JavaScript函数缓存防止内存无限增长
前端开发 · 2026-07-17

如何设计JavaScript函数缓存防止内存无限增长

防止函数缓存导致内存无限增长,需主动控制容量和寿命。采用LRU策略自动淘汰闲置条目,设定TTL防止脏数据长期驻留,优化键生成避免冗余堆积,并提供手动清理接口配合业务生命周期。

JavaScript布尔值转换核心逻辑详解
前端开发 · 2026-07-17

JavaScript布尔值转换核心逻辑详解

JavaScript布尔转换遵循明确的真值与假值规则,仅有6个假值:false、0、空字符串、null、undefined、NaN,其余所有值包括空数组、空对象均为真值。显式转换推荐Boolean(),隐式转换常见于条件判断与逻辑运算,需注意??与||的区别。

如何利用Performance API识别长任务来源的完整方法详解
前端开发 · 2026-07-17

如何利用Performance API识别长任务来源的完整方法详解

利用性能API识别长任务来源,需结合时间锚点、开发者工具验证和主动埋点。通过性能观察者捕获基础信息,使用性能标记缩小范围,借助Chrome开发者工具定位代码行,最后通过主动埋点验证,将长任务映射到具体代码段。