Activit BPMN Java Spring Boot

Activiti整合應用 - 以Spring Boot為例

Liam Tang 2020/03/06 14:47:19
6148

前言

Activiti是ㄧ套用java開發出來的工作流程引擎(workflow engine),支援JDK 6以上,目前最新的版本為Activiti 7,其每個版本都包含了以下內容:

1.核心API,基本上不會因為版本而大幅改變,僅有優化上的差異。

2.利用核心引擎開發好的功能模組,例如寫好的WebRestful API,或是ㄧ些功能元件,這部分各版本提供的內容不見得ㄧ樣。

而上述的內容都是open-source (專案連結),因此開發人員在利用Activiti搭建服務時,可以根據自己的需要來修改、取用原始碼並建置自己的服務。本文會就整合Activiti的角度來做介紹,首先會簡介Activiti引擎的主要架構,包括Table Schema7大核心服務;接著,會建置ㄧ個Spring Boot專案,並用Maven來做管理,逐步整合Activiti到此專案中

 

Activiti核心簡介

Activiti引擎與核心API架構如下圖(取自官方使用者說明)

如上圖所示,ProcessEngine為Activiti的核心,要取得各個服務之前,必須先產生ProcessEngine的實例,實作上使用者可以像圖中使用activiti.cfg.xml來產生ProcessEngineConfiguration,再用它來建置ProcessEngine。

activiti.cfg.xml其實就是ㄧ個Spring的配置檔,可以藉由它來透過Spring容器管理Activiti相關的Bean,如果不想用XML檔,使用者亦可以用Java物件的方式來實作配置(這會在後面介紹)。在這邊先請讀者看一個activiti.cfg.xml簡單範例,藉此了解一下Activiti基本配置:

<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 -->  
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">  
        <property name="url"  
            value="jdbc:sqlserver://192.168.99.100:1433;DatabaseName=Activiti_TEST" />  
        <property name="driverClass" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />  
        <property name="username" value="sa" />  
        <property name="password" value="1qaz2wsx" />  
    </bean>  
  
    <!-- 事務管理器bean -->  
    <bean id="transactionManager"  
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource" />  
    </bean>  
  
    <!-- 流程引擎配置bean -->  
    <bean id="processEngineConfiguration"  
        class="org.activiti.spring.SpringProcessEngineConfiguration">  
          
        <property name="dataSource" ref="dataSource" />  
        <property name="transactionManager" ref="transactionManager"/>  
        <property name="databaseSchemaUpdate" value="true" />  
      
        <property name="jobExecutorActivate" value="false" />  
        <property name="asyncExecutorEnabled" value="true" />  
        <property name="asyncExecutorActivate" value="false" />  
    </bean>  
</beans>  

上述的XML內容,我們可以看到配置了3Bean

1. 帶有資料庫連線資訊的Data Source

2. 利用Data Source產生的Transaction Manager

3. 由前2者,加上其它需要的屬性配置,組裝產生的ProcessEngineConfiguration 

至此我們可以得知,資料庫是Activiti重要的部分,我們只要有ActivitiLibrary +資料庫就能搭建出Activiti核心服務,那這邊可能會有一個疑問:

有了資料庫,那Table Schema怎麼辦?

這部分,在官方的提供下載檔案裡是有SQL Script讓我們手動執行的,不過不用這麼麻煩,使用者只要在ProcessEngineConfiguration的設置上,加入databaseSchemaUpdate = trueActiviti就會於啟動時檢查資料庫,並視乎狀況自動在資料庫建立需要的Table了。

註:Activiti有預設了一個In-Memory Database的資料庫H2,如果測試時沒有可用的資料庫,可以改用它,配置方式如下:

	<!-- 資料來源bean -->
	<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		<property name="url" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000" />
		<property name="driverClass" value="org.h2.Driver" />
		<property name="username" value="sa" />
		<property name="password" value="" />
	</bean>

 

下圖為Activiti自動於資料庫中建立的資料表:

 

根據Activiti的官方文件,所有ActivitiTable前面都會加上”ACT”的字首,後續都會用底線來分隔單字,根據第2個單字(通常為2個字母組成),可以將這個Table做功能上的分類,以下用表格說明:

表格名稱

解釋

ACT_RE_*

RE代表repository,儲存靜態資料,例如:流程圖、流程定義等等。

