该项目综合参考了:chenhai201 和 secsea
在进行下面的项目实践时,请先生成一个spring boot项目,并配置swagger2,开发工具建议使用IDEA。为了方便正文的描述,下面先给出项目的整体目录结构。
项目已经上传到github,需要可以自行下载
首先从github上下载Activiti的源码,命令如下
git clone https://github.com/Activiti/Activiti.git
git checkout 5.23.0-release
在项目中加上对activiti的依赖,整体项目pom如下所示
4.0.0
org.springframework.boot
spring-boot-starter-parent
1.5.4.RELEASE
org.self
activitidemo
0.0.1-SNAPSHOT
activitidemo
Demo project for Spring Boot Activiti
UTF-8
UTF-8
1.8
5.22.0
2.6.1
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
net.sourceforge.nekohtml
nekohtml
1.9.22
org.activiti
activiti-spring-boot-starter-basic
${activiti.version}
org.activiti
activiti-spring-boot-starter-actuator
${activiti.version}
org.activiti
activiti-rest
${activiti.version}
org.apache.xmlgraphics
batik-codec
1.7
org.apache.xmlgraphics
batik-css
1.7
org.apache.xmlgraphics
batik-svg-dom
1.7
org.apache.xmlgraphics
batik-svggen
1.7
org.activiti
activiti-explorer
${activiti.version}
org.activiti
activiti-diagram-rest
${activiti.version}
org.activiti
activiti-simple-workflow
${activiti.version}
org.activiti
activiti-spring
${activiti.version}
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-test
test
io.springfox
springfox-swagger2
${swagger.version}
io.springfox
springfox-swagger-ui
${swagger.version}
io.jsonwebtoken
jjwt
0.7.0
org.springframework.cloud
spring-cloud-dependencies
Dalston.SR1
pom
import
org.springframework.boot
spring-boot-maven-plugin
1)在org.self.activitidemo.config包下建立activiti.modeler.exploer包,然后在下载的Activiti源码中,找到目录modules\activiti-webapp-explorer2\src\main\java\org\activiti\explorer\servlet,然后复制FilterServletOutputStream.java、GenericResponseWrapper.java、JsonpCallbackFilter.java到上面建立的activiti.modeler.exploer包下,如下所示
2)从目录modules\activiti-webapp-explorer2中复制src\main\resources路径下的stencilset.json文件到本地项目的resources目录下
3)从目录modules\activiti-webapp-explorer2中复制src\main\webapp路径下的文件diagram-viewer和editor-app目录,以及modeler.html文件
4)从目录modules\activiti-webapp-explorer2中复制src\main\java\org\activiti\reset\editor路径下的文件main和model到controller下的editor包
如下所示:
1)找到controller.editor.main和controller.editor.model中的StencilsetRestResource.java、ModelEditorJsonRestResource.java、ModelSaveRestResource.java,统一在类上加
@RequestMapping(value = "/service")
2)在Spring boot的启动类ActivitidemoApplication,加上如下注解【ComponentScan里面的第二个包记得换成自己的包】
@ComponentScan({"org.activiti.rest.diagram", "org.self.activitidemo"})
@EnableAutoConfiguration(exclude = {
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class,
org.activiti.spring.boot.SecurityAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration.class
})
并在启动类中加上Bean,如下
@Bean
public JsonpCallbackFilter filter(){
return new JsonpCallbackFilter();
}
3)修改resource/static/editor-app下的app-cfg.js为
ACTIVITI.CONFIG = {
'contextRoot' : '/service',
};
4)在constant包下,添加两个辅助类Pagination.java和ResponseConstantManager.java
package org.self.activitidemo.constant;
import java.util.List;
/**
* 分页类
*/
public class Pagination {
public static int DEFAULT_PAGENUMBER = 1;
public static int DEFAULT_PAGESIZE = 10;
private List rows = null; //当前返回的记录列表
private int rowTotal = 0; //总记录数
private int pageTotal = 0; //总页数
protected int pageNumber = DEFAULT_PAGENUMBER; //页码
protected int pageSize = DEFAULT_PAGESIZE; //每页记录数
public Pagination() {
this(DEFAULT_PAGENUMBER, DEFAULT_PAGESIZE);
}
public Pagination(int pageNumber, int pageSize) {
setPageNumber(pageNumber);//pageNumber从1开始不是从0开始
setPageSize(pageSize);
}
public static Pagination getInstance(int pageNumber, int pageSize) {
return new Pagination(pageNumber, pageSize);
}
public static Pagination getInstance2Top(int top) {
return new Pagination(1, top);
}
public List getRows() {
return rows;
}
public void setRows(List rows) {
this.rows = rows;
}
public int getRowTotal() {
return rowTotal;
}
public void setRowTotal(int rowTotal) {
this.rowTotal = rowTotal;
}
public int getPageTotal() {
return pageTotal;
}
public void setPageTotal(int pageTotal) {
this.pageTotal = pageTotal;
}
public int getPageNumber() {
return pageNumber;
}
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getStart(){
return (getPageNumber()-1)*getPageSize();
}
public int getEnd(){
return getStart() + getPageSize();
}
}
package org.self.activitidemo.constant;
/**
* 用于维护响应需要的常量信息
*/
public class ResponseConstantManager {
public static final String STATUS_SUCCESS = "success";
public static final String STATUS_FAIL = "fail";
}
在service包下,加入ModelServer.java接口类,建立service.impl包,在service.impl下加入ModelServiceImpl.java,如下
package org.self.activitidemo.service;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
/** 流程模型管理的接口 */
public interface ModelService {
/** 新建一个模型 */
HashMap newModel(String modelName, String description, String key);
/** 获取所有模型 */
HashMap modelList();
/** 获取指定页码的模型 */
HashMap modelsPage(int pageSize, int pageNumber);
/** 删除指定模型 */
HashMap deleteModel(String modelId);
/** 部署模型 */
HashMap deployModel(String modelId);
/** 上传已有xml文件,并生成相应模型*/
HashMap uploadModel(MultipartFile modelFile);
}
package org.self.activitidemo.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.repository.Model;
import org.activiti.explorer.util.XmlUtil;
import org.apache.commons.lang3.StringUtils;
import org.self.activitidemo.constant.Pagination;
import org.self.activitidemo.constant.ResponseConstantManager;
import org.self.activitidemo.service.ModelService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
@Service
public class ModelServiceImpl implements ModelService {
private static final Logger logger = LoggerFactory.getLogger(ModelServiceImpl.class);
@Autowired
RepositoryService repositoryService;
@Autowired
ObjectMapper objectMapper;
@Override
public HashMap newModel(String modelName, String description, String key) {
HashMap result = new HashMap<>();
//初始化一个空模型
Model model = repositoryService.newModel();
//int revision = 1;
ObjectNode modelNode = objectMapper.createObjectNode();
modelNode.put(ModelDataJsonConstants.MODEL_NAME, modelName);
modelNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, description);
// modelNode.put(ModelDataJsonConstants.MODEL_REVISION, revision);
model.setName(modelName);
model.setKey(key);
model.setMetaInfo(modelNode.toString());
repositoryService.saveModel(model);
String id = model.getId();
//完善ModelEditorSource
ObjectNode editorNode = objectMapper.createObjectNode();
editorNode.put("id", "canvas");
editorNode.put("resourceId", "canvas");
ObjectNode stencilSetNode = objectMapper.createObjectNode();
stencilSetNode.put("namespace",
"http://b3mn.org/stencilset/bpmn2.0#");
editorNode.put("stencilset", stencilSetNode);
try {
repositoryService.addModelEditorSource(id, editorNode.toString().getBytes("utf-8"));
result.put("status", ResponseConstantManager.STATUS_SUCCESS);
result.put("modelId", id);
} catch (UnsupportedEncodingException e) {
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", e.toString());
}
return result;
}
@Override
public HashMap modelList() {
HashMap result = new HashMap<>();
List models = repositoryService.createModelQuery().orderByCreateTime().asc().list();
result.put("status", ResponseConstantManager.STATUS_SUCCESS);
result.put("models", models);
return result;
}
@Override
public HashMap modelsPage(int pageNumber, int pageSize) {
HashMap result = new HashMap<>();
Pagination pagination = new Pagination(pageNumber, pageSize);
int totalCount = (int) repositoryService.createModelQuery().count();
int totalPages = (int) Math.ceil(totalCount / pageSize);
pagination.setRowTotal(totalCount);
pagination.setPageTotal(totalPages);
List models = repositoryService.createModelQuery()
.orderByCreateTime().asc().listPage(pagination.getStart(), pagination.getEnd());
pagination.setRows(models);
result.put("status", ResponseConstantManager.STATUS_SUCCESS);
result.put("modelsPage", pagination);
return result;
}
@Override
public HashMap deleteModel(String modelId) {
HashMap result = new HashMap<>();
repositoryService.deleteModel(modelId);
result.put("status", ResponseConstantManager.STATUS_SUCCESS);
return result;
}
@Override
public HashMap deployModel(String modelId) {
HashMap result = new HashMap<>();
//获取模型
Model modelData = repositoryService.getModel(modelId);
byte[] bytes = repositoryService.getModelEditorSource(modelData.getId());
if (bytes == null) {
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", "模型数据为空,请先设计流程并成功保存,再进行发布。");
return result;
}
try {
JsonNode modelNode = new ObjectMapper().readTree(bytes);
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
if (model.getProcesses().size() == 0) {
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", "数据模型不符要求,请至少设计一条主线流程。");
}
//debug
byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(model);
// System.out.println(new String(bpmnBytes, "UTF-8"));
//发布流程
String processName = modelData.getName() + ".bpmn20.xml";
// DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
// .name(modelData.getName())
// .addString(processName, new String(bpmnBytes, "UTF-8"));
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
.name(modelData.getName())
.addBpmnModel(processName, model);
Deployment deployment = deploymentBuilder.deploy();
modelData.setDeploymentId(deployment.getId());
repositoryService.saveModel(modelData);
} catch (Exception e) {
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", e.toString());
}
return result;
}
@Override
public HashMap uploadModel(MultipartFile modelFile) {
HashMap result = new HashMap<>();
InputStreamReader in = null;
try {
try {
boolean validFile = false;
String fileName = modelFile.getOriginalFilename();
if (fileName.endsWith(".bpmn20.xml") || fileName.endsWith(".bpmn")) {
validFile = true;
XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
in = new InputStreamReader(new ByteArrayInputStream(modelFile.getBytes()), "UTF-8");
XMLStreamReader xtr = xif.createXMLStreamReader(in);
BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xtr);
if (bpmnModel.getMainProcess() == null || bpmnModel.getMainProcess().getId() == null) {
// notificationManager.showErrorNotification(Messages.MODEL_IMPORT_FAILED,
// i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMN_EXPLANATION));
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", "数据模型无效,必须有一条主流程");
} else {
if (bpmnModel.getLocationMap().isEmpty()) {
// notificationManager.showErrorNotification(Messages.MODEL_IMPORT_INVALID_BPMNDI,
// i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMNDI_EXPLANATION));
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", "locationMap为空");
} else {
String processName = null;
if (StringUtils.isNotEmpty(bpmnModel.getMainProcess().getName())) {
processName = bpmnModel.getMainProcess().getName();
} else {
processName = bpmnModel.getMainProcess().getId();
}
Model modelData;
modelData = repositoryService.newModel();
ObjectNode modelObjectNode = new ObjectMapper().createObjectNode();
modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, processName);
modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);
modelData.setMetaInfo(modelObjectNode.toString());
modelData.setName(processName);
repositoryService.saveModel(modelData);
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
ObjectNode editorNode = jsonConverter.convertToJson(bpmnModel);
byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(bpmnModel);
// System.out.println(new String(bpmnBytes, "UTF-8"));
// System.out.println(editorNode);
repositoryService.addModelEditorSource(modelData.getId(), editorNode.toString().getBytes("utf-8"));
result.put("status", ResponseConstantManager.STATUS_SUCCESS);
result.put("modelId", modelData.getId());
}
}
} else {
// notificationManager.showErrorNotification(Messages.MODEL_IMPORT_INVALID_FILE,
// i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_FILE_EXPLANATION));
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", "后缀名无效");
System.out.println("err3");
}
} catch (Exception e) {
// String errorMsg = e.getMessage().replace(System.getProperty("line.separator"), "
");
// notificationManager.showErrorNotification(Messages.MODEL_IMPORT_FAILED, errorMsg);
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", e.toString());
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// notificationManager.showErrorNotification("Server-side error", e.getMessage());
result.put("status", ResponseConstantManager.STATUS_FAIL);
result.put("message", e.toString());
}
}
}
return result;
}
}
在controlle.clientr包下,加入 ModelController.java,代码如下:
package org.self.activitidemo.controller.client;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.self.activitidemo.service.ModelService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
/**
* 流程模型Model操作相关
* Created by chenhai on 2017/5/23.
*/
@Api(description = "流程模型Model操作相关", tags = {"activitimodeler"})
@RestController
@RequestMapping("models")
public class ModelController {
private final static Logger logger = LoggerFactory.getLogger(ModelController.class);
@Autowired
private ModelService modelService;
/**
* 新建一个空模型
*
* @return
* @throws UnsupportedEncodingException
*/
@ApiOperation(value = "新建一个空模型")
@PostMapping(value = "newModel")
public ResponseEntity> newModel(@RequestParam(value = "modelName") String modelName,
@RequestParam(value = "description") String description,
@RequestParam(value = "key") String key) throws UnsupportedEncodingException {
HashMap responseBody = modelService.newModel(modelName, description, key);
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
/**
* 获取所有模型
*
* @return
*/
@ApiOperation(value = "获取所有模型")
@GetMapping("/getAll")
public ResponseEntity> modelList() {
HashMap responseBody = modelService.modelList();
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
/**
* 删除模型
*
* @param modelId
* @return
*/
@ApiOperation(value = "删除模型")
@DeleteMapping("delete/{modelId}")
public ResponseEntity> deleteModel(@PathVariable("modelId") String modelId) {
HashMap responseBody = modelService.deleteModel(modelId);
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
/**
* 发布模型为流程定义
*
* @param modelId
* @return
*/
@ApiOperation(value = "发布模型为流程定义")
@PostMapping("deploy/{modelId}")
public ResponseEntity> deploy(@PathVariable("modelId") String modelId) {
HashMap responseBody = modelService.deployModel(modelId);
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
@ApiOperation(value = "上传一个已有模型")
@PostMapping(value = "/uploadFile")
public ResponseEntity> deployUploadedFile(@RequestParam("modelFile") MultipartFile modelFile) {
HashMap responseBody = modelService.uploadModel(modelFile);
return ResponseEntity.status(HttpStatus.OK).body(responseBody);
}
}
5)修改controller.editor.model.ModelSaveRestResource.java,如下
@RestController
@RequestMapping(value = "/service")
public class ModelSaveRestResource implements ModelDataJsonConstants {
protected static final Logger LOGGER = LoggerFactory.getLogger(ModelSaveRestResource.class);
@Autowired
private RepositoryService repositoryService;
@Autowired
private ObjectMapper objectMapper;
@RequestMapping(value="/model/{modelId}/save", method = RequestMethod.PUT)
@ResponseStatus(value = HttpStatus.OK)
public void saveModel(@PathVariable String modelId, @RequestParam("name") String name,
@RequestParam("json_xml") String json_xml, @RequestParam("svg_xml") String svg_xml,
@RequestParam("description") String description) {//对接收参数进行了修改
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);
}
}
}
上面 操作之后整体目录结构如下所示:
6)进行数据库配置
先进行配置文件application.properties,内容如下
spring.application.name=activitidemo
server.port=8082
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://XXXX:3306/activiti_demo?ccharacterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=XXX
spring.datasource.password=XXX
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.show-sql=true
#关闭activiti的自动部署机制
spring.activiti.check-process-definitions=false
从上述的项目github地址中,下载db下的sql文件,并运行生成activiti的数据库表
运行Spring Boot项目,访问http://localhost:8082/swagger-ui.html,界面图如下所示:
点击newModel,填充参数,生成一个新的模型图,获取模型编号,如下
访问地址:http://localhost:8082/modeler.html?modelId=297501
界面如下
随便编辑,然后保存
加入thymeleaf依赖
org.springframework.boot
spring-boot-starter-thymeleaf
加入application.properties配置
#thymeleaf
spring.mvc.view.prefix=classpath:/templates
spring.mvc.view.suffix=.html
#spring.thymeleaf.prefix=classpath:/templates
spring.thymeleaf.mode=LEGACYHTML5
#spring.thymeleaf.cache=false
在resources加入templates目录,同时加入modeler.html文件
!doctype html>
Activiti Editor
{
{alerts.current.message}}
{
{alerts.queue.length + 1}}
在controller.client下加入EditorController.java
package org.self.activitidemo.controller.client;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
//这里要使用Controller,因为RestController是返回json数据的
@Api(tags = "EditorController", description = "模型编辑器")
@Controller
public class EditorController {
/**
* 等同于访问:modeler.html?modelId=300001,这个是静态资源直接访问
* @return
*/
@ApiOperation(value = "进入流程编辑器,需要接入模型参数,editor?modelId=XXX")
@GetMapping(value = "editor")
public String edtior() {
return "modeler";
}
}
访问http://localhost:8082/editor?modelId=297501