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

生成的文件结构如下:

J2CL 项目结构

关键文件说明:

  • index.html – 发送到浏览器的页面
  • j2cl-task-manager.css – 样式文件
  • MyJ2CLApp.java主 Java 类
  • MyJ2CLApp.native.js – JavaScript 入口点
  • MyJ2CLAppTest.java – 包含 @J2clTestInput 注解的测试
  • web.xml – Java EE (Jakarta EE) 部署描述符
  • pom.xml集成 J2CL、Elemental2JSInteropJetty 服务器

⚠️ 本教程中 web.xmlMyJ2CLAppTest.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 21 等新版运行转译过程
  • 代码必须严格遵循 Java 8 语法和特性

此外,pom.xml 中所有依赖基于 Java 11,随意修改可能导致兼容性问题(如 Jetty 版本不匹配)。

2.3. 构建并在浏览器查看结果

这种开发模式能显著减少周转时间:

  1. 终端 1 运行:mvn j2cl:watch
  2. 等待提示:"Build Complete: ready for browser refresh"
  3. 终端 2 运行:mvn jetty:run
  4. 等待提示:"Started Jetty Server"
  5. 保持两个终端运行
  6. 浏览器访问:http://localhost:8080/

✨ 任何 Java 代码修改会立即反映到 JavaScript 中。若需生成最终优化版 JS 文件,改用 mvn j2cl:build

原型生成的演示页面

开发时建议保持开发者工具打开并禁用浏览器缓存。更多细节参考 J2CL Maven 插件文档

2.4. 自定义 Web 页面

这是任务管理器的 index.html。为保持 Maven 目标正常工作,CSS 和 JavaScript 文件路径必须与原型一致

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    [... 其他可选 meta 标签,如移动设备适配、Google Translate、robots 等 ...]
    <title>J2CL 任务管理器</title>
    <link rel="stylesheet" href="css/j2cl-task-manager.css">
</head>
<body>
    <h1>任务管理器</h1>
    <input type="text" id="taskInput" placeholder="输入任务" />
    <button id="addTask">添加任务</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 获取任务,实现会话恢复
  • 本地和服务器同步删除任务,确保数据一致性

3.1. 可用类与注解文档

J2CL 是 Google Web Toolkit (GWT) 的演进版本,包含浏览器环境模拟的标准 Java 库子集。支持类/方法列表见 GWT 模拟参考

进阶功能依赖:

  • Elemental2:操作浏览器 DOM 和调用外部 API
  • JSInterop:通过注解实现 Java-JavaScript 桥接

深入学习推荐:

3.2. JavaScript 入口点

MyJ2CLApp.native.js 内容:

setTimeout(function(){
  var ep = new MyJ2CLApp();
  ep.onModuleLoad()
}, 0);

onModuleLoad() 模拟 GWT 行为,作为应用入口点setTimeout(..., 0) 是 JavaScript 常用技巧,确保在当前执行栈清空后运行,避免 Java 类未加载导致的意外。

3.3. MyJ2CLApp 类序列图

通过序列图理解代码执行流程:

任务管理器序列图

为简化,未处理多浏览器并发操作同一任务列表的复杂场景。

4. 方法实现

以下是 MyJ2CLApp.java 核心方法概览,包含连接 Java 与 JavaScript 的 JSInterop 注解。主要逻辑包括:

完整代码参考:MyJ2CLApp.java

4.1. JSInterop 注解

@JsType 注解类使其可被 JavaScript 访问。*@JsMethod* 允许将 JavaScript 静态方法 JSON.stringify(...) 作为 native Java 静态方法使用:

@JsType
public class MyJ2CLApp {
    // 使用 @JsMethod 封装 JSON.stringify 示例
    @JsMethod(namespace = JsPackage.GLOBAL, name = "JSON.stringify")
    private static native String jsonStringify(Object obj);

    // ...
}

4.2. onModuleLoad()

应用启动时检查 URL 是否包含 uuid 参数:

  • 若存在,从服务器获取任务
  • 绑定"添加任务"按钮事件
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) {
                // ...
                // 填充 JsArray 并更新 UI
            }
            return null;
        })
        .catch_(error -> {
            showErrorMessage("无法获取任务,请检查控制台");
            return null;
        });
}

4.4. addTaskToUI()

辅助方法:为每个任务创建带"删除"按钮的

  • 元素:

    private void addTaskToUI(String taskText) {
        HTMLLIElement taskItem = (HTMLLIElement) DomGlobal.document.createElement("li");
        taskItem.textContent = taskText;
    
        HTMLButtonElement deleteButton = (HTMLButtonElement) DomGlobal.document.createElement("button");
        deleteButton.textContent = "删除";
        deleteButton.addEventListener("click", e -> {
            // ...
            // 从 JsArray 移除任务,同步服务器,更新 UI
        });
    
        taskItem.appendChild(deleteButton);
        taskList.appendChild(taskItem);
    }
    

    4.5. sendTask()

    添加新任务时,根据 uuid 是否存在决定:

    • 无 uuid:POST 创建新对象 → PUT 更新数组
    • 有 uuid:直接 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,服务器生成 id 存入 uuid

    private Promise<Object> createObjectOnServer() {
        // 构建 JSON 请求体
        JsPropertyMap<Object> jsonBody = JsPropertyMap.of();
        jsonBody.set("data", tasksArray);
        // ...
        return DomGlobal.fetch(API_URL, requestInit)
          .then(response -> response.json())
          .then(result -> {
              uuid = [...]; // 从响应提取
              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 持续访问同一任务列表: 正常网络测试结果

    断网错误测试

    断网错误测试结果

    所有功能按预期工作。

    6. 结论

    本文展示了如何:

    1. 使用 Maven 搭建 J2CL 项目
    2. 自定义简单 Web 页面
    3. 通过 Java 和 JavaScript 实现任务管理器
    4. 连接 RESTful 后端进行任务存储/检索
    5. 使用 Elemental2 和 JSInterop 动态更新 UI

    这让我们无需离开 Java 生态即可构建现代 Web 应用。我们还学习了如何利用 Maven 插件监听文件变更、实时重建转译输出,并在浏览器中快速测试。


  • 原始标题:Introduction to J2CL | Baeldung