ACT_RU_*

RU代表runtime,儲存起案後,流程的執行時期所產生之資訊,例如:流程實體、關卡實體、流程變數….等等。

這些資訊在流程結束後消失,而必須至歷史檔(ACT_HI_*)查詢。

ACT_HI_*

HI代表history,儲存過去執行時期的各歷史資訊,例如:流程實體、關卡實體、流程變數等等。

ACT_ID_*

ID代表identity,儲存身份認證資訊,例如:使用者、群組等等。

然而,大部分系統都有自己針對使用者、群組與權限的控管方式,通常不會使用到這些Activiti預設的Table

ACT_GE_*

GE代表general,僅有兩個Table:

ACT_GE_PROPERTY存放Activiti引擎參數值(例如版本) ACT_GE_BYTEARRAY負責以二進位制形式儲存流程定義檔內容。

ACT_EVT_LOG

EVT代表event,此分類目前只有一個ACT_EVT_LOG,如果有開啟以下設定,Activiti用此Table來記錄Log

processEngineConfiguration.setEnableDatabaseEventLogging(true)

ACT_PROCDEF_INFO

此分類也是只有一個表,就字面上來看,應該也是跟流程定義檔案有關。

 

在取得ProcessEngine以後,就可以取得Activiti7個核心服務物件,如下程式碼範例(取自官方文件)

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(); 

各服務簡介如下表:

服務名稱

解釋

RepositoryService

想要對流程圖的定義檔進行存取,或是流程定義的佈署進行操作時,可使用此服務,就定位上是處理流程引擎中較為靜態的資料。

RuntimeService

想要利用佈署中的流程定義來產生案件的時候,可以使用此服務執行起案以產生流程實體,同時也能對進行中流程的流程實體、變數與狀態進行查詢與操作。

TaskService

想要對流程實體中的關卡進行操作時,就會使用到此服務,基本上從待辦關卡的查詢、關卡的指派、關卡的認領以及完成關卡等等的系統動作,都可以藉由此服務來達成。

IdentityService

如果有使用Activiti預設之身份認證功能,可以用此服務來針對使用者與群組進行管理操作。

FormService

流程中不同階段可能會與特定的表單內容有關聯,例如起案時有起案用的表單,關卡簽核時也有個自的表單,這些表單的變數內容都可以定義在流程圖檔(bpmn)內,並且利用此服務來傳入表單內容值進行起案與送審。

HistoryService

所有流程引擎過去蒐集到的資訊都可以透過此服務來查詢,包括已結案與尚未結案的資料。

ManagementService

可以對流程引擎本身進行管理與查詢的服務,包括取得資料庫metadata的資訊,或是對一些Activiti的棑程功能(像是Timers)進行管理。

 

Activiti整合Spring Boot

接下來將用實際範例,介紹如何整合Activiti到自己專案中,以讓各位有更具體的了解,當然在實作前我們也先訂下一些目標:

1. 範例將會移植官方的Activiti Modeler到專案中,這讓使用者可以透過Web畫面來進行流程圖(bpmn)的繪製;

2. 使用者可以針對已存檔的流程圖進行編輯、刪除與匯出成流程圖檔(bpmn,即xml檔)。

此範例預計使用Spring Boot來建置,開發環境為Eclipse + JDK 1.8Spring Boot的版本採用2.2.2Activiti的版本採用5.23.0,展示層模板框架使用Spring Boot推薦的thymeleaf,架構上則是ㄧ般的MVC Web

     一、 創建新Maven專案

              開啟Eclipse -> New -> Project -> 選擇Maven Project,按照步驟完成新增。

      

 

專案建立好了之後可以先調整一下專案的路徑結構,並先新增一個Spring Boot默認的屬性設定檔,可參考下圖:

      

      請注意:

      1. 確保有src/main/resources路徑

      2. application.properties (application.yml)Spring Boot標準的屬性配置檔案,默許位置需放在src/main/resources之下。

      3. src/main/resources/staticSpring Boot默許任可之靜態資源放置路徑,之後會使用到它。

      4. src/main/resources/templates會用來放置thymeleafhtml檔,一樣是Spring Boot默許認可的位置。

     

      二、pom.xml設定

      關於pom.xml,我們可以先加入Parent標籤使其繼承spring-boot-starter-parent,這樣後續加入其它依賴(depency)的時候可以減少一些 版本上的對應。根據預想的需求,請先在pom.xml加入以下內容(還未包括Activiti的部分):

