Files
contract-manager/docs/task/client_server_tasker_communication_rules.md
songqq ad42a49858 docs(task): 更新任务通信规则文档并添加任务注册描述
添加任务注册信息的描述字段到tasker_mapper.json
完善WebSocket通信机制文档,补充核心组件说明
修正属性同步机制中的空指针问题
优化代码格式和注释
2025-09-25 09:56:27 +08:00

521 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 客户端 Tasker 至 服务器端 Tasker 通信规则与逻辑
本文档总结了 Contract-Manager 项目中客户端 Tasker 与服务器端 Tasker 之间的通信规则、调用逻辑和实现模式,基于对以下文件的分析:
- `d:\idea-workspace\Contract-Manager\server\src\main\java\com\ecep\contract\ds\customer\tasker\CompanyCustomerEvaluationFormUpdateTask.java`
- `d:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\controller\customer\CompanyCustomerEvaluationFormUpdateTask.java`
- `d:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\controller\customer\CustomerTabSkinFile.java`
## 1. 架构设计原则
项目采用了清晰的客户端-服务器分离架构,任务处理遵循以下原则:
- **客户端轻量级**:负责任务发起、参数传递和结果展示
- **服务器端重量级**:负责实际业务逻辑处理和数据操作
- **WebSocket通信**使用WebSocket实现客户端与服务器端的任务通信和进度同步
### 1.1 核心通信组件
#### WebSocketClientService
`WebSocketClientService`类是客户端WebSocket通信的核心服务组件负责建立、维护与服务器的WebSocket连接并提供消息发送和接收的功能。主要职责包括
- **连接管理**初始化WebSocket连接、处理连接关闭和重连逻辑
- **心跳维护**:定期发送心跳消息保持连接活跃
- **会话管理**创建和管理WebSocket会话`WebSocketClientSession`
- **消息路由**:接收并路由服务器消息到对应的会话处理器
- **服务调用**:提供`invoke`方法调用服务器端服务
#### WebSocketClientSession
`WebSocketClientSession`类代表一个特定的WebSocket会话每个Tasker任务执行时都会创建一个对应的会话实例。主要职责包括
- **会话标识**维护唯一的会话ID
- **任务提交**将Tasker任务提交到服务器端执行
- **消息处理**:处理服务器返回的各类消息(包括进度更新、属性更新、状态变更等)
- **属性同步**根据服务器消息更新Tasker的属性值
## 2. 类命名与结构规范
### 2.1 命名规则
- 客户端与服务器端的对应Tasker类**必须使用相同的类名**(如示例中的`CompanyCustomerEvaluationFormUpdateTask`
- 客户端Tasker位于`client`模块的控制器包下
- 服务器端Tasker位于`server`模块的tasker包下
### 2.2 任务名称注册规则
- 所有服务器端Tasker类必须通过配置文件`tasker_mapper.json`进行注册,并由`WebSocketServerTaskManager`类在`afterPropertiesSet`方法中加载
- 注册格式为`"TaskClassName": "fully.qualified.ClassName"`
- 注册的Task名称将用于客户端和服务器端之间的任务识别
- 配置文件`tasker_mapper.json`应位于`src/main/resources`目录下,格式如下:
```json
{
"taskers": {
"TaskClassName": "fully.qualified.ClassName",
// 更多任务映射...
}
}
```
#### 任务名称注册实现示例
```java
@Override
public void afterPropertiesSet() throws Exception {
// 从tasker_mapper.json文件中加载任务注册信息
try {
Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json");
try (InputStream inputStream = resource.getInputStream()) {
JsonNode rootNode = objectMapper.readTree(inputStream);
JsonNode taskersNode = rootNode.get("taskers");
if (taskersNode != null && taskersNode.isObject()) {
Map<String, String> taskMap = new java.util.HashMap<>();
taskersNode.fields().forEachRemaining(entry -> {
taskMap.put(entry.getKey(), entry.getValue().asText());
});
taskClzMap = taskMap;
}
}
} catch (Exception e) {
logger.error("Failed to load tasker_mapper.json", e);
// 使用默认值作为fallback
taskClzMap = Map.of();
}
}```
### 2.3 接口实现区分
- 客户端Tasker实现`WebSocketClientTasker`接口
- 服务器端Tasker实现`WebSocketServerTasker`接口
### 2.4 继承关系
- 客户端和服务器端Tasker均继承自`Tasker<Object>`基类
## 3. 客户端Tasker实现规则
客户端Tasker是任务的发起方需要遵循以下实现规则
### 3.1 核心属性
- 通常包含一个可设置的业务对象如示例中的`@Setter private CompanyCustomerVo customer;`
- 配置Logger日志记录器
### 3.2 核心方法实现
- **getTaskName()**返回任务名称通常使用类名
- **updateProgress()**继承或重写进度更新方法
- **execute()**调用`callRemoteTask()`方法将任务发送到服务器端传递必要参数, 参数类型只允许基本类和Vo类对象
### 3.3 示例实现
```java
public class CompanyCustomerEvaluationFormUpdateTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerEvaluationFormUpdateTask.class);
@Setter
private CompanyCustomerVo customer; // 业务对象
@Override
public String getTaskName() {
return getClass().getSimpleName();
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("客户评估表更新任务"); // 设置任务标题
// 调用远程任务传递locale和业务对象ID
return callRemoteTask(holder, getLocale(), customer.getId());
}
}
```
## 4. 服务器端Tasker实现规则
服务器端Tasker是任务的实际执行者需要遵循以下实现规则
### 4.1 参数接收
- 实现`init(JsonNode argsNode)`方法接收客户端传递的参数
- 从参数中提取业务对象ID并加载完整业务对象
### 4.2 服务获取
- 通过`getCachedBean(Service.class)`方法获取所需的服务实例
- 可以提供辅助方法封装服务获取逻辑
### 4.3 任务执行
- 实现`execute(MessageHolder holder)`方法包含实际业务逻辑
- 使用`holder.info()/error()`等方法记录任务执行状态
- 调用`updateProgress()`方法更新任务进度
- 调用`updateTitle()`方法更新任务标题
- 调用`updateProperty()`方法更新任务属性
- `holder``updateProgress()``updateTitle()``updateProperty()` 等方法通过 webSocket 把数据传输到 client 端的 taskerclient 端的 tasker 收到数据后会调用对应的方法更新任务属性
### 4.4 示例实现
```java
public class CompanyCustomerEvaluationFormUpdateTask extends Tasker<Object> implements WebSocketServerTasker {
private CompanyCustomer customer; // 业务对象
@Override
public void init(JsonNode argsNode) {
// 从参数中提取业务对象ID并加载
int customerId = argsNode.get(0).asInt();
customer = getCachedBean(CompanyCustomerService.class).findById(customerId);
}
// 辅助方法获取服务
CompanyCustomerFileService getCompanyCustomerFileService() {
return getCachedBean(CompanyCustomerFileService.class);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 执行实际业务逻辑
updateEvaluationForm(holder);
return null;
}
// 具体业务逻辑实现
public void updateEvaluationForm(MessageHolder holder) {
// 业务逻辑代码...
updateProgress(1, 10); // 更新进度
// 更多业务逻辑...
}
}
```
## 5. 客户端Tasker调用流程
在UI控制器中调用客户端Tasker的标准流程如下
### 5.1 实例化与参数设置
1. 创建客户端Tasker实例
2. 设置必要的业务对象参数
### 5.2 任务执行与监控
1. 使用`UITools.showTaskDialogAndWait()`显示任务对话框
2. 调用`initializeTask()`初始化任务执行环境
3. 通过消费者函数处理任务消息
### 5.3 任务完成处理
1. 任务完成后执行必要的数据刷新操作
### 5.4 示例调用
```java
public void onUpdateEvaluationFormAction(ActionEvent event) {
// 1. 创建Tasker并设置参数
CompanyCustomerEvaluationFormUpdateTask task = new CompanyCustomerEvaluationFormUpdateTask();
task.setCustomer(getCompanyCustomerService().findById(viewModel.getId().get()));
// 2. 显示任务对话框并执行任务
UITools.showTaskDialogAndWait("更新评价表", task, consumer -> {
initializeTask(task, "更新评价表", msg -> consumer.accept(Message.info(msg)));
});
// 3. 任务完成后刷新数据
loadTableDataSet();
}
```
## 6. 任务进度管理
- 服务器端使用`updateProgress(current, total)`方法更新任务进度
- 进度值通常以0-1000或类似小范围数值表示完成百分比
- 客户端通过WebSocket接收进度更新并显示
## 7. 异常处理机制
- 服务器端使用`MessageHolder.error()`方法记录错误信息
- 客户端通过任务对话框展示错误信息
- 服务器端在关键操作点进行异常捕获和处理
## 9. 数据一致性保障
- 任务完成后客户端通常调用数据刷新方法(如`loadTableDataSet()`确保UI显示最新数据
- 服务器端负责业务数据的持久化操作
## 10. WebSocketServerTaskManager功能与作用
`WebSocketServerTaskManager`是服务器端管理WebSocket任务的核心组件负责处理客户端发起的任务请求创建并执行对应的任务实例并维护任务执行过程中的双向通信。
### 10.1 核心职责
- **任务注册管理**:从`tasker_mapper.json`文件加载并管理任务名称与对应实现类的映射关系
- **消息处理分发**:处理客户端发送的任务创建请求
- **任务实例化**根据任务名称动态创建服务器端Tasker实例
- **处理器设置**为Tasker设置各类回调处理器实现服务器端到客户端的消息推送
- **异步任务执行**:使用虚拟线程异步执行任务
- **消息通信**:维护任务执行过程中的双向通信,包括进度更新、状态通知等
- **异常处理**:处理任务执行过程中的异常情况并通知客户端
### 10.2 任务注册机制
`WebSocketServerTaskManager`在初始化阶段(通过实现`InitializingBean`接口的`afterPropertiesSet()`方法)从`tasker_mapper.json`文件中加载任务注册信息:
```java
@Override
public void afterPropertiesSet() throws Exception {
// 从tasker_mapper.json文件中加载任务注册信息
try {
Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json");
try (InputStream inputStream = resource.getInputStream()) {
JsonNode rootNode = objectMapper.readTree(inputStream);
JsonNode taskersNode = rootNode.get("taskers");
if (taskersNode != null && taskersNode.isObject()) {
Map<String, String> taskMap = new java.util.HashMap<>();
taskersNode.fields().forEachRemaining(entry -> {
taskMap.put(entry.getKey(), entry.getValue().asText());
});
taskClzMap = taskMap;
}
}
} catch (Exception e) {
logger.error("Failed to load tasker_mapper.json", e);
// 使用默认值作为fallback
taskClzMap = Map.of();
}
}
```
#### 10.2 消息处理流程
`WebSocketServerTaskManager`的消息处理流程如下:
1. **接收消息**:通过`onMessage`方法接收来自WebSocketSession的消息
2. **解析会话ID**:从消息中提取会话标识
3. **分发处理**:调用`handleAsSessionCallback`根据消息类型进行分发
4. **异常处理**:捕获处理过程中的异常并发送错误消息
```java
public void onMessage(WebSocketSession session, JsonNode jsonNode) {
// 处理 sessionId 的消息
String sessionId = jsonNode.get(WebSocketConstant.SESSION_ID_FIELD_NAME).asText();
try {
handleAsSessionCallback(session, sessionId, jsonNode);
} catch (Exception e) {
sendError(session, sessionId, e.getMessage());
logger.warn("处理会话回调失败 (会话ID: {}): {}", sessionId, e.getMessage(), e);
}
}
private void handleAsSessionCallback(WebSocketSession session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has("type")) {
throw new IllegalArgumentException("缺失 type 参数");
}
String type = jsonNode.get("type").asText();
if (type.equals("createTask")) {
createTask(session, sessionId, jsonNode);
}
}
private void sendError(WebSocketSession session, String sessionId, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
String errorMessage = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
WebSocketConstant.SUCCESS_FIELD_VALUE, false,
WebSocketConstant.MESSAGE_FIELD_NAME, message));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
```
#### 10.3 任务创建与初始化
`createTask()`方法负责创建和初始化任务实例:
1. **参数提取**:从消息中提取任务名称和参数
2. **类型查找**:根据任务名称查找对应的类名
3. **实例创建**:通过反射创建任务实例
4. **国际化设置**设置任务的Locale信息
5. **处理器绑定**:为任务绑定各类消息处理器
6. **参数初始化**:调用任务的`init()`方法进行参数初始化
7. **异步执行**:使用虚拟线程启动任务执行
```java
private void createTask(WebSocketSession session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has(WebSocketConstant.ARGUMENTS_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 " + WebSocketConstant.ARGUMENTS_FIELD_NAME + " 参数");
}
JsonNode argsNode = jsonNode.get(WebSocketConstant.ARGUMENTS_FIELD_NAME);
String taskName = argsNode.get(0).asText();
String clzName = taskClzMap.get(taskName);
if (clzName == null) {
throw new IllegalArgumentException("未知的任务类型: " + taskName);
}
Object tasker = null;
try {
Class<?> clz = Class.forName(clzName);
tasker = clz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("未知的任务类型: " + taskName + ", class: " + clzName);
} catch (Exception e) {
throw new IllegalArgumentException("任务类型: " + taskName + ", class: " + clzName + " 实例化失败");
}
if (tasker instanceof Tasker<?> t) {
String locale = argsNode.get(1).asText();
t.setLocale(Locale.forLanguageTag(locale));
}
if (tasker instanceof WebSocketServerTasker t) {
t.setTitleHandler(title -> sendToSession(session, sessionId, "title", title));
t.setMessageHandler(msg -> sendMessageToSession(session, sessionId, msg));
t.setPropertyHandler((name, value) -> sendToSession(session, sessionId, "property", name, value));
t.setProgressHandler((current, total) -> sendToSession(session, sessionId, "progress", current, total));
t.init(argsNode.get(2));
}
if (tasker instanceof Callable<?> callable) {
Thread.ofVirtual().start(() -> {
try {
sendToSession(session, sessionId, "start");
callable.call();
sendToSession(session, sessionId, "done");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
```
#### 10.4 消息通信机制
`WebSocketServerTaskManager`提供了多种消息发送方法,实现服务器端到客户端的通信:
- **`sendToSession()`**发送各类消息title、property、progress、start、done等
- **`sendMessageToSession()`**:专门发送带级别的消息
- **`sendError()`**:发送错误消息给客户端
```java
private boolean sendToSession(WebSocketSession session, String sessionId, String type, Object... args) {
try {
String text = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
"type", type,
WebSocketConstant.ARGUMENTS_FIELD_NAME, args));
session.sendMessage(new TextMessage(text));
} catch (IOException e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
return true;
}
private boolean sendMessageToSession(WebSocketSession session, String sessionId, Message msg) {
return sendToSession(session, sessionId, "message", msg.getLevel().getName(), msg.getMessage());
}
private void sendError(WebSocketSession session, String sessionId, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
String errorMessage = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
WebSocketConstant.SUCCESS_FIELD_VALUE, false,
WebSocketConstant.MESSAGE_FIELD_NAME, message));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
#### 10.5 与WebSocketServerHandler的关系
`WebSocketServerTaskManager`与`WebSocketServerHandler`协同工作共同处理客户端的WebSocket请求
- `WebSocketServerHandler`作为WebSocket消息的入口点接收所有客户端消息
- 对于与任务相关的消息(如创建任务),`WebSocketServerHandler`将消息转发给`WebSocketServerTaskManager`处理
- `WebSocketServerTaskManager`负责具体的任务创建、初始化和执行逻辑
- 两者结合实现了完整的客户端-服务器端Tasker通信机制
## 11. WebSocketClientService工作机制
### 11.1 连接管理流程
`WebSocketClientService`的连接管理流程包括以下几个核心步骤:
1. **初始化连接**:通过`initWebSocket()`方法创建与服务器的WebSocket连接
2. **连接状态监控**:使用`online`和`message`属性实时反映连接状态
3. **自动重连机制**:当连接断开时,通过`scheduleReconnect()`方法安排自动重连
4. **心跳维护**:通过`startHeartbeat()`和`heartbeat()`方法定期发送ping消息
### 11.2 消息处理机制
`WebSocketClientService`使用内置的`WebSocketListener`处理各类WebSocket事件
- `onOpen`:连接建立时触发,启动心跳并更新连接状态
- `onMessage`:接收服务器消息时触发,解析并路由消息到对应处理器
- `onClosing`/`onClosed`:连接关闭时触发,停止心跳并准备重连
- `onFailure`:连接失败时触发,记录错误并尝试重连
### 11.3 消息路由策略
`WebSocketClientService`采用以下策略路由接收到的消息:
1. **回调消息**:如果消息包含`messageId`,则路由到对应的`CompletableFuture`回调
2. **会话消息**:如果消息包含`sessionId`,则路由到对应的`WebSocketClientSession`处理
3. **错误消息**:如果消息包含错误码,则按错误类型进行处理(如未授权时自动重新登录)
## 12. WebSocketClientSession工作机制
### 12.1 会话生命周期管理
每个`WebSocketClientSession`实例的生命周期包括:
1. **创建**:通过`WebSocketClientService.createSession()`方法创建
2. **任务提交**:通过`submitTask()`方法提交Tasker任务到服务器
3. **消息处理**:处理服务器返回的各类消息
4. **关闭**:任务完成后通过`close()`方法关闭会话
### 12.2 消息类型处理
`WebSocketClientSession`能够处理以下几种主要的消息类型:
- **message**:普通文本消息,通过`handleAsMessage()`处理
- **title**:任务标题更新,通过`handleAsTitle()`处理
- **property**:任务属性更新,通过`handleAsProperty()`处理
- **progress**:任务进度更新,通过`handleAsProgress()`处理
- **start**:任务开始通知,通过`handleAsStart()`处理
- **done**:任务完成通知,通过`handleAsDone()`处理
### 12.3 属性同步机制
`WebSocketClientSession`的`handleAsProperty()`方法实现了服务器到客户端的属性同步机制:
1. 接收属性名和属性值
2. 通过Java反射获取Tasker类的属性描述符
3. 将接收到的属性值转换为正确的Java类型
4. 调用属性的setter方法更新Tasker实例的属性值
## 13. 最佳实践建议
1. **任务拆分**:复杂任务应拆分为多个小任务,便于进度跟踪和错误定位
2. **状态反馈**:在关键节点提供清晰的状态信息,增强用户体验
3. **资源释放**:确保文件流等资源正确关闭,避免资源泄露
4. **事务控制**:对于涉及多步数据操作的任务,考虑使用事务确保数据一致性
5. **错误重试**:针对网络波动等临时性问题,考虑实现任务重试机制
6. **配置管理**:使用`tasker_mapper.json`文件统一管理任务注册信息,避免在代码中硬编码任务映射
7. **异常处理**确保实现配置文件加载失败的fallback机制保证系统稳定性
8. **版本控制**:对任务配置文件进行版本控制,便于追踪变更历史
9. **会话管理**确保任务完成后正确关闭WebSocket会话避免资源泄露
10. **属性同步**:合理使用属性同步机制,确保客户端显示与服务器状态一致
通过遵循以上规则和模式可以确保Contract-Manager项目中客户端与服务器端Tasker通信的一致性、可靠性和可维护性。