使用Activiti统一调度业务流程

activiti.jpg

什么是工作流/工作引擎?

Georgakopoulos给出的工作流定义是:工作流是将一组任务组织起来以完成某个经营过程:定义了任务的触发顺序和触发条件,每个任务可以由一个或多个软件系统完成,也可以由一个或一组人完成,还可以由一个或多个人与软件系统协作完。它主要解决的是“使在多个参与者之间按照某种预定义的规则传递文档、信息或任务的过程自动进行,从而实现某个预期的业务目标,或者促使此目标的实现”。工作流管理系统的目标:管理工作的流程以确保工作在正确的时间被期望的人员所执行——在自动化进行的业务过程中插入任何的执行和干预。所谓工作流引擎是指workflow作为应用系统的一部分,并为之提供对各应用系统有决定作用的根据角色、分工和条件的不同决定信息传递路由、内容等级等核心解决方案。例如开发一个系统最关键的部分不是系统的界面,也不是和数据库之间的信息交换,而是如何根据业务逻辑开发出符合实际需要的程序逻辑并确保其稳定性、易维护性和弹性。比如你的系统中有一个任务流程,一般情况下这个任务的代码逻辑、流程你都要自己来编写。实现它是没有问题的。但是谁能保证逻辑编写的毫无纰漏?经过无数次的测试与改进,这个流程没有任何漏洞也是可以实现的,但是明显就会拖慢整个项目的进度。


BPMN2.0规范

BPMN(Business Process Model and Notation)--业务流程模型与符号。

BPMN是一套流程建模的标准,主要目标是被所有业务用户容易理解的符号,支持从创建流程轮廓的业务分析到这些流程的最终实现,知道最终用户的管理监控。

通俗一点其实就是一套规范,画流程模型的规范。流程模型包括:流程图、协作图、编排图、会话图。


什么是Activiti

Activiti的创始人也就是JBPM(也是一个优秀的BPM引擎)的创始人,从Jboss离职后开发了一个新的BPM引擎:Activiti。所以,Activiti有很多地方都有JBPM的影子。Activiti是一个开源的工作流引擎,它实现了BPMN 2.0规范,可以发布设计好的流程定义,并通过api进行流程调度。Activiti 作为一个遵从 Apache 许可的工作流和业务流程管理开源平台,其核心是基于 Java 的超快速、超稳定的 BPMN2.0 流程引擎,强调流程服务的可嵌入性和可扩展性,同时更加强调面向业务人员。Activiti 流程引擎重点关注在系统开发的易用性和轻量性上。每一项 BPM 业务功能 Activiti 流程引擎都以服务的形式提供给开发人员。通过使用这些服务,开发人员能够构建出功能丰富、轻便且高效的 BPM 应用程序。ProcessEngine对象,这是Activiti工作的核心。负责生成流程运行时的各种实例及数据、监控和管理流程的运行。所有的操作都是从获取引擎开始的,所以一般会把引擎作为全局变量。

ProcessEngine processEngine = ProcessEngine.getDefaultProcessEngine();

为什么要用工作流引擎?

简单来说,就是为了统一管理流程业务。想想看,如果要设计一个流程的程序,通常需要在数据库中存各种状态值,比如一个订单程序,要标记订单是未付款、已付款、已出库等等状态,而这些各种各样的状态参杂在程序中,逻辑自然就变得复杂了。而将这些状态对应流程里的一个个步骤,交由流程引擎去管理,这样不仅简化了业务逻辑代码,而且,还有很强的扩展性。我可以修改我的流程,我可以添加一些步骤而不用改我的数据库表结构,不用改我的业务逻辑。


在Eclipse中使用Activiti

推荐使用离线安装:断网

  1. 通过网盘:https://pan.baidu.com/s/15jZrf5LzlrgG82gPAh3cng 密码:4kxd ,下载在离线安装包。

  2. 将下载好的jars文件夹里的3个jar文件复制到eclipse安装目录的plugins目录下。

  3. 删除eclipse安装目录下,configuration文件夹里的org.eclipse.update文件夹,重启eclipse。

  4. 打开eclipse,在Help->Install New Software后的弹出窗点击Available Software Sites,删除设置过的Activiti资源信息,add确认安装。