<!-- 專案基於Spring Boot 2.2.2建置,繼承此POM,方便整合其他依賴 -->  
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>2.2.2.RELEASE</version>  
    <relativePath /> <!-- lookup parent from repository -->  
</parent>  
  
<dependencies>  
    <!-- Spring Boot Web應用開發相關依賴 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <!-- 使用 thymeleaf 作為Web MVC 展示層框架 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-thymeleaf</artifactId>  
    </dependency>  
    <!-- Spring Boot應用測試相關依賴 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
    <!-- 資料庫連線使用之JDBC(可自行調整) -->  
    <dependency>  
        <groupId>com.microsoft.sqlserver</groupId>  
        <artifactId>mssql-jdbc</artifactId>  
        <scope>provided</scope>  
    </dependency>  
    <!-- 前端library -->  
    <dependency>  
        <groupId>org.webjars</groupId>  
        <artifactId>bootstrap</artifactId>  
        <version>3.3.7</version>  
    </dependency>  
    <!-- Maven專案預設引用的junit依賴 -->  
    <dependency>  
        <groupId>junit</groupId>  
        <artifactId>junit</artifactId>  
        <!-- <version>3.8.1</version> -->  
        <scope>test</scope>  
    </dependency>  
</dependencies>

 

      三、Activiti整合

在此範例我們預計使用Activiti 5版,因此我們先在pom.xmlProperties標籤中加入Activiti的版本號,如下:

<properties>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
    <!-- Java Compile Version -->  
    <java.version>1.8</java.version>   
    <!-- Activiti Version -->  
    <activiti-version>5.23.0</activiti-version>  
</properties>

 接著加入Activiti核心引擎的相關依賴,如下:

<!-- Activiti核心引擎 -->  
<dependency>  
    <groupId>org.activiti</groupId>  
    <artifactId>activiti-engine</artifactId>  
    <version>${activiti-version}</version>  
</dependency>  
<!-- Activiti整合Spring -->  
<dependency>  
    <groupId>org.activiti</groupId>  
    <artifactId>activiti-spring</artifactId>  
    <version>${activiti-version}</version>  
</dependency> 

 

     接下來,將進行Activiti的配置,我們將利用Spring透過Java物件的方式來完成,以下逐步說明:

     1. application.properties加入資料庫連線相關屬性(視乎資料庫環境調整):    

activiti.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver  
activiti.datasource.url=jdbc:sqlserver://192.168.99.100:1433;DatabaseName=Activiti_TEST  
activiti.datasource.username=sa  
activiti.datasource.password=

   如果想要用Activiti自帶的H2 資料庫,也可以設定如下:

activiti.datasource.driver-class-name=org.h2.Driver  
activiti.datasource.url=jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000  
activiti.datasource.username=sa  
activiti.datasource.password= 

   

      2. 有別於使用activiti.cfg.xml,本範例將使用java + annotation的方式來配置Actitviti,請於source folder的/config/底下新增一支 java類別,本範例命名為ActivitiConfig.java,內容如下:

package com.liam.config;  
  
import java.sql.Driver;  
  
import javax.sql.DataSource;  
  
import org.activiti.engine.FormService;  
import org.activiti.engine.HistoryService;  
import org.activiti.engine.IdentityService;  
import org.activiti.engine.ManagementService;  
import org.activiti.engine.ProcessEngine;  
import org.activiti.engine.RepositoryService;  
import org.activiti.engine.RuntimeService;  
import org.activiti.engine.TaskService;  
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;  
import org.activiti.spring.ProcessEngineFactoryBean;  
import org.activiti.spring.SpringProcessEngineConfiguration;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.jdbc.datasource.DataSourceTransactionManager;  
import org.springframework.jdbc.datasource.SimpleDriverDataSource;  
import org.springframework.transaction.PlatformTransactionManager;  
  
@Configuration  
public class ActivitiConfig {  
  
    @Value ("${activiti.datasource.url}")  
    private String url;  
      
    @Value("${activiti.datasource.username}")  
    private String username;  
      
    @Value("${activiti.datasource.password}")  
    private String password;  
      
    @Value("${activiti.datasource.driver-class-name}")  
    private String driverClassName;  
  
