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 – 样式文件
- MyJ2CLApp.java – 主 Java 类
- MyJ2CLApp.native.js – JavaScript 入口点
- MyJ2CLAppTest.java – 包含 @J2clTestInput 注解的测试
- web.xml – Java EE (Jakarta EE) 部署描述符
- pom.xml – 集成 J2CL、Elemental2、JSInterop 和 Jetty 服务器
⚠️ 本教程中 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 21 等新版运行转译过程
- ❌ 代码必须严格遵循 Java 8 语法和特性
此外,pom.xml 中所有依赖基于 Java 11,随意修改可能导致兼容性问题(如 Jetty 版本不匹配)。
2.3. 构建并在浏览器查看结果
这种开发模式能显著减少周转时间:
- 终端 1 运行:
mvn j2cl:watch
- 等待提示:
"Build Complete: ready for browser refresh"
- 终端 2 运行:
mvn jetty:run
- 等待提示:
"Started Jetty Server"
- 保持两个终端运行
- 浏览器访问:
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 注解。主要逻辑包括:
- DOM 操作
- 使用 JsArray 管理任务
- 通过 DomGlobal.fetch(...) 调用 REST API
完整代码参考: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. 浏览器结果页面
正常网络环境测试
断网错误测试
所有功能按预期工作。
6. 结论
本文展示了如何:
- 使用 Maven 搭建 J2CL 项目
- 自定义简单 Web 页面
- 通过 Java 和 JavaScript 实现任务管理器
- 连接 RESTful 后端进行任务存储/检索
- 使用 Elemental2 和 JSInterop 动态更新 UI
这让我们无需离开 Java 生态即可构建现代 Web 应用。我们还学习了如何利用 Maven 插件监听文件变更、实时重建转译输出,并在浏览器中快速测试。