Activiti用户指南之Activiti的API

标签: activiti 用户 activiti | 发表时间:2013-04-27 15:23 | 作者:
出处:http://www.iteye.com


 一、流程引擎的API和服务(services)

     引擎的API是影响Activiti最常见的一种方法。我们一开始最关注的中心是ProcessEngine,像之前描述的那样,流程引擎可以被多种方式创建。从这个流程引擎里面,你能获得各个包含workflow/BPM方法的服务。流程引擎和这些获得的服务是线程安全的。所以你能为整个服务器保留这些中的一个引用。
     
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();
ManagementService managementService = processEngine.getManagementService();
IdentityService identityService = processEngine.getIdentityService();
HistoryService historyService = processEngine.getHistoryService();
FormService formService = processEngine.getFormService();
 
     ProcessEngines.getDefaultProcessEngine();这个方法被调用之后,会首先初始化并且创建一个流程引擎,并且以后会一直返回同一个流程引擎。ProcessEngine.init()和ProcessEngine.destroy方法会被用来创建和关闭所有流程引擎的属性。
 
     ProcessEngine这个类会被所有的activiti.cfg.xml和activiti-context.xml文件扫描到。对于所有的activiti.cfg.xml来说,流程引擎都会用一种典型的方式被创建。这种方式就是:
                              ProcessEngineConfiguration.
                                        createProcessEngineConfigurationFromInputStream(inputStream).
                                        buildProcessEngine()。
 
     对于所有的activiti-context.xml来说,流程引擎会被用Spring的方式来创建。这种方式是:
               首先Spring的应用上下文被创建,然后流程定义会在应用的上下文中获得。     
 
     所有的Services服务是没有国界的。这就意味着你可以很轻松的在一个集群的多个节点中运行Activiti,每一个都用到相同的数据库,并且在以前的调用中,哪一个机器实际上被执行不会出现错误。任何服务的调用都是同等的,不管它在哪里被执行。
     当用流程引擎工作的时候,第一个被需要的服务大概就是 RepositoryService。这个服务为管理、多个部署和流程定义都提供了操作。这里不作详细介绍,一个流程定义是BPMN 2.0的一个java相关。它是一个流程每一步的结构和行为的表现。一个部署包是Activiti引擎下的包的单元。一个部署包能包含多个BPMN 2.0 xml文件和其他的一些资源。一个部署包里面包含什么由开发者决定。这些包括从单一的流程BPMN 2.0文件到一个整个的流程包和相关的资源。RepositoryService允许去部署这样的包。部署一个部署包意味着这是上传到引擎中,引擎中的所有的流程在被存到数据库之前都会被检查和解析。从这点来看,这个部署包被系统能识别,并且任何流程都被包含进这个部署包中,这样的部署包现在才能被启动。
     进一步说,这样的服务允许去:
  • 为引擎在部署包和流程定义中提供查询
  • 终止或者激活部署包作为整个或者特别的流程定义。终止意味着不能再在部署包和流程定义中进行操作,当然,激活意味着相反的操作。
  • 取回各种各样的资源,比如包含部署包或者流程程序的文件。然后这些资源被引擎自动发布。
  • 取回一个流程定义的pojo版本,这个版本能被用于用java比xml反省流程。
     RepositoryService相当于静态的信息, RuntimeService正好是相反的。它处理一个流程定义中新的流程实例。像上面说的,一个流程定义决定着一个流程中的不同步骤的结构和行为。一个流程实例是这样一个流程定义的的一个执行。对于每一个流程定义,同时会有很多实例在运行。RuntimeService同样是被用在取回和存储流程变量的服务。这些数据是特别是流程实例的,并且这些数据能被用在流程中的各种各样的结构。RuntimeService同样允许在流程实例和executions中去查询。Executions是BPMN 2.0 概念‘token’的一种表现。基本上,一个execution就是一个指向流程实例当前在哪里的指针。最后,任何一个流程实例在等待一个额外的出触发器和流程需要继续的时候,RuntimeService都会被用到。一个流程实例有各种各样的等待状态,并且这个服务对单个实例包含了各种各样的操作。这个实例额外的触发器会被接收到并且这个流程实例会比继续执行。
     Tasks是BPM的核心,比如Activiti。它需要被系统中的真实用户所执行。在 TaskService中,围绕着任务的每一件事情都是分组的。比如:
  • 查询分配给人或组的任务
  • 创建新的单独任务。这些任务都不跟流程实例有关。
  • 操纵哪一个用户的任务被分配或者在一些方式下哪一些用户卷入到任务中。
  • 请求或者完成一个任务。请求意味着有些人被分配到到任务,意味着这个人将会完成这个任务。完成意味着正在做任务的某一部分工作。
     
      IdentityService是相当简单的。它允许对于用户和组的管理。在运行的时候,Activiti实际上不对用户作任何检查。这对于理解Activiti是很重要的。举个例子,一个任务能分配给任何人,但是如果系统知道这个用户,引擎不会去区分它。这是因为Activiti引擎也能和服务一起结合使用。
      FormService是一个可选的服务。意味着Activiti没有FormService,也能正确的被使用,不会牺牲消失任何其他的功能。这个服务介绍了start form和task form的概念。一个start form是一个表单,这个表单在流程实例启动之前展现给用户。然而一个task form是一个用户想要去完成一个表单的时候,展示的表单。Activiti允许在BPMN  2.0的流程定义中去定义这些表单。
      HistoryService暴露了所有被Activiti 引擎生成的历史数据。当流程执行的时候,一些数据能被引擎保留。比如流程实例开始的时间,谁做的哪个任务,这个任务完成花了多长时间,在这个流程实例中,哪个路径是被跟踪的等等。这个服务暴露了主要的访问数据的查询方法。
     用activiti的时候,当编码自定义应用的时候, ManagementService是不需要的。它允许去取回数据库表信息和表的元数据。进一步说,它暴露了对计划的查询和管理操作。在Activiti里,计划被用于各种各样的事情,比如计时器,异步连续,延迟暂停/激活等。这些话题会在更多的细节中进行讨论。
     要获得更多的关于服务和引擎API的详细信息,请参考doc文档。
 
