2019-02-26 · Develop

使用MDC记录Activiti流程信息

从Activiti 5.12开始,SLF4J被用作日志框架,替换了之前使用java.util.logging。需要在 Maven 的 pom.xml 中添加如下的依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>

同时在 5.13 版本中支持了 SLF4j 的 MDC 功能。通过查看 org.activiti.engine.logging.LogMDC 源码知道 下面的几个基础信息会传递到日志中进行记录

当系统进行高风险任务,日志必须严格检查时,这个功能就非常有用,比如需要使用日志进行分析的情况。

这里需要先回顾下 Slf4j 的 MDC 功能。

MDC ( Mapped Diagnostic Contexts ),顾名思义,其目的是为了便于我们诊断线上问题而出现的方法工具类。 其提供了四个重要方法来操作变量

public class MDC {
  //Put a context value as identified by key
  //into the current thread's context map.
  public static void put(String key, String val);

  //Get the context identified by the key parameter.
  public static String get(String key);

  //Remove the context identified by the key parameter.
  public static void remove(String key);

  //Clear all entries in the MDC.
  public static void clear();
}

其内部使用了 InheritableThreadLocal 来共享变量, InheritableThreadLocal 继承至 ThreadLocal 在保证同一个线程中共享变量的同时扩展了子线程可以从父线程中获取局部变量的初始值

回到 Activiti 中,继续来创建一个栗子演示 MDC 记录流程ID; 使用过程中有下面几点需要注意

1.常见流程文件

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="process">
    <startEvent id="start" />
    <sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
    <userTask id="someTask" name="Activiti is awesome!"/>
    <!--<serviceTask id="someTask" activiti:class="com.example.activiti.delegate.MdcErrorDelegate"/>-->
    <sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
    <endEvent id="end"/>
  </process>
</definitions>

2.编写拦截器

上面说了需要使用拦截器来处理全部的 MDC 记录。拦截器中的代码可以参考 DebugCommandInvoker 来实现

public class MdcCommandInvoker extends CommandInvoker {

    @Override
    public void executeOperation(Runnable runnable) {
        // 记录 MDC 状态
        boolean mdcEnabled = LogMDC.isMDCEnabled();
        // 开启 MDC 
        LogMDC.setMDCEnabled(true);
        if (runnable instanceof AbstractOperation) {
            AbstractOperation operation = (AbstractOperation) runnable;
            if (operation.getExecution() != null) {
                LogMDC.putMDCExecution(operation.getExecution());
            }
        }
        // 执行流程
        super.executeOperation(runnable);
        // 清除父子线程中的共享变量
        LogMDC.clear();
        // 恢复 MDC 状态
        LogMDC.setMDCEnabled(mdcEnabled);
    }
}

3.配置启用拦截器

在 activiti.cfg.xml 文件中配置使用上面的拦截器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
        <property name="databaseSchemaUpdate" value="true"/>
        <property name="commandInvoker" ref="mdcCommandInvoker"/>
    </bean>

    <bean id="mdcCommandInvoker" class="com.example.activitilog.interceptor.MdcCommandInvoker"/>
</beans>

4.编写日志文件

这里使用 logback 进行日志记录,需要引入相应的 jar ,下面是日志文件 logback.xml 的配置

<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false" scan="true" scanPeriod="30 seconds">
    <property name="mdc" value="[%X{mdcProcessDefinitionID},%X{mdcExecutionId},%X{mdcProcessInstanceID},%X{mdcBusinessKey},%X{mdcTaskId}]%n"/>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${mdc}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <encoding>UTF-8</encoding>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

5.编写测试类

测试类中使用到了 Junit 的 rule 功能,不了解的可以自行百度。

@Slf4j
public class ActivitiLogApplicationTests {

    @Rule
    public ActivitiRule activitiRule = new ActivitiRule("activiti.cfg.xml");

    @Test
    @Deployment(resources = {"process.bpmn20.xml"})
    public void contextLoads() {
        // 开启 MDC 功能
        LogMDC.setMDCEnabled(true);
        ProcessInstance instance = activitiRule.getRuntimeService().startProcessInstanceByKey("process");
        assertNotNull(instance);

        Task task = activitiRule.getTaskService().createTaskQuery().singleResult();
        assertEquals("Activiti is awesome!", task.getName());

        activitiRule.getTaskService().complete(task.getId());
    }
}

这样就可以看到下面的输出

[process:1:3,5,4,,]
[process:1:3,5,4,,]
[process:1:3,5,4,,]
[process:1:3,5,4,,]
[process:1:3,5,4,,]
[process:1:3,5,4,,]
[,,,,]
[,,,,]

可以看出能获取到的地方都记录了流程信息,在使用中只需要在需要记录的地方开启 MDC, 记录完了再关闭就行了。


参考资料
Activiti 5.16 用户手册#映射诊断上下文
Activiti6.0工作流引擎深度解析
Slf4j MDC 使用和 基于 Logback 的实现分析