Spring 通常与服务器端 Java 联系在一起。但这并不意味着它只能用于服务器端。桌面应用程序同样可以受益于依赖注入、配置、Profile、数据访问,以及 Spring 为后端项目提供的熟悉的服务层。

本文将介绍如何在一个 Java 桌面应用程序中结合使用 Spring Boot、Vue 和 JxBrowser。

该应用程序使用 Spring Boot 处理业务逻辑并提供本地 HTTP 端点,使用 Vue 构建用户界面。 JxBrowser 则提供基于 Chromium 的 BrowserView,用于在 Swing 窗口中渲染 Vue 应用程序。

要求 

  • Java 17 或更高版本。
  • 用于构建 Vue 应用程序的 Node.js 和 npm。
  • JxBrowser 许可证密钥或评估许可证密钥。
  • Java 构建使用 Gradle Kotlin DSL。

应用程序结构 

该应用程序由三个部分组成:

  • Spring Boot 与桌面应用程序运行在同一个 Java 进程中。它负责配置、服务和 REST 端点。
  • Vue 是一个常规的 Vue 3 和 Vite 应用程序。它负责布局、交互和图表。
  • JxBrowser 提供嵌入到 Swing 窗口中的、基于 Chromium 的 BrowserView。

最终得到的是一个在原生窗口中包含 Web 仪表板的 Java 桌面应用程序。

Admin dashboard rendered in a JxBrowser Swing window

Java 桌面窗口中的管理仪表板。

一个简洁的项目结构可以如下所示:

spring-jxbrowser-desktop/
├── build.gradle.kts
├── src/main/java/com/example/desktop/
│   ├── DesktopApplication.java
│   ├── dashboard/
│   │   ├── DashboardController.java
│   │   └── DashboardService.java
│   └── user/
│       ├── UserController.java
│       └── UserService.java
├── src/main/resources/
│   ├── application.yml
│   └── static/
└── web-app/
    ├── package.json
    ├── vite.config.ts
    └── src/

主要流程非常直观:

  1. Spring Boot 在 127.0.0.1 上使用嵌入式 Web 服务器启动。
  2. 应用程序读取服务器实际分配到的本地端口。
  3. Swing 打开桌面窗口。
  4. JxBrowser 从本地 Spring Boot 服务器加载 Vue UI。
  5. Vue 使用标准的 fetch() 调用本地 Spring API。

配置构建 

本文使用 Gradle Kotlin DSL。在 build.gradle.kts 中,应用 JxBrowser Gradle 插件,并添加当前平台和 Swing 依赖项:

plugins {
    java
    application
    id("org.springframework.boot") version "4.1.0"
    id("com.teamdev.jxbrowser") version "2.0.0"
}

jxbrowser {
    version = "9.5.6"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation(jxbrowser.currentPlatform)
    implementation(jxbrowser.swing)
}

application {
    mainClass.set("com.example.desktop.DesktopApplication")
}

tasks.withType<JavaExec> {
    systemProperties(
        System.getProperties().mapKeys { it.key as String }
    )
    jvmArgs("--add-opens=java.desktop/java.awt=ALL-UNNAMED")
}

jxbrowser.currentPlatform 依赖项会添加构建运行所在平台对应的 Chromium 二进制文件。对于部署构建,请添加应用程序要分发到的操作系统所需的特定平台 JxBrowser 依赖项。

启动 Spring 

入口点是一个常规的 Spring Boot 应用程序。对于桌面应用程序而言,重要的一点是 headless(false)。Spring Boot 应用程序通常在没有图形环境的情况下运行,但 Swing 应用程序需要访问 AWT。

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.context.WebServerApplicationContext;

@SpringBootApplication
public final class DesktopApplication {

    public static void main(String[] args) {
        var context = new SpringApplicationBuilder(DesktopApplication.class)
                .headless(false)
                .run(args);

        var port = ((WebServerApplicationContext) context)
                .getWebServer()
                .getPort();
        var appUrl = "http://127.0.0.1:" + port + "/";

        DesktopWindow.open(context, appUrl);
    }
}

将嵌入式服务器配置为使用随机本地端口:

server:
  port: 0
  address: 127.0.0.1

port: 0 会让操作系统选择一个空闲端口。绑定到 127.0.0.1 可以避免服务器暴露在本地网络中。但这并不能阻止同一台计算机上的其他进程发送请求,因此生产环境中的应用程序应添加启动时生成的本地令牌或其他本地授权检查机制。

打开窗口 

Swing 窗口负责显示桌面 UI。JxBrowser 负责 Chromium 引擎以及加载到该窗口中的浏览器实例。

import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;

import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import java.awt.BorderLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.springframework.context.ConfigurableApplicationContext;

public final class DesktopWindow {

    public static void open(ConfigurableApplicationContext context,
                            String appUrl) {
        var engine = Engine.newInstance(HARDWARE_ACCELERATED);
        var browser = engine.newBrowser();

        SwingUtilities.invokeLater(() -> {
            var view = BrowserView.newInstance(browser);
            var frame = new JFrame("Spring and JxBrowser Desktop App");

            frame.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent event) {
                    engine.close();
                    context.close();
                }
            });

            frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            frame.add(view, BorderLayout.CENTER);
            frame.setSize(1280, 800);
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);

            browser.navigation().loadUrl(appUrl);
        });
    }
}

此时,Java 进程会启动 Spring Boot、打开 Swing 窗口,并通过 JxBrowser 加载 Web UI。

提供 UI 

