xiaobaoqiu Blog

Think More, Code Less

Activiti事件处理

1.事件处理

Activiti 内部充斥着各种各样的事件,详见:org.activiti.engine.delegate.event.ActivitiEventType的定义。

在 Activiti 5.15 以前,处理事件需要在流程设计的时候显示的加入各种Listener,比如:

1
2
3
4
5
6
7
    <userTask id="leaderVerifyTask" activiti:taskType="2">
        <extensionElements>
            <activiti:taskListener event="create" delegateExpression="${taskCreateListener}"/>
            <activiti:taskListener event="assignment" delegateExpression="${taskAssignmentListener}"/>
            <activiti:taskListener event="complete" delegateExpression="${taskCompleteListener}"/>
        </extensionElements>
    </userTask>

Spring会注入 taskCreateListener 这个bean来作为 leaderVerifyTask 这个任务的创建事件响应逻辑。 这种方式的问题在于,流程设计的时候需要知道 代码中具体的类或者bean的名称,因此无法做到将流程的设计交与开发人员之外的人来做。

在 Activiti 5.15 中加入了全局的事件监听器 ActivitiEventListener,我们只需要实现一个自己的全局的事件处理器,然后自定义各种事件的处理器。

 /**
 * 流程服务的全局 Listener
 *
 * @author xiaobaoqiu  Date: 16-12-19 Time: 下午7:16
 */
public class GlobalProcessEventListener implements ActivitiEventListener {

    /**
     * 各类 Event 的处理器
     * 其中事件类型参见 ActivitiEventType 定义
     *
     * @see org.activiti.engine.delegate.event.ActivitiEventType
     */
    private Map<ActivitiEventType, ProcessEventHandler> handlers = new HashMap<>();

    @Override
    public void onEvent(ActivitiEvent event) {
        ProcessEventHandler handler = handlers.get(event.getType());
        if (handler != null) {
            handler.handle(event);
        }
    }

    @Override
    public boolean isFailOnException() {
        return false;
    }

    public Map<ActivitiEventType, ProcessEventHandler> getHandlers() {
        return handlers;
    }

    public void setHandlers(Map<ActivitiEventType, ProcessEventHandler> handlers) {
        this.handlers = handlers;
    }
}

/**
 * 流程事件处理
 *
 * @author xiaobaoqiu  Date: 16-12-21 Time: 下午1:33
 */
public interface ProcessEventHandler {

    void handle(ActivitiEvent event);
}

ActivitiEventListener能做到和流程设计无关,只需要在配置 Activiti 的时候将其注入,并且可以在其中配置能处理哪些类型的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
    <property 其他配置 />
    <property name="eventListeners">
        <list>
            <ref bean="globalProcessEventListener"/>
        </list>
    </property>
</bean>


<!--流程全局事件处理器-->
<bean id="globalProcessEventListener" class="com.qunar.scm.ct.biz.process.event.GlobalProcessEventListener">
    <property name="handlers">
        <map>
            <entry key="TASK_CREATED">
                <bean class="com.qunar.xxx.ProcessProcessTaskCreateListener"/>
            </entry>
            <entry key="TASK_COMPLETED">
                <bean class="com.qunar.xxx.ProcessProcessTaskCompleteListener"/>
            </entry>
            <entry key="TASK_ASSIGNED">
                <bean class="com.qunar.xxx.ProcessProcessTaskAssignmentListener"/>
            </entry>
        </map>
    </property>
</bean>

参考: https://www.activiti.org/userguide/index.html#eventDispatcherConfiguration

2.Listener异常处理

需要注意的一点是 GlobalProcessEventListener 中的异常处理,我们在实现 GlobalProcessEventListener 的时候,ActivitiEventListener有一个接口需要实现:

1
2
3
4
5
/**
 * @return whether or not the current operation should fail when this listeners execution
 * throws an exception. 
 */
boolean isFailOnException();

即当 listener 抛出一个异常的时候,当前操作时序应该失败(其实就是是否需要吞掉 listener 的异常).