在IDEA中使用Activiti

  1. 点击菜单【File】-->【Settings...】打开【Settings】窗口。

点击左侧【Plugins】按钮,在右侧输出"actiBPM",点击下面的【Search in repositories】链接会打开【Browse Repositories】窗口

  1. 进入【Browse Repositories】窗口,选中左侧的【actiBPM】,点击右侧的【Install】按钮,开始安装。

  2. 安装完成后,会提示【Restart IntelliJ IDEA】,重启IDEA即可完成安装。


如何更优雅地使用Activiti

随着微服务的普及,Rest风格的编码规范越来被推崇,因而我们使用Activiti Rest模块进行Activiti统一Rest调用。

1. Activiti REST模块介绍

Activiti-rest是Activiti提供的一组可以直接操作工作流引擎的REST API,使用者可以在自己应用中直接调用Activiti-rest接口。

1.1 使用REST的好处

  1. 简单化:利用现有模块(activiti-rest.war)代替直接API调用

  2. 标准化:各个系统根据rest模块的接口规范访问REST资源,统一处理;对于工作流平台来说此特性尤为突出

  3. 扩展性:如果官方提供的REST接口还不能满足可以继续在其基础上进行扩展以满足业务系统(平台)的需求

1.2 不适合使用REST的场景

业务数据与流程数据分离:就像kft-activiti-demo中普通表单的演示一样,业务数据保存在一张单独设计的表中,而不是把表单数据保存在引擎的变量表中,所以对于这样的场景中需要联合事务管理的就不能使用REST了,例如:启动流程、任务完成、业务与流程数据联合查询。

1.3 部署Rest模块

5.11版本开始不再使用ant脚本的方式启动demo,并且把activiti-explorer和activiti-rest分离并分别提供一个war包,在wars目录可以找到它。

把activiti-rest.war解压到Web服务器的应用部署目录(例如tomcat的webapps),根据实际需求修改activiti-rest/WEB-INF/classes/db.properties里面的数据库配置后启动应用。

可以通过REST工具测试是否部署成功可以正常的提供服务,例如Chrome的插件REST Console,或者通过Spring MVC提供的RestTemplate。

2. 访问REST资源

对于REST模块提供的接口可以参考用户手册的REST API章节,有着详细的介绍(包括URL和参数含义)。

2.1 身份认证

REST接口的大部分功能都需要验证,默认使用Basic Access Authentication(基本连接认证),所以在访问资源时要在header中添加验证信息,当然为了安全期间把用户名和密码进行base 64位加密。

可以在用户登陆之后把用户名和密码进行加密并设置到session中,这样在前端就可以直接通过Ajax方式获取资源了:

import jodd.util.Base64;
String base64Code = "Basic " + Base64.encodeToString(user.getId() + ":" + user.getPassword());
session.setAttribute("BASE_64_CODE", base64Code);

2.2 通过Ajax方式读取资源

下面通过kft-activiti-demo中的代码片段介绍:

