使用 Angular 构建 Java 桌面应用

使用 Angular 构建 Java 桌面应用
在这里插入图片描述


本文介绍如何构建一个跨平台的 Java 桌面应用,在原生 Swing 窗口中集成现代化的 Angular Web 界面。

前置条件

要完成本教程,您需要:

  • Git
  • Java 17 或更高版本
  • Node.js 22.0+
  • npm 9+
  • 有效的 JxBrowser 许可证(评估版或商业版)。有关许可证的更多信息,请参阅许可指南。

项目设置

本教程示例应用程序的代码与其他示例一起,存储在一个基于 Gradle 的 GitHub 仓库中。

如果您想构建一个基于 Maven 的项目,请参考 Maven 配置指南。如果您希望从头开始构建一个基于 Gradle 的项目,请参考 Gradle 配置指南。

获取代码

要获取代码,请执行以下命令:

git clone https://github.com/TeamDev-IP/JxBrowser-Gallery.git cd JxBrowser-Gallery/desktop-angular-dashboard 

添加许可证

要运行本教程,您需要设置许可证密钥

您将构建什么

在本教程中,您将创建一个桌面应用程序,该应用具备以下特性:

  • 在原生 Java Swing 窗口中嵌入 Angular 仪表板
  • 在开发模式下,从本地开发服务器加载 Angular UI
  • 在生产模式下,将所有 UI 资源直接打包到 JAR 中,支持离线可用
  • 使用 JxBrowser 提供的 JavaScript-Java 桥接器,实现 Angular 和 Java 之间的通信

项目架构

本文创建的桌面应用由两个主要部分组成:

  • Java 后端:一个基于 Swing 的应用程序,用于托管浏览器窗口并提供数据服务
  • Angular 前端:一个提供用户界面并与 Java 后端通信的 Web 应用程序

项目结构

desktop-angular-dashboard/ ├── build.gradle.kts # Gradle 构建配置 ├── src/main/ │ ├── java/.../angular/ # Java 后端 │ │ ├── App.java # 入口点 │ │ ├── AppInitializer.java # 窗口设置和 JS-Java bridge │ │ └── production/ # URL 拦截器和 MIME 类型 │ └── resources/ │ └── web/ # 打包的 Angular 文件(生产环境) └── web-app/ # Angular 前端 ├── package.json # npm 依赖 ├── angular.json └── src/app/ ├── app.config.ts └── services/ └── backend.service.ts 

创建应用窗口

主窗口使用 Java Swing 的 JFrame 实现。
在窗口内嵌入 JxBrowser 的 BrowserView 来渲染 Angular UI。