我们可以看看 Activiti 相关的源代码,在 ActivitiEventSupport 中的 dispatchEvent 函数,我们可以清晰的看到,listener.isFailOnException()为false则会讲异常吞掉,只是打一行日志.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void dispatchEvent(ActivitiEvent event, ActivitiEventListener listener) {
    try {
        listener.onEvent(event);
    } catch (Throwable t) {
        if (listener.isFailOnException()) {
            throw new ActivitiException("Exception while executing event-listener", t);
        } else {
            // Ignore the exception and continue notifying remaining listeners. The
            // listener
            // explicitly states that the exception should not bubble up
            LOG.warn("Exception while executing event-listener, which was ignored", t);
        }
    }
}

我的建议(也是我现在的做法)是:isFailOnException()设置为true,然后在对应的 ProcessEventHandler 自己处理需要吞掉异常的场景.

3.Listener注册

通常而言,我们的 ProcessEventHandler 需要使用到 Activiti 的Service,比如我的一个简单场景,我需要在 UserTask 获取一些业务信息,新词我需要使用 RuntimeService 来查询一些信息

1
2
3
4
5
6
7
8
9
10
11
/**
 * 任务创建事件
 *
 * @author xiaobaoqiu  Date: 16-12-23 Time: 下午7:09
 */
public class ProcessTaskCreateListener extends AbstractProcessTaskListener {

    @Resource
    private RuntimeService runtimeService;
    ...
}

按照第一节的写法,就会出现Bean循环依赖的问题:

1
2
3
4
5
1. ProcessEngineConfiguration 实例创建会触发 GlobalProcessEventListener 创建;
2.GlobalProcessEventListener 实例创建会触发 ProcessProcessTaskCreateListener 创建;
3.ProcessProcessTaskCreateListener 实例创建会触发 RuntimeService 创建;
4.RuntimeService 实例创建依赖 ProcessEngineFactoryBean 创建;
5.ProcessEngineFactoryBean 创建依赖 ProcessEngineConfiguration实例;

并且 ProcessEngineFactoryBean 是 Activiti 自己实现的 FactoryBean,用来获得 ProcessEngine 实例 :

1
2
3
public class ProcessEngineFactoryBean implements FactoryBean<ProcessEngine>, DisposableBean, ApplicationContextAware {
    ...
}

这个问题一个比较好的解法是, 将 GlobalProcessEventListener 的注册延后到 GlobalProcessEventListener 初始化完成之后,因此涉及到两个问题:

1.GlobalProcessEventListener 什么时机去做自己注册;
2.GlobalProcessEventListener 如何将自己注册到 SpringProcessEngineConfiguration 中;

其中第一个问题很好回答, GlobalProcessEventListener 自己初始化完成之后去自注册,这个时候依赖的各种Bean已经创建初始化完成了.因此只需要实现 ApplicationContextAware 接口,然后在 setApplicationContext 中做注册的工作.

1
2
3
4
5
6
7
8
9
public class GlobalProcessEventListener implements ActivitiEventListener, ApplicationContextAware {
    /**
     * 将当前 EventListener 注册到 Activiti 中
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ...
    }
}

第二个问题,如何自己注册上去,这个看看源代码就会找到方法, 我们在配置文件中给 SpringProcessEngineConfiguration 配置 EventListener 调用的其实是 setEventListeners 方法

1
2
3
  public void setEventListeners(List<ActivitiEventListener> eventListeners) {
      this.eventListeners = eventListeners;
  }

我们看远吗发现其中使用 eventListeners 的只有一个地方,就是将其赋值给 ActivitiEventDispatcher

1
2
3
4
5
if(eventListeners != null) {
    for(ActivitiEventListener listenerToAdd : eventListeners) {
        this.eventDispatcher.addEventListener(listenerToAdd);
    }
}

而 ActivitiEventDispatcher 才是使用 EventListener 的地方.我们再看源码发现 ActivitiEventDispatcher 提供了 addEventListener 的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface ActivitiEventDispatcher {

    /**
     * Adds an event-listener which will be notified of ALL events by the dispatcher.
     * @param listenerToAdd the listener to add
     */
    void addEventListener(ActivitiEventListener listenerToAdd);
    
    /**
     * Adds an event-listener which will only be notified when an event of the given types occurs.
     * @param listenerToAdd the listener to add
     * @param types types of events the listener should be notified for
     */
    void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... types);