二、异常策略
   在activiti中,最基本的异常是org.activiti.engine.ActivitiException,一个未经检查的异常。这个异常在任何时间都能被API抛出,但是除非这个异常是发生在那些在doc文档中记录的那些特殊的方法。举个例子,从TaskService中抛出的一个异常:
     
/** 
* 当任务被成功执行的时候调用. 
* @param taskId 是要去完成的任务的ID,不能为空.
* @throws ActivitiObjectNotFoundException 当给出的id没有任务存在的时候,会抛出异常.
*/ 
void complete(String taskId);
      
     在上面的例子中,当传的id任务不存在时,会抛出一个异常。同样,因此在javadoc中明确的规定任务id不能为空,当传过来的是个null,就会抛出ActivitiIllegalArgumentException。
     即使我们想避开一个大级别的异常,当在特殊情况下一些异常被抛出的时候,下面的子类会被添加。在经过process-execution或者API-invokation报的错如果不在下面列出的异常之内,他们通常都会抛出一个ActivitiException。
  • ActivitiWrongDbException:当Activiti引擎发现在数据库概要和引擎版本不匹配的时候会抛此异常。
  • ActivitiOptimisticLockingException:在数据保存的时候,当同时被访问相同数据进入的时候,引起的乐观所。会抛此异常。
  • ActivitiClassLoadingException:当一个需要被加载的类没有找打或者正当加载的时候报错,就会抛出此异常。
  • ActivitiIllegalArgumentException:
  • ActivitiTaskAlreadyClamiedException:当一个任务已经被请求,然后调用taskService.clami(...)的时候,会抛出此异常。
