J2CL 簡介
1. 概述
J2CL允許我們用 Java 編寫 Web 應用程式並將其編譯為最佳化的 JavaScript,使其成為利用 Java 生態系統並針對現代瀏覽器的強大工具。透過將 J2CL 與 Maven 集成,我們可以簡化開發流程並有效地管理依賴關係。
在本教程中,我們將指導如何使用 Maven 設定 J2CL 專案、自訂 Web 介面以及實作簡單任務管理器應用程式的核心功能。我們將探討如何使用 Java 和 JavaScript 互通性與 RESTful 後端進行互動。
為了創建一個完整的工作範例,我們將使用restful-api.dev ,它公開了用於儲存和檢索資料的免費 REST API。
2. 使用 Maven 設定 J2CL 專案結構
J2CL 是一個 Google 項目,使用Bazel作為其預設建置系統。但是,為了簡化專案設定並使用更熟悉的工作流程,我們將使用Vertispan 開發的 Maven 插件。
2.1. j2cl-archetype-simple
最小的 J2CL 專案需要仔細配置pom.xml和其他檔案。要開始使用現成的東西,我們可以使用j2cl-archetype-simple Maven 原型:
mvn archetype:generate -DarchetypeGroupId=com.vertispan.j2cl.archetypes \
 -DarchetypeArtifactId=j2cl-archetype-simple \
 -DarchetypeVersion=0.22.0 \
 -DgroupId=com.baeldung.j2cl.taskmanager \
 -DartifactId=j2cl-task-manager \
 -Dversion=1.0-SNAPSHOT \
 -Dmodule=MyJ2CLApp這些是生成的文件:
    
以下是簡要總結:
-   index.html– 傳送到瀏覽器的頁面
-   j2cl-task-manager.css– CSS
-   MyJ2CLApp.java–主 Java 類
-   MyJ2CLApp.native.js– JavaScript 入口點
-   MyJ2CLAppTest.java– 包含使用@J2clTestInput註解的測試
-   web.xml– Java EE(Jakarta EE)的部署描述符。
-   pom.xml–整合 J2CL、 Elemental2 、 JSInterop和 Jetty Server 進行部署
我們將更詳細地討論這些文件。對於本教程, web.xml和MyJ2CLAppTest.java是不需要的,我們可以刪除它們。
2.2. Java 版本
讓我們來關註一下pom.xml中的這兩行:
<maven.compiler.target>11</maven.compiler.target>
 <maven.compiler.source>11</maven.compiler.source>雖然 J2CL 本身可以處理 Java 11,但 Maven 外掛程式的目前實作強制相容 Java 8。這意味著雖然我們可以使用較新的 JDK(例如 JDK 21)運行轉編譯過程,但我們的程式碼必須嚴格遵守 Java 8 語法和特性。
此外, pom.xml中的所有依賴項都是基於 Java 11,因此我們不想觸碰任何東西,以免破壞它們。例如,如果我們指定了 Java 的更高版本,那麼原型中包含的 Jetty 版本將不再起作用。
2.3.在瀏覽器中建置並查看結果
這種方法顯著減少了開發週轉時間,因為 Java 程式碼的變更會自動重新編譯並反映在瀏覽器中,從而實現更有效率的工作流程:
-   開啟終端機並執行: mvn j2cl:watch
-   等待訊息: “Build Complete: ready for browser refresh”
-   打開另一個終端機並運行: mvn jetty:run
-   等待訊息: “Started Jetty Server”
- 讓兩個終端機都保持開啟狀態
-   在瀏覽器中開啟http://localhost:8080/
這樣,對 Java 程式碼的任何更改都會幾乎立即反映在 JavaScript 程式碼中。如果我們想產生最終優化的 JavaScript 文件,我們可以使用: mvn j2cl:build
這是原型產生的演示頁面。在開發過程中,我們希望保持開發人員工具開啟並且瀏覽器快取被停用:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Demo-archetype.mp4
有關更多信息,**我們可以查閱J2CL Maven 插件目標的文檔**。
2.4.自訂網頁
這是我們的任務管理器的index.html 。為了確保 Maven 目標繼續如預期運作, CSS 和 JavaScript 檔案的路徑必須與原型使用的路徑相同:
<!DOCTYPE html>
 <html>
 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 [... Other optional meta tags, eg for mobile devices, Google Translate, robots, etc. ...]
 <title>J2CL Task Manager</title>
 <link rel="stylesheet" href="css/j2cl-task-manager.css">
 </head>
 <body>
 <h1>Task Manager</h1>
 <input type="text" id="taskInput" placeholder="Enter a task" />
 <button id="addTask">Add Task</button>
 <ul id="taskList"></ul>
 <script src='j2cl-task-manager/j2cl-task-manager.js'></script>
 </body>
 </html>這是j2cl-task-manager.css :