并且 SpringProcessEngineConfiguration 有 getEventDispatcher 来获得 ActivitiEventDispatcher 实例,因此我们可以通过 getEventDispatcher 来拿到 ActivitiEventDispatcher 实例,然后将当前的 EventListener 通过 addEventListener 的方法增加到 ActivitiEventDispatcher 的 EventListener 列表中.

因此,最终的实现代码很简单:

1
2
3
4
5
6
7
8
/**
 * 将当前 EventListener 注册到 Activiti 中
 */
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    SpringProcessEngineConfiguration configuration = applicationContext.getBean(SpringProcessEngineConfiguration.class);
    configuration.getEventDispatcher().addEventListener(this);
}

Activiti任务增加属性

最近逐步将组内的审核业务迁移到 Activiti 上,为了适配已有业务,需要原生的 Activiti 的用户任务上增加一些属性,比如用户任务(UserTask)增加类型属性等。 Activiti版本为:5.22.0

1.任务增加属性

下面以增加任务类型为例子,记录修改点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
1.修改表结构,ACT_RU_TASK表增加 TASK_TYPE_ 字段
在 activiti-engine 包下,找自己对应的数据库的文件(mysql版本...)

1.UserTask增加taskType属性
包路径:org.activiti.bpmn.model.UserTask

3.Task接口增加setTaskType方法
包路径:org.activiti.engine.task.Task

4.TaskEntity增加taskType属性
包路径:org.activiti.engine.impl.persistence.entity.TaskEntity

5.DelegateTask借口增加 getTaskType 和 setTaskType
包路径:org.activiti.engine.delegate.DelegateTask

6.TaskDefinition增加taskTypeExpression
包路径:org.activiti.engine.impl.task.TaskDefinition

7.DynamicBpmnConstants增加USER_TASK_TYPE
包路径:org.activiti.engine.DynamicBpmnConstants

8.UserTaskActivityBehavior设置taskType属性
包路径:org.activiti.engine.impl.bpmn.behavior.UserTaskActivityBehavior

9.UserTaskParseHandler解析taskType属性
包路径:org.activiti.engine.impl.bpmn.parser.handler.UserTaskParseHandler

10.BpmnXMLConstants增加 ATTRIBUTE_TASK_USER_TYPE
包路径:org.activiti.bpmn.constants.BpmnXMLConstants

11.UserTaskXMLConverter注册 ATTRIBUTE_TASK_USER_TYPE
包路径:org.activiti.bpmn.converter.UserTaskXMLConverter

12. Semantic.xsd 文件
包括各个版本的 XSD 文件(XML结构定义)

13. HistoricTaskWrapper 增加 taskType
包路径:org.activiti.explorer.ui.task.data.HistoricTaskWrapper

14.HistoricTaskInstanceEntity 增加 taskType
包路径:org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntity

15. 更多...

在自己的API中定义任务类型,然后我们可以修改 activiti-modeler 的相关代码,使得Activiti支持画图时候指定任务类型。

Activiti表

整理 Activiti 中的表含义,以及一些重要表的字段。本文的 Activiti 版本为 5.22.0

1.表含义

Activiti 中表定义 activiti-engine 这个 module 下,具体位置 /resources/org.activiti/db/create 下,包括各种类型数据库的建表语句,这里我们以Mysql为DB。

首先简单介绍表的前缀的含义,参考官网:http://www.activiti.org/userguide/index.html#database.tables.explained

表名称包含三个部分,第一个为所有的表共有的ACT_表示Activiti,第二部分表示表的使用场景

1
2
3
4
5
6
7

ACT_RE_*: RE表示repository,这个前缀的表包含了流程定义和流程静态资源
ACT_RU_*: RU表示runtime,这些运行时的表,包含流程实例,任务,变量,异步任务等运行中的数据。Activiti只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。
ACT_ID_*: ID表示identity,这些表包含身份信息,比如用户,组等。
ACT_HI_*: HI表示history,这些表包含历史数据,比如历史流程实例, 变量,任务等。
ACT_GE_*: 通用数据, 用于不同场景下。
ACT_EVT_*: EVT表示EVENT,目前只有一张表ACT_EVT_LOG,存储事件处理日志,方便管理员跟踪处理。