$.ajax({
    type: "get",
    url: REST_URL + 'process-definition/' + processDefinitionId + '/form',
    beforeSend: function(xhr) {
        xhr.setRequestHeader('Authorization', BASE_64_CODE);
    },
    dataType: 'html',
    success: function(form) {
        // 获取的form是字符行,html格式直接显示在对话框内就可以了,然后用form包裹起来
        $(dialog).html(form).wrap("

在第5行处设置了ajax请求的header信息,这样REST模块就可以通过header的信息进行身份认证,通过之后就可以执行资源请求并返回处理结果。

2.3 通过Java方式读取资源

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;

public class TestLogin {

  public static void main(String[] args) throws Exception, IOException {
    DefaultHttpClient httpClient = new DefaultHttpClient();
    HttpPost postRequest = new HttpPost("http://localhost:8080/activiti-rest/service/login");

    StringEntity input = new StringEntity("{\"userId\":\"kermit\",\"password\":\"kermit\"}");
    input.setContentType("application/json");
    postRequest.setEntity(input);

    HttpResponse response = httpClient.execute(postRequest);

    BufferedReader br = new BufferedReader(new InputStreamReader((response.getEntity().getContent())));

    String output;
    System.out.println("Output from Server .... \n");
    while ((output = br.readLine()) != null) {
      System.out.println(output);
    }

    httpClient.getConnectionManager().shutdown();
  }
}

3. Ajax跨域问题解决办法

  1. 把REST模块和应用部署在同一个Web服务器中(废话……)

  2. 等待官方提供JSONP的支持,JIRA Issue:ACT-1534

  3. 利用后台代理方式,把请求的URL发送给后台代理服务器,获取数据之后再把结果返回给前台


在WEB界面绘制流程图

Activiti Modeler 组件,是一套支持WEB界面实现流程绘制的服务组件,允许使用方自定义流程。

1.下载 Activiti-activiti-5.22.0 包

下载地址:https://github.com/Activiti/Activiti/tree/activiti-5.22.0/

  1. 1 提取 Activiti-activiti-5.22.0 包内容,放置到项目中

1.1.1 从 Activiti-activiti-5.22.0>modules>activiti-webapp-explorer2>src>main>webapp 下提取diagram-viewer,editor-app,modeler.html 放置到 resource/static 中1.1.2 从Activiti-activiti-5.22.0>modules>activiti-webapp-explorer2>src>main>resources 下提取stencilset.json 放置到 resource/static 中1.1.3 从Activiti-activiti-5.22.0>modules>activiti-modeler>src>main>java>org>activiti>rest>editor 下提取 ModelEditorJsonRestResource.java,ModelSaveRestResource.java ,StencilsetRestResource.java放置到项目中1.1.4 修改ModelEditorJsonRestResource.java

@RestController
@RequestMapping(value = "/service") //添加

1.1.5 修改ModelSaveRestResource.java

@RestController
@RequestMapping("/service")
public class ModelSaveRestResource implements ModelDataJsonConstants {

  protected static final Logger LOGGER = LoggerFactory.getLogger(ModelSaveRestResource.class);
  @Autowired
  private RepositoryService repositoryService;
  @Autowired
  private ObjectMapper objectMapper;

  /**
   * 保存模型
   * @param modelId
   * @param json_xml
   * @param svg_xml
   * @param name
   * @param description
   */
  @RequestMapping(value = "/model/{modelId}/save", method = RequestMethod.PUT)
  public void saveModel(@PathVariable String modelId, @RequestParam String json_xml,
                        @RequestParam  String svg_xml,@RequestParam String name, @RequestParam String description) {
      try {

          // 获取模型信息并更新模型信息
          ObjectMapper objectMapper = new ObjectMapper();
          Model model = repositoryService.getModel(modelId);
          ObjectNode modelJson = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
          modelJson.put(MODEL_NAME, name);
          modelJson.put(MODEL_DESCRIPTION, description);
          model.setMetaInfo(modelJson.toString());
          model.setName(name);
          repositoryService.saveModel(model);
          repositoryService.addModelEditorSource(model.getId(), json_xml.getBytes("utf-8"));

          // 基于模型信息做流程部署
          InputStream svgStream = new ByteArrayInputStream(svg_xml.getBytes("utf-8"));
          TranscoderInput input = new TranscoderInput(svgStream);
          PNGTranscoder transcoder = new PNGTranscoder();
          ByteArrayOutputStream outStream = new ByteArrayOutputStream();
          TranscoderOutput output = new TranscoderOutput(outStream);
          transcoder.transcode(input, output);
          final byte[] result = outStream.toByteArray();
          repositoryService.addModelEditorSourceExtra(model.getId(), result);
          outStream.close();
          Model modelData = repositoryService.getModel(modelId);
          ObjectNode modelNode = (ObjectNode) objectMapper.readTree(repositoryService.getModelEditorSource(modelData.getId()));
          byte[] bpmnBytes = null;
          BpmnModel model2 = new BpmnJsonConverter().convertToBpmnModel(modelNode);
          bpmnBytes = new BpmnXMLConverter().convertToXML(model2);
          String processName = modelData.getName() + ".bpmn";
          Deployment deployment = repositoryService.createDeployment()
                  .name(modelData.getName())
                  .addString(processName, StringUtils.toEncodedString(bpmnBytes, Charset.forName("UTF-8")))
                  .deploy();
      } catch (Exception e) {
          LOGGER.error("模型保存失败", e);
          throw new ActivitiException("模型保存失败", e);
      }

  }
}

1.1.6 修改StencilsetRestResource.java

@RestController
@RequestMapping(value = "/service")
public class StencilsetRestResource {

  /**
   * 加载模板集配置
   * @return
   */
  @RequestMapping(value="/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
  public @ResponseBody String getStencilset() {
      try {
          InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("static/stencilset-cn.json");
          return IOUtils.toString(stencilsetStream, "utf-8");
      } catch (Exception e) {
          throw new ActivitiException("加载模板集配置失败", e);
      }
  }
}

1.1.7 修改app-cfg.js

ACTIVITI.CONFIG = {
// 'contextRoot' : '/activiti-explorer/service',
 'contextRoot' : '/activiti/service',
}
  1. 2 activiti 绘制器的汉化

使用汉化的stencilset.json替换resource/static 下的stencilset.json

点击下载

  1. 3 实现控制类,用于模型的构建

创建ActivitiModelController.java

@RestController
public class ActivitiModelController {

    public static Logger logger = Logger.getLogger(ActivitiModelController.class);
    @Autowired
    private ProcessEngine processEngine;                    // 定义流程引擎
    @Autowired
    private ObjectMapper objectMapper;
    private final static Integer MODEL_REVISION = 1;       // 模型的版本
    private final static String ACTIVITI_REDIRECT_MODELER_INDEX = "/activitiService/modeler.html?modelId=";         // modeler.html页面地址
    private final static String ACTIVITI_NAMESPACE_VALUE = "http://b3mn.org/stencilset/bpmn2.0#";                    // 默认的空间值
    private final static String ACTIVITI_ID_VALUE = "canvas";                                                             // 默认ID值

    /**
     * 新建一个模型
     * @param response
     * @throws IOException
     */
    @RequestMapping("/activiti-ui.html")
    public void newModel(HttpServletResponse response) throws IOException {
        RepositoryService repositoryService = processEngine.getRepositoryService();

        // 初始化空的流程资源模型,填充信息并持久化模型
        Model model = repositoryService.newModel();
        String uuid = UUUID.randomUUID().toString().replaceAll("-", "");
        ObjectNode modelNode = objectMapper.createObjectNode();
        modelNode.put(ModelDataJsonConstants.MODEL_NAME, uuid);
        modelNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, "");
        modelNode.put(ModelDataJsonConstants.MODEL_REVISION, MODEL_REVISION);
        model.setName(uuid);
        model.setKey(uuid);
        model.setMetaInfo(modelNode.toString());
        repositoryService.saveModel(model);

        // 将 editorNode  数据填充到模型中, 并做页面的重定向
        ObjectNode editorNode = objectMapper.createObjectNode();
        editorNode.put("id", ACTIVITI_ID_VALUE);
        editorNode.put("resourceId", ACTIVITI_ID_VALUE);
        ObjectNode stencilSetNode = objectMapper.createObjectNode();
        stencilSetNode.put("namespace",ACTIVITI_NAMESPACE_VALUE);
        editorNode.put("stencilset", stencilSetNode);
        repositoryService.addModelEditorSource(model.getId(),editorNode.toString().getBytes("utf-8"));
        response.sendRedirect(ACTIVITI_REDIRECT_MODELER_INDEX + model.getId());
    }
}
  1. 4 验证 activiti modeler

http://ip:port/activiti/activiti-ui.html

activiti modeler

搭建工作流平台

参考开源社区:

  1. Spring平台整合activiti工作流引擎实例

  2. springboot+activiti+angular 这是spring boot框架集成activiti工作流实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348

推荐阅读更多精彩内容