var engine =Engine.newInstance(HARDWARE_ACCELERATED);var browser = engine.newBrowser();SwingUtilities.invokeLater(()->{var view =BrowserView.newInstance(browser);var frame =newJFrame("Angular Dashboard"); frame.addWindowListener(newWindowAdapter(){@OverridepublicvoidwindowClosing(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);});

Engine 管理底层的 Chromium 进程,而 BrowserView 是在 Java 应用程序中显示 Web 内容的 Swing 组件。要深入了解这些组件如何交互,请参阅架构指南。

在桌面应用中加载 Angular UI

应用程序的 Angular 部分是位于 web-app/ 目录中的标准 Web 项目。
启动开发服务器时,您会看到熟悉的输出:

$ npm start Initial chunk files | Names | Raw size polyfills.js | polyfills | 90.20 kB | main.js | main | 18.18 kB | Application bundle generation complete. [1.204 seconds] ➜ Local: http://localhost:4200/ 

该应用支持两种模式(开发模式和生产模式)以满足不同的使用需求。

在开发模式下,我们希望修改组件或样式时能获得即时反馈。
在生产环境下,我们希望用户界面是安全且独立封装的,不依赖任何外部服务器。

开发模式

在开发模式下,我们希望对 Angular 组件或样式的更改能立即生效,而无需重新构建 Java 应用程序。
为此,我们会单独启动 Angular 的开发服务器,并将内嵌浏览器指向本地的 localhost。

在一个终端中启动开发服务器:

./gradlew :desktop-angular-dashboard:startDevServer 

然后在浏览器中加载 URL:

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

内嵌的 Web 视图会连接到本地的开发服务器,使得在 Java 应用程序保持运行的同时也能够支持热重载。

生产模式

在生产模式下,桌面应用需要完全离线运行。依赖本地 Web 服务器不仅会增加复杂度,也带来潜在的安全风险 — 用户可能会在浏览器中加载 Web 应用程序 URL 并查看应用程序的源代码,从而暴露敏感逻辑。我们当然不希望这种情况发生,我们希望用户界面只能在桌面应用程序内部访问,并且其源代码隐藏在应用程序内部。

为此,我们将 Angular 的构建产物直接打包至 JAR 资源目录。Gradle 构建过程会编译 web-app/ 下的 Angular 源码,并将输出复制到 src/main/resources/web/ 目录中。

应用打包后,这些文件可以从类路径(classpath)中的 /web 路径访问,通过 JxBrowser 的自定义协议拦截 API 从类路径中直接加载资源。

为了让请求拦截器能够处理 Web 资源请求,我们为其分配一个自定义协议:

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

Angular 应用被直接打包到 Java 资源中,并由 JxBrowser 加载。这使得 UI 部分完全独立封装:

  • 所有 HTML、CSS 和 JavaScript 文件都从应用 JAR 包中读取
  • 无需依赖任何外部服务器
  • 源代码不会被浏览器开发者工具审查

这种方案确保了应用的高性能、安全性与可移植性,在 Windows、macOS 和 Linux 三大操作系统上均能稳定运行。

定义应用配置

创建一个类,用于确定当前运行模式,并提供应用程序 URL:

publicfinalclassAppDetails{...publicstaticStringappUrl(){returnisDevMode()?"http://localhost:"+ DEV_SERVER_PORT : APP_SCHEME +"://"+ APP_HOST;}publicstaticbooleanisDevMode(){return"true".equals(System.getProperty("app.dev.mode"));}...}

通过一个系统属性 app.dev.mode 在两种模式之间进行切换,使得在快速开发迭代和安全的生产部署之间切换变得非常简单。

从 JAR 中提供 Angular 文件

本节介绍如何构建一个拦截器,用于从 JAR 中提供 Angular 文件。

要加载打包在 JAR 内部的文件,您需要使用 JxBrowser 的 InterceptUrlRequestCallback
实现此接口的拦截器会在每一次网络请求发出时被调用,允许您用从类路径读取内容的自定义响应替换默认网络行为。

创建拦截器

首先,创建一个拦截器,将请求的路径解析为对应的文件:

importcom.teamdev.jxbrowser.net.callback.InterceptUrlRequestCallback;importjava.net.URI;finalclassUrlRequestInterceptorimplementsInterceptUrlRequestCallback{@OverridepublicResponseon(Params params){var uri = URI.create(params.urlRequest().url());var path = uri.getPath();var fileName = path.equals("/")?"/index.html": path;// 我们将在这里加载资源。returnResponse.proceed();}}

处理文件请求

在生产模式下,Angular 的构建产物(HTML、CSS 和 JavaScript 文件)会打包到 JAR 内部的 /web 目录下。

拦截器需要完成以下工作:

  1. 从类路径 classpath 中读取相应的文件
  2. 以正确的 Content-Type 作为 HTTP 响应返回

添加以下逻辑来读取文件并将其作为响应返回:

importcom.teamdev.jxbrowser.net.HttpHeader;...finalclassUrlRequestInterceptorimplementsInterceptUrlRequestCallback{@OverridepublicResponseon(Params params){var uri = URI.create(params.urlRequest().url());var path = uri.getPath();var fileName = path.equals("/")?"/index.html": path;returnloadResource(params, fileName);}/** * 从 classpath 读取文件,并将其作为 HTTP 响应返回。 */privateResponseloadResource(Params params,String fileName){try(var stream =getClass().getResourceAsStream("/web"+ fileName)){if(stream ==null){var job =createUrlRequestJob(params,HttpStatus.NOT_FOUND); job.complete();returnResponse.intercept(job);}var job =createUrlRequestJob(params,HttpStatus.OK); job.write(stream.readAllBytes()); job.complete();returnResponse.intercept(job);}catch(IOException e){var job =createUrlRequestJob(params,HttpStatus.INTERNAL_SERVER_ERROR); job.complete();returnResponse.intercept(job);}}/** * 使用指定的 HTTP 状态创建一个 UrlRequestJob。 */privateUrlRequestJobcreateUrlRequestJob(Params params,HttpStatus status){var options =UrlRequestJob.Options.newBuilder(status).build();return params.newUrlRequestJob(options);}}

loadResource 方法使用 getResourceAsStream 从类路径读取文件,包括打包在 JAR 内的资源。
有关拦截请求的更多详细信息,请参阅加载本地内容教程。

在下一节中,我们将添加 Content-Type 头,以便浏览器能够正确显示内容。

添加 MIME 类型支持

为了发送正确的 Content-Type 头,引入一个小型工具类,用于根据文件扩展名推断 MIME 类型,并在构建 HTTP 响应时使用它:

importcom.teamdev.jxbrowser.net.MimeType;...finalclassMimeTypes{privatestaticfinalMap<String,MimeType> MIME_TYPES =loadMimeTypes();staticMimeTypemimeType(String fileName){int dotIndex = fileName.lastIndexOf('.');if(dotIndex <0|| dotIndex == fileName.length()-1){returnMimeType.of("application/octet-stream");}String extension = fileName.substring(dotIndex +1).toLowerCase();return MIME_TYPES.getOrDefault(extension,MimeType.of("application/octet-stream"));}privatestaticMap<String,MimeType>loadMimeTypes(){Map<String,MimeType> mimeTypes =newHashMap<>();URL propsUrl =MimeTypes.class.getClassLoader().getResource("mime-types.properties");if(propsUrl !=null){Properties props =newProperties();try(InputStream inputStream = propsUrl.openStream()){ props.load(inputStream); props.forEach((key, value)-> mimeTypes.put(key.toString(),MimeType.of(value.toString())));}catch(IOException e){// 回退到默认值。}}return mimeTypes;}}

MimeTypes 类使用一个预先填充好的属性文件,用于将扩展名映射到对应的 MIME 类型:

html=text/html css=text/css png=image/png ... 

接下来,在拦截器中使用这个工具类,使每个响应都包含正确的 Content-Type 头:

importcom.teamdev.jxbrowser.net.HttpHeader;...finalclassUrlRequestInterceptorimplementsInterceptUrlRequestCallback{.../** * 使用指定的 HTTP 状态和 Content-Type 头创建一个 UrlRequestJob。 */privateUrlRequestJobcreateUrlRequestJob(Params params,HttpStatus status,String fileName){var mimeType =MimeTypes.mimeType(fileName);var options =UrlRequestJob.Options.newBuilder(status).addHttpHeader(HttpHeader.of("Content-Type", mimeType.value())).build();return params.newUrlRequestJob(options);}}

设置 JavaScript - Java 桥接器

JxBrowser 内置了 JavaScript 与 Java 的直接通信桥,允许 Web UI 直接调用 Java 方法。Angular 前端可通过该桥调用后端接口,Java 后端也能主动向前端发送事件或数据更新。

创建后端类

创建一个将方法暴露给 JavaScript 的类。类本身或其方法必须添加 @JsAccessible 注解:

importcom.teamdev.jxbrowser.js.JsAccessible;@JsAccessiblepublicclassDashboardBackend{publicStringgetAppInfo(){return"Angular Dashboard v1.0";}}

注入 Java 后端对象

使用 InjectJsCallback 将后端对象注入到浏览器中:

importcom.teamdev.jxbrowser.js.JsObject;importcom.teamdev.jxbrowser.browser.callback.InjectJsCallback;DashboardBackend backend =newDashboardBackend(); browser.set(InjectJsCallback.class, params ->{JsObject window = params.frame().executeJavaScript("window");if(window !=null){ window.putProperty("backend", backend);}returnInjectJsCallback.Response.proceed();});

该回调会在每个框架中的任何 JavaScript 执行之前运行。它将后端对象添加到 window 对象中,使其可以在 JavaScript 中通过 window.backend 访问。

从 Angular 调用 Java

在 Angular 端,创建一个服务,用于调用 Java 后端:

// 此接口声明了 JxBrowser 通过 InjectJsCallback 注入到 window 对象中的 Java 后端对象。declare global {interfaceWindow{ backend?:{// JxBrowser 支持 Java 和 JavaScript 之间基本类型和集合(List、Map、Set)的自动类型转换。// 但是,自定义 Java 对象会变成代理对象,每次字段访问都会触发进程间调用。// 这里使用 JSON 字符串以获得更好的性能和兼容性。getTopCards():string;getChartData(timeRange:string):string;};}}@Injectable({ providedIn:'root'})exportclassBackendService{getTopCards(): TopCard[]{if(window.backend){returnJSON.parse(window.backend.getTopCards());}return[];}getChartData(timeRange:string): ChartSeries[]{if(window.backend){returnJSON.parse(window.backend.getChartData(timeRange));}return[];}}

该服务会检查 window.backend 是否存在。在 JxBrowser 环境中运行时,它会调用 Java 方法并解析返回的 JSON 数据。TypeScript 中的 declare global 声明块为注入的 Java 对象提供了类型安全保障。

运行应用程序

开发模式

# 终端 1:启动 Angular 开发服务器 ./gradlew :desktop-angular-dashboard:startDevServer # 终端 2:运行 Java 应用程序 ./gradlew :desktop-angular-dashboard:run 

生产模式

# 构建 Angular 和 Java ./gradlew :desktop-angular-dashboard:jar # 运行 JAR 包 java -jar build/dist/JxBrowserAngularApp-1.0.jar 

启动后,应用程序显示使用 Tailwind CSS 构建的深色主题仪表板。
界面包含现代 UI 组件,包括统计数据、交互式图表、活动动态和数据表格。
所有仪表板数据都通过 JxBrowser 的 JavaScript-Java 桥接器从 Java 后端获取。

在这里插入图片描述

Read more

零代码上手!用 Rokid 灵珠平台,5 步搭建专属旅游 AR 智能体

零代码上手!用 Rokid 灵珠平台,5 步搭建专属旅游 AR 智能体

零代码上手!用 Rokid 灵珠平台,5 步搭建专属旅游 AR 智能体 灵珠平台简介 okid 自研 AI 开发平台,基于多模态大模型与轻量化架构,打造零门槛、全栈化 AI 开发体系。平台提供可视化编排、预置能力组件,支持原型到云端、端侧一站式敏捷部署,并深度适配 Rokid Glasses 智能眼镜,通过专属硬件接口与低功耗优化,实现 AI 应用高效端侧落地,助力开发者快速打造视觉识别、语音交互等穿戴式 AI 应用,拓展 AI + 物理世界的交互边界可视化编排工具,拖拽式快速搭建应用预置丰富能力组件库,涵盖对话引擎、视觉识别等核心模块支持从原型设计到云端、端侧的一站式敏捷部署提供设备专属适配接口,实现硬件深度协同搭载低功耗运行优化方案,保障端侧持久稳定运行 实战:搭建旅游类AR智能体 1、进入灵珠平台 登录灵珠平台后,你将看到简洁直观的工作台界面 点击创建智能体按钮,

宇树G1机器人强化学习训练完整实战教程

宇树G1机器人强化学习训练完整实战教程

0. 前言 人形机器人的运动控制一直是机器人领域的重要挑战,而强化学习为解决这一问题提供了强有力的工具。本教程将基于宇树G1人形机器人,从基础的强化学习环境搭建开始,逐步深入到高自由度模型的训练配置、奖励函数设计与优化,最终实现复杂动作的训练控制。作者看到一个很棒的系列,所以针对性的对文章内容进行了整理和二次理解,方便大家更好的阅读《不同自由度的宇树G1机器人强化学习训练配置及运行实战 + RSL-RL代码库问题修复》、《宇树G1机器人强化学习训练奖励函数代码架构 + 创建新的奖励函数(1)》、《RL指标分析与看板应用 — 宇树G1机器人高自由度模型强化学习训练实战(3)》、《调参解析 — 宇树G1机器人高自由度模型强化学习训练实战(4)》、《舞蹈训练?手撕奖励函数 — 宇树G1机器人高自由度模型强化学习训练实战(5)》。 1. 强化学习训练环境配置 1.1 基础环境搭建 宇树机器人的强化学习训练基于Isaac Gym物理仿真环境和RSL-RL强化学习框架。首先需要确保这两个核心组件正确安装和配置。 在开始训练之前,我们通过简单的命令来启动12自由度G1机器人的基础训练:

Seedance 2.0 × 飞书机器人深度集成:从API鉴权到上下文感知对话,97%开发者忽略的4个关键配置陷阱

第一章:Seedance 2.0 × 飞书机器人深度集成:从API鉴权到上下文感知对话,97%开发者忽略的4个关键配置陷阱 飞书机器人Token与Encrypt Key的双向校验陷阱 飞书机器人启用「事件订阅」后,必须同时验证 token(用于签名比对)与 encrypt_key(用于消息解密),但多数开发者仅配置了前者。若 encrypt_key 为空或未在服务端正确初始化,飞书将返回 400 Bad Request,且错误日志不显式提示原因。 // Go 示例:初始化飞书加解密器(需显式传入 encrypt_key) cipher, err := larksuite.NewAesCipher("your_encrypt_key_here") // ⚠️ 此处不可省略 if err != nil

零成本搭建飞书机器人:手把手教你用Webhook实现高效消息推送

1. 为什么你需要一个飞书机器人? 在日常工作中,我们经常需要处理各种通知需求。比如系统报警、任务提醒、审批结果通知等等。传统的解决方案包括短信、邮件或者第三方推送平台,但这些方式要么成本高,要么实时性差。飞书机器人提供了一种零成本、高效率的替代方案。 我去年负责的一个ERP系统升级项目就遇到了这个问题。当时我们需要在关键业务流程节点给不同部门的同事发送实时通知。如果使用短信,按照每天200条计算,一个月就要花费上千元。后来我们改用飞书机器人,不仅完全免费,还能实现更丰富的消息格式和精准的@提醒功能。 飞书机器人本质上是一个自动化程序,它通过Webhook技术接收外部系统的消息,并转发到指定的飞书群聊中。这种机制特别适合企业内部系统与飞书之间的集成,比如: * 运维报警通知 * 审批流程提醒 * 业务系统状态更新 * 日报/周报自动推送 * 数据监控预警 2. 5分钟快速创建你的第一个机器人 创建飞书机器人非常简单,不需要任何开发经验。下面我以电脑端操作为例,手把手带你完成整个过程。 首先打开飞书客户端,进入你想要添加机器人的群聊。点击右上角的"..."菜单,