下面是各个表的大致意义:

意义 备注
ACT_EVT_LOG 事件处理日志
ACT_GE_BYTEARRAY 二进制数据表 保存流程定义图片和xml
ACT_GE_PROPERTY 属性数据表存储整个流程引擎级别的数据
ACT_HI_ACTINST 历史节点表 只记录usertask内容
ACT_HI_ATTACHMENT 历史附件表
ACT_HI_COMMENT 历史意见表
ACT_HI_DETAIL 历史详情表,提供历史变量的查询 流程中产生的变量详细,包括控制流程流转的变量等
ACT_HI_IDENTITYLINK 历史流程人员表
ACT_HI_PROCINST 历史流程实例表
ACT_HI_TASKINST 历史任务实例表
ACT_HI_VARINST 历史变量表
ACT_ID_GROUP 用户组信息表
ACT_ID_INFO 用户扩展信息表
ACT_ID_MEMBERSHIP 用户与用户组对应信息表
ACT_ID_USER 用户信息表
ACT_PROCDEF_INFO
ACT_RE_DEPLOYMENT 部署信息表
ACT_RE_MODEL 流程设计模型部署表 流程设计器设计流程后,保存数据到该表
ACT_RE_PROCDEF 流程定义数据表
ACT_RU_EVENT_SUBSCR throwEvent,catchEvent时间监听信息表
ACT_RU_EXECUTION 运行时流程执行实例表
ACT_RU_IDENTITYLINK 运行时流程人员表,主要存储任务节点与参与者的相关信息
ACT_RU_JOB 运行时定时任务数据表
ACT_RU_TASK 运行时任务节点表
ACT_RU_VARIABLE 运行时流程变量数据表

参考:http://craft6.cn/detail/activiti_research_database.do http://blog.csdn.net/flygoa/article/details/51895228

你真的了解String吗

String是Java中最重要最常用的类,没有之一。但是我们很多人可能只知道String的存在和简单使用,并不了解其背后的实现以及为什么这么实现。 这里我就简单的挑选几个重要的节点讲解。

本文基于JDK 1.7

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~ java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

实现

Java其实就是char数组的一个包装,String真实的内容就是存储在char数组中,数组的一个特性就是在逻辑和存储上都是连续的、可随机访问的。

1
2
/** The value is used for character storage. */
private final char value[];

String的操作其实都是围绕底层的char数组来的,比如trim实现,trim的其实是value的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */

    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

需要注意的是,String中的很多实现都是使用 System.arraycopy 来实现数组数据的快速拷贝,System.arraycopy是一个native方法:

1
2
3
4
5
public char[] toCharArray() {
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

final & immutable

String基本约定中最重要的一条是Immutable,Immutable的意思就是对String的每次修改都是生成一个新的String,原始对象不受影响,类似于copy-on-write的思想。

先具体看一下什么是Immutable。String不可变很简单,如下图,给一个已有字符串"abcd",第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

那String怎么做到不可变?主要通过三个手段 (1).String类声明为final(不会被继承改掉这种Immutable特性); (2).底层的value数组声明为final(value这个引用地址不可变); (3).所有String的方法里很小心的没有去动value数组里的元素,没有暴露内部成员字段;

其实String是不可变的关键都在底层的实现,而不是一个final修饰符。

不可变对象带来的最大好处就是线程安全,因为不会被改写。

hashcode

String内部属性除了value数组外,还有一个 hash,用于记录String的hash值。 String能这样讲hash值cache下来的原因就是上面提到的

1
2
/** Cache the hash code for the string */
private int hash; // Default to 0

每次调用hashCode,先检测 hash 是否已经计算好了,如果计算了,就不要重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

hash32

String中有一个hash32()的方法,它的结果被缓存在成员变量int hash32 中。

1
2
3
4
/**
 * Cached value of the alternative hashing algorithm result
 */
private transient int hash32 = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * Calculates a 32-bit hash value for this string.
 *
 * @return a 32-bit hash value for this string.
 */
int hash32() {
    int h = hash32;
    if (0 == h) {
       // harmless data race on hash32 here.
       h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);

       // ensure result is not zero to avoid recalcing
       h = (0 != h) ? h : 1;

       hash32 = h;
    }

    return h;
}