body {
 background-color: #cefb56;
 color: black;
 font-family: Calibri, Arial, sans-serif;
 }
 .deleteButton {
 margin-left: 1rem;
 }
 .errorMessage {
 display: block;
 width: fit-content;
 color: darkred;
 background-color: white;
 border-radius: 1rem;
 border: 1px solid #f5c6cb;
 padding: 0.2rem;
 margin: 0.5rem 0;
 }沒有什麼需要解釋的,到目前為止,它只是非常簡單的 HTML 和 CSS 程式碼。
3.任務管理器應用程式
從高層次來看,我們的任務管理器允許使用者執行以下操作:
- 透過輸入欄位新增任務並將其儲存在記憶體中
- 透過建立新任務列表或更新現有任務列表,使用遠端 REST API 同步任務
- 如果任務清單不存在,應用程式將建立一個新的任務清單並為其指派一個唯一識別碼 (UUID),然後將其儲存在 URL 中以供將來參考
- 使用 UUID 從 API 檢索任務,允許使用者使用相同的任務清單來恢復會話
- 在本機和伺服器上刪除任務,確保UI和後端之間的資料一致性
在深入研究程式碼之前,讓我們先看一下文件。
3.1.可用類別和註解的文檔
J2CL 是 Google Web Toolkit (GWT) 的演變,因此包含了已模擬在瀏覽器環境中使用的標準 Java 程式庫的子集。受支援的類別和方法的列表可以在官方GWT 模擬參考中找到。
但這只是一個起點。我們也有 Elemental2 來與瀏覽器 DOM 互動並與外部 API 通訊。另一方面,JSInterop 透過註解為我們提供了 Java 和 JavaScript 之間的橋樑。
關於 JSInterop 和 Elemental2 的深入了解,我們可以參考GWTcon 的演講:JsInterop、Elemental2 和 2.8 以外的編碼和JsInterop 規格。
3.2. JavaScript 入口點
這是MyJ2CLApp.native.js的內容:
setTimeout(function(){
 var ep = new MyJ2CLApp();
 ep.onModuleLoad()
 }, 0);onModuleLoad()模擬 GWT 的行為,其中**onModuleLoad()方法是應用程式的入口點**。 setTimeout(…, 0)結構通常用於 JavaScript 中,以延遲執行,直到目前執行堆疊被清除。在這種情況下,它可以確保當 Java 類別尚未被 JavaScript 載入時不會出現任何意外。
3.3. MyJ2CLApp類別的序列圖
透過清楚了解程式碼執行流程,我們可以更理解 Java 方法:
    
為了簡單起見,我們不解決同時的複雜性,即同時在不同的瀏覽器上使用相同的任務清單。
4. 實施方法
以下是MyJ2CLApp.java中核心方法的簡要概述,包括將 Java 程式碼與 JavaScript 橋接起來的 JSInterop 註解。大多數邏輯都很簡單:我們操作 DOM、在JsArray中追蹤任務,並呼叫DomGlobal.fetch(…) API 與 REST API 進行互動。
不過,作為參考,**我們可以看看MyJ2CLApp.java 的完整程式碼**。
4.1. JSInterop 註釋
我們用@JsType註解整個類,以便可以從JavaScript程式碼存取它。另一方面, @JsMethod允許我們將 JavaScript 靜態方法JSON.stringify(…)用作本機靜態 Java 方法:
@JsType
 public class MyJ2CLApp {
 // Example of using @JsMethod to wrap JSON.stringify
 @JsMethod(namespace = JsPackage.GLOBAL, name = "JSON.stringify")
 private static native String jsonStringify(Object obj);
 // ...
 }4.2. onModuleLoad()
當應用程式啟動時,我們檢查瀏覽器URL是否包含uuid參數。如果是的話,我們就從伺服器取得相關任務。我們也連接了Add Task按鈕:
public void onModuleLoad() {
 if (uuid != null) {
 fetchTasks();
 }
 addTaskButton.addEventListener("click", event -> {
 String taskText = taskInput.value.trim();
 if (!taskText.isEmpty()) {
 sendTask(taskText);
 taskInput.value = "";
 }
 });
 }4.3. fetchTasks()