    /** 
     * 配置Activiti Data Source 
     * @throws ClassNotFoundException  
     */  
    @SuppressWarnings("unchecked")  
    @Bean  
    public DataSource activitiDataSource() throws ClassNotFoundException {  
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();  
        dataSource.setUrl(url);  
        dataSource.setDriverClass((Class<? extends Driver>) Class.forName(driverClassName));  
        dataSource.setUsername(username);  
        dataSource.setPassword(password);  
        return dataSource;  
    }  
  
    /** 
     * 配置Transaction Manager 
     * @throws ClassNotFoundException  
     */  
    @Bean  
    public PlatformTransactionManager transactionManager() throws ClassNotFoundException {  
        return new DataSourceTransactionManager(activitiDataSource());  
    }  
  
    /** 
     * 配置並產生流程引擎設置實例 
     * @throws ClassNotFoundException  
     */  
    @Bean  
    public ProcessEngineConfigurationImpl processEngineConfiguration() throws ClassNotFoundException {  
        SpringProcessEngineConfiguration processEngineConfiguration = new SpringProcessEngineConfiguration();  
        processEngineConfiguration.setDataSource(activitiDataSource());  
        processEngineConfiguration.setDatabaseSchemaUpdate("true");  
        processEngineConfiguration.setTransactionManager(transactionManager());  
        processEngineConfiguration.setJobExecutorActivate(false); // Activiti 5  
        // processEngineConfiguration.setAsyncExecutorActivate(true); //Activiti 6  
        processEngineConfiguration.setAsyncExecutorEnabled(true); // Activiti 5  
        processEngineConfiguration.setAsyncExecutorActivate(true);  
        processEngineConfiguration.setHistory("full");  
  
        return processEngineConfiguration;  
    }  
  
    /** 
     * 利用流程引擎設置實例產生流程引擎FactoryBean 
     * @throws ClassNotFoundException  
     */  
    @Bean  
    public ProcessEngineFactoryBean processEngineFactoryBean() throws ClassNotFoundException {  
        ProcessEngineFactoryBean processEngineFactoryBean = new ProcessEngineFactoryBean();  
        processEngineFactoryBean.setProcessEngineConfiguration(processEngineConfiguration());  
        return processEngineFactoryBean;  
    }  
  