关于 hash32 为什么存在,可以看下面官网的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Java SE 7u6 introduces an improved, alternative hash function for the following map and map-derived collection implementations:

HashMap
Hashtable
HashSet
LinkedHashMap
LinkedHashSet
WeakHashMap
ConcurrentHashMap

The alternative hash function improves the performance of these map implementations when a large number of key hash collisions are encountered.

For Java SE 7u6, this alternative hash function is implemented as follows:

The alternative hash function is only applied to maps with a capacity larger than a specified threshold size. By default, the threshold is -1. This value disables the alternative hash function. To enable the alternative hash function, set the jdk.map.althashing.threshold system property to a different value. The recommended value is 512. Setting this system property to 512 causes all maps with a capacity larger than 512 entries to use the alternative hash function. You can set this system property to 0, which causes all maps to use the alternative hash function.

The following describes the jdk.map.althashing.threshold system property in more detail:

Value type: Integer
Value default: -1
Value range: From -1 to 2147483647, inclusive
Description: Threshold capacity at which maps use the alternative hash function. The value -1 is a synonym for 2147483647 (which is easier to remember). All other values correspond to threshold capacity.
For example, the following command runs the Java application MyApplication and sets the jdk.map.althashing.threshold system property to 512:

java -Djdk.map.althashing.threshold=512 MyApplication
If the alternative hash function is being used, then the iteration order of keys, values, and entities vary for each instance of HashMap, Hashtable, HashSet, and ConcurrentHashMap. This change in iteration order may cause compatibility issues with some programs. This is the reason that the alternative hash function is disabled by default. Hashing improvements will be investigated in future releases. In the meantime, the system property jdk.map.althashing.threshold is experimental. It is strongly recommended that you test your applications with the alternative hashing function enabled (by setting jdk.map.althashing.threshold to 0) to determine if your applications are affected by iteration order. If there is an impact, you should fix your applications as soon as possible because there is no guarantee of iteration order.

简单说就是为了解决 HashMap 这种hash 碰撞严重时候的性能,因为HashMap使用链地址法解决Hash碰撞,当链很长的时候,性能会受影响。 但是这个问题在Java 8 中已经解决了,因为Java 8中引入一个新的策略,当链比较长的时候(默认阈值10),会转化为采用红黑树实现后面的链表,可以讲时间从O(N)降到O(lgN).

参考: http://stackoverflow.com/questions/23716836/what-is-use-of-hash32-in-string-class http://docs.oracle.com/javase/7/docs/technotes/guides/collections/changes7.html http://stackoverflow.com/questions/23926312/difference-between-hash32-and-hash-in-java-string-object http://stackoverflow.com/questions/23926312/difference-between-hash32-and-hash-in-java-string-object/23926405#23926405

hash32这个方法最大的变化是,同一个字符串在不同的JVM上执行hash32()的结果可能不同(确切的说,多数情况下都会不同,因为其内部分别调用了一次System.currentTimeMillis()和两次System.nanoTime()用于初始化seed)。因此,某些容器在每次运行程序时迭代顺序都不同。

CaseInsensitiveComparator

在String中,有一个护额略大小写的比较方法,它的实现就是我们这里要说的CaseInsensitiveComparator

1
2
3
4
5
public int compareToIgnoreCase(String str) {
    return CASE_INSENSITIVE_ORDER.compare(this, str);
}

public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();

CaseInsensitiveComparator的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 8575799808933029326L;

    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }
}

我们发现比较时候需要现将字符转化为大小比较,再转化为小写比较。

Crate文档翻译

最近准备开始抽个人时间着手翻译Crate的一些文档,主要是考虑两个方面:

1.使用了1年的Crate,还没有比较全面的看Crate文档,很多特性肯定还不知道;
2.Crate被越来越多的人使用,中文文档肯定ui别人有帮助;

在 github 上开了个项目(其实很早就建工程了,一直没行动,也想通过这种方式驱动自己:先把牛B吹出去…):

https://github.com/xiaobaoqiu/crate_doc_cn.git

主要分三大块:

1.Crate Server相关文档;
2.Crate Client相关文档;
3.Crate其他文档;

欢迎监督和参与。