用 Spring Boot + Vue + Fuzio 构建现代 Java 桌面应用

很多人以为 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:https://zhida.zhihu.com/search?content_id=278634520&content_type=Article&match_order=1&q=REST+%E6%8E%A5%E5%8F%A3&zhida_source=entity(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 = "http://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.fuzio</id>
        <url>https://jiku.mycloudrepo.io/public/repositories/releases</url>
    </repository>
</repositories>

<dependencies>
    <!-- Fuzio:获取所有平台的 Chromium 二进制文件 -->
    <dependency>
        <groupId>tech.fuzio</groupId>
        <artifactId>fuzio-cross-platform</artifactId>
        <version>2026.3.0</version>
        <type>pom</type>
    </dependency>

    <!-- Fuzio:Swing UI 工具包集成 -->
    <dependency>
        <groupId>tech.fuzio</groupId>
        <artifactId>fuzio-swing</artifactId>
        <version>2026.3.0</version>
    </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.navigation().loadUrl(appUrl);
});

这里 appUrl 指向内嵌 Spring Boot 的本地地址(http://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<String, Object> 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()
}

这里暂时省略了鉴权逻辑。实际企业项目或生产环境中,即使服务只监听 127.0.0.1,也建议在请求中携带应用启动时生成的本地访问 token,例如通过 Authorization 或自定义请求头传递,避免同一台机器上的其他进程直接调用本地接口。

前端的构建产物也由这个内嵌 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 加载 http://127.0.0.1:<port>/ 就能显示完整的 Vue 页面,而前端的 fetch('/api/dashboard/summary') 也落在同一个内嵌服务上。

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

第三步那套"内嵌服务器 + HTTP"已经能跑通完整应用。但它隐含了一个前提:你愿意在本地起一个 HTTP 服务器。有些场景并不需要 - 比如你只想用 Spring 的依赖注入、配置、数据访问,并不打算对外暴露任何 HTTP 接口;或者出于安全考虑,不希望前端代码和接口能被本地浏览器直接访问。

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

用自定义协议托管前端

第三步是把 Vue 构建产物交给内嵌 Tomcat、通过 http://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.navigation().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().<JsObject>executeJavaScript("window").putProperty("springBridge", bridge);
    return InjectJsCallback.Response.proceed();
});

前端只要声明一下类型,就能像调用本地方法一样调用后端:

复制代码
declare const springBridge: {
  listUsers(): string
}

const users = JSON.parse(springBridge.listUsers())
相关推荐
ZhengEnCi1 小时前
S02-SpringBoot实体类新增字段对已有数据的影响及自动DDL同步机制详解
数据库·spring boot
你挚爱的强哥1 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
程序员张32 小时前
SpringBoot集成BCrypt密码加密库
java·spring boot·后端
闲猫2 小时前
Spring AI Agentic 模式(第1部分):Agent Skills——模块化、可复用的能力
java·人工智能·spring
源图客2 小时前
云途物流API开发-鉴权认证(Java)
java·网络·python
liguojun20252 小时前
篮球馆自动计时收费系统:从规则配置到自动结算的全流程拆解
java·大数据·运维·人工智能·物联网·1024程序员节
952363 小时前
RabbitMQ-基础操作
java·spring boot·分布式·后端·spring·rabbitmq
卓怡学长3 小时前
w266基于spring boot + vue 圣地延安美食乐享系统
java·数据库·vue.js·spring boot·spring·intellij-idea
C++、Java和Python的菜鸟3 小时前
第7章 Java高级技术
java·开发语言
程序员佳佳4 小时前
模型网关灰度不是调百分比:把放量、观测和回滚做成账本
java·数据库·人工智能·redis·gpt·aigc·embedding