飞龙审批流一次 ThreadLocal 泄露导致的流程预览 Bug
某次线上反馈流程预览偶发显示异常数据,排查发现是 ThreadLocal 未清理导致线程池复用污染。
· · ·项目里使用的是企业版的飞龙工作流。某天同事反馈:在审批流中,部分带有条件分支的流程偶发性错乱异常。
诡异的是:
- 无法100%复现
- 出现错误数据时,数据内容**似乎是之前某个流程的表单参数**
- 重启应用后一段时间内正常,运行越久概率越高
- 仅影响预览/查询类操作,实际提交流程的数据是正确的
根据"重启后正常,越久概率越高"和"数据像是之前流程残留"这两个特征,第一反应是线程池复用导致的状态污染。
为了避免在复杂的调用链中层层传递表单参数,系统设计了 FlowDataTransfer 工具类,利用 ThreadLocal 来暂存当前请求的表单和上下文数据
// FlowDataTransfer 核心逻辑
public class FlowDataTransfer {
// 线程本地变量,存储当前请求的流程参数
private static final ThreadLocal<Map<String, Object>> flowData = new ThreadLocal<>();
public static void put(String key, Object value) {
Map<String, Object> dataMap = getAll();
if (null != dataMap) {
dataMap.put(key, value);
} else {
put(new HashMap<String, Object>() {{ put(key, value); }});
}
}
public static Map<String, Object> getAll() {
return flowData.get();
}
public static void remove() {
flowData.remove();
}
}
同时,提供了一个面向业务的包装类 FlowForm,用来将前端传入的 JSON 表单解析并放入 ThreadLocal:
public class FlowForm {
public static void argsTransfer(String formContent) {
FlowDataTransfer.put("flowFormArgs", convertArgs(formContent));
}
// ...
}
问题呼之欲出:
请求中往 ThreadLocal 中写入了数据,但在请求结束时没有显式调用 remove() 清理,那么这些数据就会一直驻留在该线程的 ThreadLocalMap 中。
修复方案:
1.完善清理机制
首先,在 FlowForm 中补充 clear() 方法,提供对 ThreadLocal 的一键清除能力:
public static void formContentTransfer(String formContent) {
FlowDataTransfer.put(FORM_CONTENT, formContent);
}
+ public static void clear() {
+ FlowDataTransfer.remove();
+ }
+
public static Map<String, Object> flowArgs(Map<String, Object> args) {
2.使用try-finally清理 threadlocal
以流程启动服务为例
@Transactional(rollbackFor = Exception.class)
@Override
public Long launchProcess(Long instanceId, ProcessStartDTO dto, FlowCreator flowCreator) {
try {
// 1. 动态节点及表单参数写入 ThreadLocal
checkLaunchPermission(dto.getProcessId(), flowCreator);
if (null != dto.getAssigneeMap()) {
FlowDataTransfer.dynamicAssignee(Collections.unmodifiableMap(dto.getAssigneeMap()));
}
FlowForm.argsTransfer(dto.getProcessForm());
FlowForm.formContentTransfer(dto.getProcessForm());
// 2. 启动流程及保存表单核心逻辑
// ...
return flwInstance.getId();
} finally {
// 3. 无论成功还是抛出 ServiceException 异常,都必须清理 ThreadLocal!
FlowForm.clear();
}
}
完