三、工作中用Activiti服务
     像上述一样,和Activiti 引擎互动的方式就是通过org.activiti.engine.ProcessEngine这个类的实例暴露的服务。下面的代码片段假设了你有一个工作中的Activiti环境,你也能获取到一个有效的org.activiti.engine.ProcessEngine。如果你仅仅想要提炼出下面的代码,你能下载或者克隆Activiti unit test template( https://github.com/Activiti/activiti-unit-test-template),把它导入到你的IDE,并且添加一个testUserguideCode()方法到org.activiti.MyUnitTest 单元测试。


 
 
     3.1 部署流程
     通过ResponsitoryService访问的任何数据都与静态数据有关(比如流程定义)。概念地,每一个这样的静态的一部分数据,都是Activiti引擎'respository'的内容。
     在src/test/resources/org/activiti/test文件夹中,创建一个新的xml文件,名字为VacationRequest.bpmn20.xml,下面是具体内容。这一节不会解释被用于上面例子中的xml的结构。
     
<?xml version="1.0" encoding="UTF-8" ?>
<definitions id="definitions"
             targetNamespace="http://activiti.org/bpmn20" 
             xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:activiti="http://activiti.org/bpmn">
  
  <process id="vacationRequest" name="Vacation request">
  
    <startEvent id="request" activiti:initiator="employeeName">
      <extensionElements>
        <activiti:formProperty id="numberOfDays" name="Number of days" type="long" value="1" required="true"/>
        <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
        <activiti:formProperty id="vacationMotivation" name="Motivation" type="string" />
      </extensionElements>
    </startEvent>
    <sequenceFlow id="flow1" sourceRef="request" targetRef="handleRequest" />
    
    <userTask id="handleRequest" name="Handle vacation request" >
      <documentation>
        ${employeeName} would like to take ${numberOfDays} day(s) of vacation (Motivation: ${vacationMotivation}).
      </documentation> 
      <extensionElements>
         <activiti:formProperty id="vacationApproved" name="Do you approve this vacation" type="enum" required="true">
          <activiti:value id="true" name="Approve" />
          <activiti:value id="false" name="Reject" />
        </activiti:formProperty>
        <activiti:formProperty id="managerMotivation" name="Motivation" type="string" />
      </extensionElements>
      <potentialOwner>
        <resourceAssignmentExpression>
          <formalExpression>management</formalExpression>
        </resourceAssignmentExpression>
      </potentialOwner>         
    </userTask>
    <sequenceFlow id="flow2" sourceRef="handleRequest" targetRef="requestApprovedDecision" />
    
    <exclusiveGateway id="requestApprovedDecision" name="Request approved?" />
    <sequenceFlow id="flow3" sourceRef="requestApprovedDecision" targetRef="sendApprovalMail">
      <conditionExpression xsi:type="tFormalExpression">${vacationApproved == 'true'}</conditionExpression>
    </sequenceFlow>
    
    <task id="sendApprovalMail" name="Send confirmation e-mail" />
    <sequenceFlow id="flow4" sourceRef="sendApprovalMail" targetRef="theEnd1" />
    <endEvent id="theEnd1" />
    
    <sequenceFlow id="flow5" sourceRef="requestApprovedDecision" targetRef="adjustVacationRequestTask">
      <conditionExpression xsi:type="tFormalExpression">${vacationApproved == 'false'}</conditionExpression>
    </sequenceFlow>
    
    <userTask id="adjustVacationRequestTask" name="Adjust vacation request">
      <documentation>
        Your manager has disapproved your vacation request for ${numberOfDays} days.
        Reason: ${managerMotivation}
      </documentation>
      <extensionElements>
        <activiti:formProperty id="numberOfDays" name="Number of days" value="${numberOfDays}" type="long" required="true"/>
        <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" value="${startDate}" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
        <activiti:formProperty id="vacationMotivation" name="Motivation" value="${vacationMotivation}" type="string" />
        <activiti:formProperty id="resendRequest" name="Resend vacation request to manager?" type="enum" required="true">
          <activiti:value id="true" name="Yes" />
          <activiti:value id="false" name="No" />
        </activiti:formProperty>
      </extensionElements>
      <humanPerformer>
        <resourceAssignmentExpression>
          <formalExpression>${employeeName}</formalExpression>
        </resourceAssignmentExpression>
      </humanPerformer>  
    </userTask>
    <sequenceFlow id="flow6" sourceRef="adjustVacationRequestTask" targetRef="resendRequestDecision" />
    
    <exclusiveGateway id="resendRequestDecision" name="Resend request?" />
    <sequenceFlow id="flow7" sourceRef="resendRequestDecision" targetRef="handleRequest">
      <conditionExpression xsi:type="tFormalExpression">${resendRequest == 'true'}</conditionExpression>
    </sequenceFlow>
    
     <sequenceFlow id="flow8" sourceRef="resendRequestDecision" targetRef="theEnd2">
      <conditionExpression xsi:type="tFormalExpression">${resendRequest == 'false'}</conditionExpression>
    </sequenceFlow>
    <endEvent id="theEnd2" />
      
  </process>
  
</definitions>
 
     要让流程引擎认识我们的这个流程,我们必须先发布它。发布意味着引擎会解析BPMN 2.0xml文件给一些事情执行并且为包含在发布包中的每一个流程定义都会添加一个新的数据库记录。这样的好处是:当流程引擎重启的时候,它会一直所有已经发布的流程:
     
     3.2 开始一个流程实例
     在把流程定义部署到流程引擎之后,我们就能启动一个新的流程实例。对于每一个流程定义来说,会有很多典型的流程实例。当流程定义的一个流程实例正在运行期,这个流程定义是'buleprint'。
     每一件和流程运行状态的事情,都会被RuntimeService监控。有各种各样的方法去启动一个新的流程实例。在下面的代码片段中,我们用我们在流程定义xml中定义的key去启动一个流程实例。我们也会在流程实例启动的时候,提供一些处理过的变量。因为第一个用户任务的描述会用到他们在它的表达式中。因为他们为一个确定的流程定义中的流程实例赋予意义的时候,这些变量通常会被用到。典型的来说,这些变量是让流程实例区别于其他实例的因素。
     
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employeeName", "Kermit");
variables.put("numberOfDays", new Integer(4));
variables.put("vacationMotivation", "I'm really tired!");      
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("vacationRequest", variables);      
// Verify that we started a new process instance
Log.info("Number of process instances: " + runtimeService.createProcessInstanceQuery().count());   
 
     3.3 正在完成的任务
     当一个流程开始的时候,第一步会成为一个用户的任务。这是一个必须被系统用户执行的步骤。典型的来说,这样的用户将会有一个'inbox of tasks','inbox of tasks'列出了所有需要被这个用户完成的任务。下面的代码就展现了这样一个查询怎么被执行:
     
// Fetch all tasks for the management group
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {  Log.info("Task available: " + task.getName());}   
      
     继续流程实例,我们需要完成这个任务。对于Activiti引擎来说,这意味着你需要这个任务。下面的代码断展示了怎么做的:
     
Task task = tasks.get(0);      
Map<String, Object> taskVariables = new HashMap<String, Object>();
taskVariables.put("vacationApproved", "false");
taskVariables.put("managerMotivation", "We have a tight deadline!");
taskService.complete(task.getId(), taskVariables);     
 
     流程实例会继续到下一步,在这个例子中,这又会成为第一个布骤,因为这个任务没有被审核过。
     
     3.4 暂停和激活一个流程
     
     暂停一个流程定义是可能的。当一个流程定义被暂停,一个新的流程实例不会被创建(一个异常将会被抛出)。通过RepositoryService去暂停一个流程定义。
     
repositoryService.suspendProcessDefinitionByKey("vacationRequest");
try {  
  runtimeService.startProcessInstanceByKey("vacationRequest");
} catch (ActivitiException e) {  
  e.printStackTrace();
}     
 
     重新激活一个流程定义,通常的调用repositoryService.activateProcessDefinitionXXX方法中的一个方法。
     暂停一个流程实例也有可能。当暂停的时候,这个流程不会继续下去并且没有计划会被执行。暂停流程实例可以通过调用runtimeService.suspendProcessInstance()方法.再次激活流程实例可以调用runtimeService.activateProcessInstanceXXX方法。
     
     3.5 进一步阅读
 
四、查询API
     从引擎中查询数据有两种方法:查询API和本地查询。查询API允许完全安全的用流畅的API编程。你能添加各种各样的条件到你的查询中并且精确地排序。下面的代码是个例子:
     
      List<Task> tasks = taskService.createTaskQuery()
         .taskAssignee("kermit")
         .processVariableValueEquals("orderId", "0815")
         .orderByDueDate().asc()
         .list();
      
     有时,你需要更强大的查询,用一个OR操作或者约束的查询,用API,你不能明确的实现。对于这种情况,我们介绍本地查询,本地查询允许你写自己的SQL查询.返回类型被你用的查询对象定义,并且数据会被映射到正确的对象.如认为,流程实例...因此,你不得不用在数据库中定义的表和字段的名字作为被映射到数据库的查询。这需要一些关于数据结构的知识,并且它推荐给我们要小心的进行本地查询。表的名字能通过API被重新寻回,以致于尽可能的远离对它的依赖。
     
      List<Task> tasks = taskService.createNativeTaskQuery()
        .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T WHERE T.NAME_ = #{taskName}")
        .parameter("taskName", "gonzoTask")
        .list();

      long count = taskService.createNativeTaskQuery()
        .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, "
               + managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_")
        .count();
      
 
五、表达式
     Activiti用UEL去解析表达式。UEL代表统一表达式语言(Unifies Expression Language),并且是EE6的一部分。为了支持在所有环境中最新的UEL的所有特点,我们用JUEL的一个编辑版本。
      表达式能被用在java service tasks/execution Listeners/Task Listener和Condition sequence flows。尽管有两种表达式类型,值表达式和方法表达式,Activiti概括了他们,以致于他们都能用在表达式需要的地方。
  • 值表达式:值得解析。默认的,所有的进程变量都能被拿来用。所有的spring-beans也能拿来用在表达式中。下面是例子:方法表达式:引用一个有或者没有参数的方法。 当引用一个没有参数的方法的时候,在方法名字的后面要确定去添加一个空的括号。传过来的参数可以是被他们自己解析的字符串或者表达式。举例:
    • ${myVar}
      ${myBean.myProperty}

       


    • ${printer.print()}
      ${myBean.addNewOrder('orderName')}
      ${myBean.doSomething(myVar, execution)}

       


          这些表达式支持解析实体类、集合、数组和map。
          在所有进程变量中最上面,有一小部分默认的对象变量是被用到表达式中的:
  • execution:
  • task:
  • authenticatedUserId:
六、在一个web应用中的流程引擎
     流程引擎是线程安全的类并且能被很容易的在多个线程中分享。在一个web应用中,这意味着当一个容器启动或者当一个容器关闭的时候,马上去创建一个流程引擎是可能的。
     下面的代码片段,就展示了你能怎么写一个简单的ServletContextListener,在一个纯粹的Servlet环境中去初始化和销毁流程引擎:
public class ProcessEnginesServletContextListener implements ServletContextListener {
  
  public void contextInitialized(ServletContextEvent servletContextEvent) {
    ProcessEngines.init();
  }

  public void contextDestroyed(ServletContextEvent servletContextEvent) {
    ProcessEngines.destroy();
  }

}

        contextInitialized方法会委托ProcessEngines.init()方法。它会去扫描activiti.cfg.xml文件,并且根据配置信息去创建一个流程引擎。如果你有多个这样的资源文件,你要确保他们都有不同的名字。 当流程引擎是需要的时候,它能用下面的方法取到。 
ProcessEngines.getDefaultProcessEngine()

or

ProcessEngines.getProcessEngine("myName");

     当然,用任何一个创建流程引擎的变体方法中也是可能的。想配置信息中描述的那样。contextDestryed方法会委托ProcessEngines.destroy()。这样,所有初始化的流程引擎都会关闭。          
               
     
     
     
 
 
 
 
 
 
 
 
 
 
           
 
 
 
     
     


已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [activiti 用户 activiti] 推荐:

Activiti用户指南之Activiti的API

- - ITeye博客
 一、流程引擎的API和服务(services).      引擎的API是影响Activiti最常见的一种方法. 我们一开始最关注的中心是ProcessEngine,像之前描述的那样,流程引擎可以被多种方式创建. 从这个流程引擎里面,你能获得各个包含workflow/BPM方法的服务. 流程引擎和这些获得的服务是线程安全的.

[Java][activiti]同步或者重构activiti identify用户数据的方法

- - CSDN博客架构设计推荐文章
同步或者重构Activiti Identify用户数据的多种方案比较. 相信每个涉及到用户的系统都有一套用户权限管理平台或者模块,用来维护用户以及在系统内的功能、数据权限,我们使用的Activiti工作流引擎配套设计了包括 User、Group的Identify模块,怎么和业务数据同步呢,这个问题是每个新人必问的问题之一,下面介绍几种同步方案,最后总结比较.

Activiti学习笔记

- - 企业架构 - ITeye博客
第一个Activiti的HelloWorld. 获取核心ProcessEngine对象 2. 根据需求,获取对应的服务实例 3. 使用服务方法,做事情 * * @author Administrator * */ public class HelloWorld {. // 加载核心API ProcessEngine.

Activiti工作流demo

- - CSDN博客综合推荐文章
继上篇《 Activiti工作流的环境配置》.        前几篇对Activiti工作流进行了介绍,并讲解了其环境配置. 本篇将会用一个demo来展示Activiti工作流具体的体现,直接上干货.        以HelloWorld程序为例.       首先说一下业务流程,员工张三提交了一个申请,然后由部门经理李四审核,审核通过后再由总经理王五审核,通过则张三申请成功.

Activiti - 设置会签

- - 企业架构 - ITeye博客
前些天在群里聊工作流和Activiti,群里有人分享了自己的工作流引擎开源项目,大伙纷纷问这问那(比如为什么突然自己搞个process engine、有没有eclipse plugin、能不能绘制流程图等等). 现实生活中的工作流程,我们也经常碰到需要会签的情况,支持会签是很必要的. 正好有两个人问道:支持会签吗.

activiti工作流使用

- - 行业应用 - ITeye博客
activiti 开发流程. JBPM 与 Activiti. jBPM项目于2002年3月由Tom Baeyens发起,2003年12月发布1.0版本. 2004年10月18日,发布了2.0版本,并在同一天加入了JBoss. 2011 年 jBPM的创建者Tom Baeyens离开JBoss了, 他的离开产生了两个结果:.

ACTIVITI 学习笔记 - 监听

- - 企业架构 - ITeye博客
ACTIVITI 学习笔记 - 监听. 所有分发的事件都是org.activiti.engine.delegate.event.ActivitiEvent的子类. 监听器监听的流程引擎已经创建完毕,并准备好接受API调用. 监听器监听的流程引擎已经关闭,不再接受API调用. 创建了一个新实体,初始化也完成了.

Activiti安装配置(转)

- - 企业架构 - ITeye博客
原文地址:http://blog.csdn.net/zhang_xinxiu/article/details/38655311. 有一段时间没有更新文章了,虽然有一直在写文章,可是一直没有更新到博客内,这段时间写的文章大多还是以技术为主. 接下来的系列文章将会来讨论企业工作流的开发,主要是来研究开源工作流Activiti的使用.

Activiti入门篇之二 Spring 与Activiti的入门整合

- - 行业应用 - ITeye博客
Activiti相对Jbpm来说,与Spring整合更加完美,具体可见本文的详细介绍. 1.     Maven的环境任务,请参考第一篇 (Activiti入门篇—Maven的环境准备). 2.     Activiti的Eclipse插件安装.               插件更新地址:http://activiti.org/designer/update/.

整合activiti在线流程设计器(ACTIVITI-MODELER 5.18.0)

- - 企业架构 - ITeye博客
一直以来都是从事大量的工作流相关的项目,用过很多商用的工作流产品,包括国内与国外的,尽管商用的工作产品在UI操作上比较人性化,但个人用户觉得,这东西只需要一些初级用户,对于我们一直在为一些高级的客户提供一些专业的数据整合、流程梳理、系统间的数据穿透时,这些系统因为不开源,给项目的实施带来巨大的风险,在一些项目栽过跟头后,我更偏向于使用开源的平台了.