    /** 
     * 取得ProcessEngine 
     */  
    @Bean  
    public ProcessEngine processEngine() {  
  
        try {  
            return processEngineFactoryBean().getObject();  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
      
    @Bean  
    public RepositoryService repositoryService() {  
        return processEngine().getRepositoryService();  
    }  
  
    @Bean  
    public RuntimeService runtimeService() {  
        return processEngine().getRuntimeService();  
    }  
  
    @Bean  
    public TaskService taskService() {  
        return processEngine().getTaskService();  
    }  
  
    @Bean  
    public HistoryService historyService() {  
        return processEngine().getHistoryService();  
    }  
  
    @Bean  
    public FormService formService() {  
        return processEngine().getFormService();  
    }  
  
    @Bean  
    public IdentityService identityService() {  
        return processEngine().getIdentityService();  
    }  
  
    @Bean  
    public ManagementService managementService() {  
        return processEngine().getManagementService();  
    }  
} 

    上述程式碼中透過@Value取得application.properties內的資訊,並依序設置了相關的物件直到取得ProcessEngine,最後由ProcessEngine取得Activiti7大服務且全部都交由Spring控管,未來只要透過注入就能使用它們。

 

    到目前為止我們已經達成基本的整合了,雖然尚無任何功能,但還是來啟動看看吧~

    請找到含有程式進入點(main方法)java類別,在新增專案時應該就有預設一支App.java了,找到它之後請幫它加上@SpringBootApplication並調整main方法(如不想執行Spring Security相關的配置就把它exclude),參考如下:   

package com.liam;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;  
  
@SpringBootApplication(exclude = {  
        SecurityAutoConfiguration.class  
        })  
public class App   
{  
    public static void main( String[] args )  
    {  
        SpringApplication.run(App.class, args);  
    }  
}  

    執行main方法,如果能順利啟動沒有出錯,就代表配置就OK了,如果一開始設定給它的資料庫是尚未建立Activiti資料表的話,於啟動完成後可以去看看是不是都自動建立起來了。

 

 

Activiti整合應用實例

此部份要來介紹幾個可以做的應用,以供各位參考:

   1. 將官方做好的Activiti Modeler移植進來,讓使用者可以於Web畫面繪製流程圖。

   2. 編輯、刪除與下載流程圖。

 

一、Activiti Modeler

Activiti Modeler是官方寫好的功能,讓使用者可以透過Web介面來繪製Activiti的流程圖檔(bpmn,內容xml),自Activiti 5版就已經使用在官方提供的Web應用內,Activiti 5的時候叫做Activiti-Explorer,Activiti 6則改版為Activiti-App下面為Activiti Modeler的功能畫面:

 

如果想要移植此畫面功能到自己的專案,我們需要先取得Activiti 5的原始碼,請至Activiti 5的GitHub (https://github.com/Activiti/Activiti/tree/5.x)將原始碼clone下來。接下來針對需要的幾個步驟一一說明:

   

    A.靜態資源與前端程式碼

程式碼clone下來後,請先找到路徑Activiti\modules\activiti-webapp-explorer2\src\main\webapp,並在該路徑找到以下3個資源:

1. diagram-viewer資料夾

2. editor-app資料夾

3. modeler.html

如下圖:

 

請將上述資源,複製到專案resources目錄的/static/底下,如下圖:

回到Activiti原始碼,至Activiti\modules\activiti-webapp-explorer2\src\main\resourcesstencilset.json檔,如下顯示:

此檔案是Activiti Modeler的文字資源,基本上可以定義不同語系的內容來套用至UI上,目前官方是只有提供英文版本的,請將它放置到resources目錄底下,如下圖:

 

B.畫面功能後端程式

以上Activiti Modeler前端與靜態資源處理好之後,接著要處理對應後端程式的整合,首先打開Activiti原始碼至以下路徑

Activiti\modules\activiti-modeler\src\main\java\org\activiti\rest\editor,以此目錄為根可以往下找到三支java類別:

..\main\StencilsetRestResource.java,此類別處理讀取stencilset.json的事務,

..\model\ModelEditorJsonRestResource.java、

..\model\ModelSaveRestResource.java,

2類別為Activiti Modeler畫面功能的後端API接口,如果開啟程式碼來看,此2支類別都有用@Autowired來注入ActivitiRepositoryService,有鑑於我們在上節的設置(ActivitiConfig)已經將Activiti7大服務都交由Spring來控管了,因此這邊不需做調整程式碼,請直接把這3支程式放到我們的專案底下吧,個人做法是把整個editor目錄移植到controller底下,如下圖:

想當然爾,剛放進來時一定會有一些error,先調整package~

調整完後會發現還是少了一些函數庫,此時請到pom.xml加入以下依賴即可:

<!-- Activiti modeler -->  
<dependency>  
    <groupId>org.activiti</groupId>  
    <artifactId>activiti-modeler</artifactId>  
    <version>${activiti-version}</version>  
</dependency>  
          
<dependency>  
    <groupId>org.apache.commons</groupId>  
    <artifactId>commons-io</artifactId>  
    <version>1.3.2</version>  
</dependency>  

加入之後重整專案, error應該都不見了囉~

 

C.前後端路徑Mapping

放置到自己專案的Activiti Modeler之前端資源裡,自以下路徑可以找到app-cfg.js:

/static/editor-app/app-cfg.js,如下圖:

如圖反白處所示,這邊定義了一個根路徑,即前端畫面向後端發出的request的時候,會帶有/activiti-explorer/service/這樣的前綴,這邊我們必須讓前後端可以對應,如果不想要修改app-cfg.js,那我們只要在StencilsetRestResource.javaModelEditorJsonRestResource.javaModelSaveRestResource.java的類別名稱上加入以下annotation就可以了:

@RequestMapping(value = "/activiti-explorer/service")

    如下圖:

    

 

          D.調整Model儲存方法

      ModelSaveRestResource.javasaveModel方法為Activiti Modeler中儲存按鈕的對應後端處理功能,然而直接使用會有問題,必須調整方法的傳入參數與內容,原方法宣告如下:

public void saveModel(@PathVariable String modelId, @RequestBody MultiValueMap<String, String> values) { 

    必須把移除Map型態的參數,將內容拆開來寫,調整如下:

@RequestMapping(value="/model/{modelId}/save", method = RequestMethod.PUT)  
@ResponseStatus(value = HttpStatus.OK)  
public void saveModel(@PathVariable String modelId, String name, String description, String json_xml, String svg_xml) {  
  try {  
      
    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();  
    // Setup output  
    ByteArrayOutputStream outStream = new ByteArrayOutputStream();  
    TranscoderOutput output = new TranscoderOutput(outStream);  
      
    // Do the transformation  
    transcoder.transcode(input, output);  
    final byte[] result = outStream.toByteArray();  
    repositoryService.addModelEditorSourceExtra(model.getId(), result);  
    outStream.close();  
      
  } catch (Exception e) {  
    LOGGER.error("Error saving model", e);  
    throw new ActivitiException("Error saving model", e);  
  }  
} 

 

            E.功能畫面進入點

最後我們需要為Activiti Modeler準備一個URL做為功能的進入點,請在/controller/底下自行增加一支java類別,本範例命名為ActivitiModelController,如下圖:

     

     程式碼如下參考內容,主要功能是新增ㄧ個預設的Model資料與對應的空白流程圖檔,並轉頁至Activiti Modeler頁面:    

package com.liam.controller;  
  
import java.io.IOException;  
import java.io.UnsupportedEncodingException;  
  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
  
import org.activiti.editor.constants.ModelDataJsonConstants;  
import org.activiti.engine.RepositoryService;  
import org.activiti.engine.repository.Model;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
import com.fasterxml.jackson.databind.ObjectMapper;  
import com.fasterxml.jackson.databind.node.ObjectNode;  
  
@RestController  
@RequestMapping("/model/")  
public class ActivitiModelController {  
    @Autowired  
    private RepositoryService repositoryService;  
  
    /** 
     * Activiti Modeler功能畫面進入點,進入時同時新增空白Model 
     * @param request 
     * @param response 
     */  
    @RequestMapping("/new")  
    public void createModel(HttpServletRequest request, HttpServletResponse response) {  
        String defaultModelName = "ModelName"; //Model初始化預設名稱  
        ObjectMapper objectMapper = new ObjectMapper();  
  
        try {             
            // 初始化Model  
            Model modelData = repositoryService.newModel();  
            ObjectNode modelObjectNode = objectMapper.createObjectNode();  
            modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, defaultModelName);  
            modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);  
            modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, "description");  
            modelData.setMetaInfo(modelObjectNode.toString());  
            modelData.setName(defaultModelName);   
              
            // 將Model資訊到ACT_RE_MODEL,取得Model ID  
            repositoryService.saveModel(modelData);  
              
            // 初始化Model對應之流程圖資訊(空白)  
            ObjectNode editorNode = objectMapper.createObjectNode();  
            ObjectNode stencilSetNode = objectMapper.createObjectNode();  
            stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");  
            editorNode.put("id", "canvas");  
            editorNode.put("resourceId", "canvas");  
            editorNode.set("stencilset", stencilSetNode);  
  
            // 將流程圖資訊存入ACT_GE_BYTEARRAY  
            repositoryService.addModelEditorSource(modelData.getId(), editorNode.toString().getBytes("utf-8"));  
              
            // 進入Activiti Modeler功能畫面 
            response.sendRedirect(request.getContextPath() + "/modeler.html?modelId=" + modelData.getId());  
        } catch (UnsupportedEncodingException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        } catch (IOException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
    }  
} 

新增完之後就可以透過URL開啟Activiti Modeler畫面囉!

 

請啟動專案,並在瀏覽器輸入以下URL: localhost:8080/model/new

    便會開啟畫面,顯示如下圖:

 

      或許有人會注意到,上述程式碼是用{ContextPath}/modeler.html?modelId={modelId} 來進入Activiti Modeler畫面的,所以在功能上,其實controller是先新增ㄧ個空白的Model,取得modelId,再做為URL參數開啟畫面,這樣的進入方法拿來應用到編輯既有的Model也是非常方便。

 

二、編輯、刪除與下載

      在這部份,我們將用ㄧ個簡單的畫面來將已經存入資料庫的流程圖Model資料列出來,並加上編輯、刪除與下載的功能進入點。

      A.畫面模版

      本範例使用Spring Boot建議搭配使用的thymeleaf模版框架來處理頁面(關於thymeleaf可至官方網站了解內容)。請至resources下的template資料夾 新增html檔,範例命名為processes.html,如下圖:

      

     

      html檔內容請參考以下,使用thymeleaf的語法來處理動態顯示的部分:               

<!DOCTYPE html>  
<html xmlns:th="http://www.thymeleaf.org">  
<head>  
<title>Process Models</title>  
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  
<link rel="stylesheet" type="text/css"  
    href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" />  
  
</head>  
<body>  
    <div class="container">  
        <div>  
            <a th:href="@{model/new}" class="btn btn-info">Add</a>  
        </div>  
        <table class="table table-striped">  
            <thead>  
                <tr>  
                    <td>ID</td>  
                    <td>Name</td>  
                    <td>Function</td>  
                </tr>  
            </thead>  
            <tbody>  
                <tr th:each="model : ${processModels}">  
                    <td th:text="${model.id}"></td>  
                    <td th:text="${model.name}"></td>  
                    <td>  
                        <a th:href="@{/modeler.html?modelId={id}(id=${model.id})}" class="btn btn-default">Edit</a>  
                        <a th:href="@{model/delete/{id}(id=${model.id})}" class="btn btn-default">Delete</a>  
                          
                        <a th:if="${model.hasEditorSourceExtra() == true}"   
                        th:href="@{model/export/{id}(id=${model.id})}" class="btn btn-default">Export</a>  
                    </td>  
                </tr>  
            </tbody>  
        </table>  
    </div>  
</body>  
</html>  

 

      B.畫面Controller

      我們替前面的頁面增加ㄧ個進入點,並做為此專案的首頁,請新增ㄧ支Controller,此範例命名為HomeController,如下圖:

 

                   

     

      這支類別需要的功能很簡單,我們只需要透過RepositoryService取得已經存在的Activiti Model資料,並把資訊提供給頁面來宣染就好了,程式碼如下:

package com.liam.controller;  
  
import java.util.List;  
  
import org.activiti.engine.RepositoryService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.servlet.ModelAndView;  
  
@Controller  
public class HomeController {  
  
    @Autowired  
    private RepositoryService repositoryService; //注入Activiti的RepositoryService  
      
    @RequestMapping({"/", "/processes"})  
    public ModelAndView home() {  
          
        // 取出已存入DB的Model清單  
        List<org.activiti.engine.repository.Model> processModels = repositoryService.createModelQuery().list();         
          
        ModelAndView modelAndView = new ModelAndView("/processes");  
        modelAndView.addObject("processModels", processModels);  
        return modelAndView;  
    }  
}  

 

      由於home方法所對應的路徑包含”/”,所以可以作為整個應用的首頁,現在我們重啟服務,連進localhost:8080就可以看到類似以下的畫面囉。

      

 

 

      Add: 初始化ㄧ個空白流程圖Model並進入Activiti Modeler,對應路徑 model/new

      Edit: 透過Activiti Modeler編輯此流程圖Model,對應路徑model/edit/{id}

      Delete: 刪除此流程圖Model,對應路徑model/delete/{id}

      Export: 下載此流程圖ModelBPMN檔,如果並未在Activiti Modeler畫面存檔過便無資料可以下載,因此按鈕不顯示,對應路徑model/export/{id}

 

      C.Model編輯

      於processes.html中,編輯按鈕對應的路徑是{model/edit/{id},請在ActivitiModelController增加ㄧ個方法對應到此路徑,並將畫面轉導至Activiti Modeler開啟編輯,方法內容如下參考:     

@RequestMapping(value = "/edit/{modelId}", method = RequestMethod.GET)  
public void editModel(@PathVariable String modelId, HttpServletRequest request, HttpServletResponse response) {  
    try {  
        // 進入Activiti Modeler功能畫面  
        response.sendRedirect(request.getContextPath() + "/modeler.html?modelId=" + modelId);  
    } catch (IOException e) {  
        // TODO Auto-generated catch block  
        e.printStackTrace();  
    }  
}  

      

      D.Model刪除

      於processes.html中,刪除按鈕對應的路徑是{model/delete/{id},請在ActivitiModelController增加ㄧ個方法對應到此路徑,並實作刪除功能,方法內容如下參考:     

 

@RequestMapping(value = "/delete/{modelId}", method = RequestMethod.GET)  
public void deleteModel(@PathVariable String modelId, HttpServletRequest request, HttpServletResponse response) {  
    try {  
        //透過repositoryService刪除Activiti Model  
        repositoryService.deleteModel(modelId);  
        response.sendRedirect(request.getContextPath() + "/processes");  
    } catch (IOException e) {  
        // TODO Auto-generated catch block  
        e.printStackTrace();  
    }  
}  

 

      E.Model匯出

      於先前的processes.html中,下載按鈕對應的路徑是{model/export/{id},請在ActivitiModelController增加ㄧ個方法對應到此路徑,並實作匯出bpmn檔的功能。匯出bpmn檔,需要把先前儲存在ACT_GE_BYTEARRAY的資料轉換成bpmn檔的格式,也就是xml,因此需要進行資料格式的轉換,不過這些功能元件Activiti官方都寫好了,只要用它們提供的轉換器即可,因此請先在pom.xml加入以下依賴:

<!-- Activiti Generate BPMN -->  
<dependency>  
    <groupId>org.activiti</groupId>  
    <artifactId>activiti-simple-workflow</artifactId>  
    <version>${activiti-version}</version>  
</dependency> 

 

      然後ActivitiModelController新增的方法,內容參考如下程式碼:      

@RequestMapping(value = "/export/{modelId}", method = RequestMethod.GET)  
public ResponseEntity<Resource> downloadFile(@PathVariable String modelId, HttpServletRequest request) {  
  
    String fileName = null;  
    byte[] bpmnBytes = null;  
    // Load file as Resource  
    Resource resource = null;  
    // Content Type = XML  
    String contentType = "application/xml";  
  
    try {  
        Model modelData = repositoryService.getModel(modelId);  
        if (null != modelData) {  
            if ("table-editor".equals(modelData.getCategory())) {  
                byte[] modelSource = repositoryService.getModelEditorSource(modelId);  
  
                SimpleWorkflowJsonConverter converter = new SimpleWorkflowJsonConverter();  
                WorkflowDefinitionConversionFactory conversionFactory = new WorkflowDefinitionConversionFactory();  
                WorkflowDefinition workflowDefinition = converter.readWorkflowDefinition(modelSource);  
                fileName = workflowDefinition.getName();  
                WorkflowDefinitionConversion conversion = conversionFactory  
                        .createWorkflowDefinitionConversion(workflowDefinition);  
                conversion.convert();  
                bpmnBytes = conversion.getBpmn20Xml().getBytes("utf-8");  
  
            } else {  
                JsonNode editorNode = new ObjectMapper()  
                        .readTree(repositoryService.getModelEditorSource(modelData.getId()));  
                BpmnJsonConverter jsonConverter = new BpmnJsonConverter();  
                BpmnModel model = jsonConverter.convertToBpmnModel(editorNode);  
                fileName = model.getMainProcess().getId() + ".bpmn20.xml";  
                bpmnBytes = new BpmnXMLConverter().convertToXML(model);  
            }  
        }  
  
        ByteArrayInputStream in = new ByteArrayInputStream(bpmnBytes);  
        resource = new InputStreamResource(in);  
    } catch (IOException ex) {  
        // logger.info("Could not determine file type.");  
    }  
  
    return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType))  
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").body(resource);  
} 

 

      到這邊所有功能都完成了,請重啟專案,在畫面上試試看這些功能吧。

      本專案有上傳至Git: 連結有需要可以Clone下來參考,目前連線資訊預設為H2 In-memory DB,所以不需要資料庫就可把服務跑起來,當然有需要調整資料庫的話也可以至application.properties調整,並於pom.xml去加入對應JDBC的依賴。

 

結語

本範例主要是圍繞Activiti Modeler與流程圖編輯相關的整合應用,因此只使用到Activiti 7大核心服務的RepositoryService,其他用於流程起案簽核的功能都未用到,實務上那些可能才是更重要的部分,不過那些要實作的東西就相當多了,可能無法簡單介紹完~ 無論如何,希望本篇能協助讀者對Activiti有多ㄧ點認識,畢竟它算是相當成熟的流程引擎,又是open source,應該蠻有機會在實務上使用到它的吧!

 

參考資料

Activiti User Guide (5)

Thymeleaf Official Documentation

Activiti 6 + SpringBoot 整合範例

springboot + activiti + modeler

Spring Boot 靜態資源處理

 

Liam Tang