Vue 应用程序是一个常规的 Vite 项目。在生产构建期间,Vite 会将输出写入 Spring Boot 的静态资源目录:

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: './',
  build: {
    outDir: fileURLToPath(
      new URL('../src/main/resources/static', import.meta.url),
    ),
    emptyOutDir: true,
  },
})

在 web-app/ 中运行前端构建:

npm run build

然后,在项目根目录中启动 Java 应用程序:

./gradlew run -Djxbrowser.license.key=<your_license_key>

Spring Boot 会从 src/main/resources/static 提供编译后的 Vue 文件。JxBrowser 会从本地 Spring Boot URL 加载生成的页面。

此时页面已经可以加载,但仍然需要数据。接下来,我们添加 Vue 应用程序要调用的 Spring 端点。

添加 REST 端点 

仪表板可以使用常规的 Spring MVC 端点。例如,控制器可以返回页面所需的汇总数据:

import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/dashboard")
public final class DashboardController {

    private final DashboardService dashboardService;

    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 Repository、本地数据库、文件或内部系统。

在这种架构下,Vue 端不需要自定义传输层:

export async function fetchSummary(): Promise<DashboardSummary> {
  const response = await fetch('/api/dashboard/summary')

  if (!response.ok) {
    throw new Error(`Failed to load dashboard data: ${response.status}`)
  }

  return response.json()
}

由于页面和 API 都来自同一个本地 Spring Boot 服务器,因此前端可以使用 /api/dashboard/summary 这样的相对 URL。

本地服务器的权衡 

嵌入式服务器方案很容易理解。它使用标准的 Spring MVC 控制器、标准的浏览器请求以及常规的前端代码。

但这也意味着应用程序会启动一个本地 HTTP 服务器。将其绑定到 127.0.0.1 可以避免暴露到网络,但其他本地进程仍然可以尝试发送请求。对于内部桌面应用程序,如果本地 API 不涉及高风险操作,使用随机端口并结合本地授权检查可以作为一个合理的基础方案,例如在应用程序启动时生成一个令牌,并要求每个本地 API 请求都携带该令牌。对于对本地攻击有更严格安全要求的应用程序,应避免在内部 UI 与 Java 之间使用 HTTP。

不使用本地 HTTP 

JxBrowser 还支持另一种架构:通过自定义 Scheme 加载前端,并通过 JavaScript-Java 桥接从 JavaScript 调用 Java。如果桌面应用程序不应启动本地 HTTP 服务器,可以使用这种方式。

在这种架构下,Spring Boot 在不启动嵌入式 Web 服务器的情况下运行:

var context = new SpringApplicationBuilder(DesktopApplication.class)
        .web(WebApplicationType.NONE)
        .headless(false)
        .run(args);

应用程序需要在创建引擎之前注册一个自定义 Scheme:

import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;

import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.EngineOptions;
import com.teamdev.jxbrowser.net.Scheme;

var domainToResourceInterceptor =
        new DomainToResourceInterceptor("desktop", "static");

var options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
        .addScheme(Scheme.of("app"), domainToResourceInterceptor)
        .build();
var engine = Engine.newInstance(options);

domainToResourceInterceptor 变量可以是 JxBrowser 本地内容教程中的 DomainToResourceInterceptor 实例。该类扩展了 DomainContentInterceptor,并通过类加载器从应用程序资源中加载 HTML、CSS 和 JavaScript 文件。完整实现请参阅加载本地内容教程。注册 Scheme 后,浏览器便可以从应用程序自己的 URL 加载 UI:

browser.navigation().loadUrl("app://desktop/");

随后,前端可以通过注入到 JavaScript window 中的桥接对象调用 Spring Bean:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.teamdev.jxbrowser.js.JsAccessible;
import org.springframework.stereotype.Component;

@Component
@JsAccessible
public final class SpringBridge {

    private final UserService userService;
    private final ObjectMapper json;

    public SpringBridge(UserService userService, ObjectMapper json) {
        this.userService = userService;
        this.json = json;
    }

    public String listUsersJson() throws JsonProcessingException {
        return json.writeValueAsString(userService.findAll());
    }
}

在页面脚本运行之前注入该 Bean:

import com.teamdev.jxbrowser.browser.callback.InjectJsCallback;
import com.teamdev.jxbrowser.js.JsObject;

var bridge = context.getBean(SpringBridge.class);

browser.set(InjectJsCallback.class, params -> {
    JsObject window = params.frame().executeJavaScript("window");
    window.putProperty("springBridge", bridge);
    return InjectJsCallback.Response.proceed();
});

在 Vue 端,该桥接对象看起来就像一个 JavaScript 对象:

declare const springBridge: {
  listUsersJson(): string
}

const users = JSON.parse(springBridge.listUsersJson())

这种方式无需本地 HTTP 服务器,但也失去了常规 REST 调用所带来的简洁性。当这一权衡对您的应用程序很重要时,可以选择这种方式。

总结 

当项目已经需要配置、依赖注入、服务和数据访问时,Spring Boot 在桌面应用程序中也可以发挥作用。JxBrowser 则负责架构的另一部分:它将基于 Chromium 的 Web View 嵌入到 Java 桌面窗口中,使 UI 可以使用 Web 技术构建。

最简单的方式是让 Spring Boot 在随机回环端口上提供 Vue 构建输出和 REST 端点。如果无法接受启动本地 HTTP 服务器,JxBrowser 的自定义 Scheme 支持和 JavaScript-Java 桥接则提供了一种无需本地 HTTP 服务器的替代方案。

Spinner

发送中。。。

抱歉,发送中断

请再次尝试,如果问题仍然存在,请联系我们 info@teamdev.com.

阅读并同意条款以继续。

您的个人 JxBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。