技术 2026.06.01 77 阅读

飞龙审批流一次 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();
    }
}