使用 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 目录下。
拦截器需要完成以下工作:
- 从类路径 classpath 中读取相应的文件
- 以正确的
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 后端获取。