我們使用 GET 請求檢索現有的任務清單。 404只是意味著尚未儲存任何任務:
private void fetchTasks() {
 DomGlobal.fetch(API_URL + "/" + uuid)
 .then(response -> {
 if (!response.ok && response.status != 404) {
 throw new Error("HTTP error " + response.status);
 }
 return response.status == 404 ? null : response.json();
 })
 .then(data -> {
 if (data != null) {
 // ...
 // Populate tasks in our JsArray and update the UI
 }
 return null;
 })
 .catch_(error -> {
 showErrorMessage("Could not fetch tasks. Check console.");
 return null;
 });
 }4.4. addTaskToUI()
此方法可協助為每個任務新增一個新的<li> ,並附有一個Delete按鈕:
private void addTaskToUI(String taskText) {
 HTMLLIElement taskItem = (HTMLLIElement) DomGlobal.document.createElement("li");
 taskItem.textContent = taskText;
 HTMLButtonElement deleteButton = (HTMLButtonElement) DomGlobal.document.createElement("button");
 deleteButton.textContent = "Delete";
 deleteButton.addEventListener("click", e -> {
 // ...
 // Remove the task from our JsArray, sync with server, then update the UI
 });
 taskItem.appendChild(deleteButton);
 taskList.appendChild(taskItem);
 }4.5. sendTask()
當使用者新增任務時,我們根據uuid是否已設定來決定是否 POST 一個全新的物件或 PUT 更新的陣列:
private void sendTask(String taskText) {
 tasksArray.push(taskText);
 if (uuid == null) {
 createObjectOnServer()
 .then(ignore -> updateTasksOnServer())
 .then(ignore -> addTaskToUI(taskText))
 // ...
 } else {
 updateTasksOnServer()
 .then(ignore -> addTaskToUI(taskText))
 // ...
 }
 }4.6. createObjectOnServer()和updateTasksOnServer()
createObjectOnServer()執行 POST,讓伺服器產生一個我們儲存在uuid中的id :
private Promise<Object> createObjectOnServer() {
 // Build JSON body and call the server
 JsPropertyMap<Object> jsonBody = JsPropertyMap.of();
 jsonBody.set("data", tasksArray);
 // ...
 return DomGlobal.fetch(API_URL, requestInit)
 .then(response -> response.json())
 .then(result -> {
 uuid = [...]; // Extract from server response
 rewriteURLwithUUID(uuid);
 return null;
 });
 }updateTasksOnServer()反而執行 PUT 來更新現有記錄:
private Promise<Object> updateTasksOnServer() {
 JsPropertyMap<Object> jsonBody = JsPropertyMap.of();
 jsonBody.set("id", uuid);
 jsonBody.set("data", tasksArray);
 // ...
 return DomGlobal.fetch(API_URL + "/" + uuid, requestInit)
 .then(response -> {
 if (!response.ok) {
 throw new Error("HTTP " + response.status);
 }
 return null;
 });
 }4.7. showErrorMessage()
這是一個簡單的實用程序,用於顯示錯誤訊息,然後在幾秒鐘後將其刪除:
private void showErrorMessage(String message) {
 HTMLDivElement errorDiv = (HTMLDivElement) DomGlobal.document.createElement("div");
 errorDiv.textContent = message;
 errorDiv.classList.add("errorMessage");
 addTaskButton.parentNode.insertBefore(errorDiv, taskList);
 DomGlobal.setTimeout((e) -> errorDiv.remove(), 5000);
 }5.瀏覽器結果頁面
第一次測試是在正常條件下進行的,即網路連線正常。我們可以將 URL 新增到書籤,以便始終參考相同的任務清單:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Task-Manager-App-Success.mp4
相反,在第二次測試中,我們斷開網路連線以產生錯誤:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Task-Manager-App-Error.mp4
一切如預期。
6. 結論
在本文中,我們探討如何使用 Maven 設定 J2CL 專案、自訂簡單網頁以及使用 Java 和 JavaScript 實作任務管理器應用程式。我們研究了連接到 RESTful 後端、儲存和檢索任務以及透過 Elemental2 和 JSInterop 使用 DOM 操作動態更新 UI 的基本步驟。
這使得我們無需離開 Java 生態系統即可建立現代 Web 應用程式。我們還了解如何使用 Maven 插件來監視變更、即時重建轉編譯的輸出以及在瀏覽器中快速測試所有內容。
與往常一樣,完整的原始程式碼可在 GitHub 上取得。