简单的Activiti Modoler 流程在线编辑器
1.需求
我们公司使用的流程是activiti5.22.0,版本有些老了,然后使用的编辑器都是eclipse的流程编辑器插件,每次编辑流程需要打开eclipse进行编辑,然后再导入到项目里面,不是特别方便,所以我们决定使用官方提供的 Activiti Modoler,实现项目集成在线编辑器,方便流程的发布编辑以及部署。
2.具体实现步骤
整体的实现步骤就是从官网下载对应的activiti5.22.0 或者版本相近的包,然后找到对应的前端需要集成的文件以及controller等文件,添加到自己的项目中,我使用的springboot项目,然后需要配置静态资源的对应的地址信息,便可以进行简单的使用了。
2.1 添加pom需要jar包
我们此处就只列举了流程所需包,其他包简略
<!-- activiti 5.22.0启动器,排除mybatis依赖 --><dependency><groupId>org.activiti</groupId><artifactId>activiti-spring-boot-starter-basic</artifactId><version>5.22.0</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.activiti</groupId><artifactId>activiti-diagram-rest</artifactId><version>5.22.0</version></dependency><!-- Activiti在线设计 --><dependency><groupId>org.activiti</groupId><artifactId>activiti-modeler</artifactId><version>5.22.0</version></dependency>
2.2 下载包
我们下载的地址为 activiti5.x.x 官方包
2.3 解压复制文件到项目中
替换文件主要是两部分,一部分为java文件,一部分为静态资源文件,首先替换java文件
activiti-webexplore\Activiti-5.x\modules\activiti-modeler\src\main\java\org\activiti\rest\editor
向下的所有java文件到自己项目中
注意,
ModelSaveRestResource
对应的请求方式修改为 POST。
在之后我们需要添加一个发起流程图编辑或新增的入口 ModelerController
,发起以及修改全部从此方法进入,最后导入后台文件效果如下
最后添加的几个controller代码如下:
-
ModelEditorJsonRestResource
package cn.git.workflow.modeler;import cn.git.common.exception.ServiceException; import cn.git.common.util.LogUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; 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.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController;import java.nio.charset.StandardCharsets;/*** @description: 通过modelId获取流程model json数据* @program: bank-credit-sy* @author: lixuchun* @create: 2024-11-21*/ @Api(value = "model流程设计器获取json数据", tags = "model流程设计器获取json数据") @Slf4j @RestController public class ModelEditorJsonRestResource implements ModelDataJsonConstants {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate ObjectMapper objectMapper;/*** 获取流程图json数据** @param modelId* @return*/@ApiOperation(value = "获取流程图json数据", notes = "获取流程图json数据")@RequestMapping(value = "/model/{modelId}/json", method = RequestMethod.GET, produces = "application/json")public Object getEditorJson(@PathVariable String modelId) {// 最终响应数据ObjectNode modelNode = null;Model model = repositoryService.getModel(modelId);// 获取流程定义if (ObjectUtil.isNotNull(model)) {try {// 获取流程定义的JSON数据if (StrUtil.isNotBlank(model.getMetaInfo())) {modelNode = (ObjectNode) objectMapper.readTree(model.getMetaInfo());} else {modelNode = objectMapper.createObjectNode();modelNode.put(MODEL_NAME, model.getName());}// 设置流程定义的IDmodelNode.put(MODEL_ID, model.getId());// 获取流程定义的JSON数据ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(new String(repositoryService.getModelEditorSource(model.getId()), StandardCharsets.UTF_8));modelNode.put("model", editorJsonNode);} catch (Exception e) {log.error(StrUtil.format("通过modelId获取model对应json数据失败,失败信息为[{}]", LogUtil.getStackTraceInfo(e)));throw new ServiceException("通过modelId获取model对应json数据失败!");}}return JSONObject.parseObject(modelNode.toString());} }
-
ModelerController
package cn.git.workflow.modeler;import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.activiti.editor.constants.ModelDataJsonConstants; import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.Model; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam;import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.rmi.ServerException;/** * @description: model流程设计器保存* @program: bank-credit-sy* @author: lixuchun* @create: 2024-11-21*/ @Api(value = "model流程设计器保存", tags = "model流程图画图模块保存") @Controller @RequestMapping("/modeler") public class ModelerController {@Resourceprivate RepositoryService repositoryService;@Resourceprivate ObjectMapper objectMapper;/*** 初始化流程设计器方法,跳转至流程设计器** @param modelName 模型名称* @param modelKey 模型key* @param modelDescription 模型描述* @param modelId 模型id* @return*/@ApiOperation(value = "初始化流程设计器方法,跳转至流程设计器", notes = "初始化流程设计器方法,跳转至流程设计器")@GetMapping("/save")public void save(HttpServletResponse response,@RequestParam(value = "modelName", required = false) String modelName,@RequestParam(value = "modelKey", required = false) String modelKey,@RequestParam(value = "modelDescription", required = false) String modelDescription,@RequestParam(value = "modelId", required = false) String modelId) throws IOException {// 如果有modelId,则直接跳转if (StrUtil.isNotBlank(modelId)) {Model model = repositoryService.getModel(modelId);if (ObjectUtil.isNotNull(model)) {response.sendRedirect("/static/activiti/modeler.html?modelId=" + model.getId());} else {throw new ServerException(StrUtil.format("模型不存在,请检查modelId[{}]", modelId));}} else {// modelId为空,则表示请求为新建一个模型,则进行必填参数校验if (StrUtil.isBlank(modelName) ||StrUtil.isBlank(modelKey) || StrUtil.isBlank(modelDescription)) {throw new ServerException("参数[modelName, modelKey, modelDescription]不能为空,请确认!");}// 创建模型Model modelData = repositoryService.newModel();ObjectNode modelObjectNode = objectMapper.createObjectNode();// 模型名称modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, modelName);// 模型版本modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);// 模型详情modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, modelDescription);// 以字符串信息存储进信息属性中modelData.setMetaInfo(modelObjectNode.toString());// 模型名称modelData.setName(modelName);// 模型keymodelData.setKey(modelKey);// 初始化json数据ObjectNode editorNode = objectMapper.createObjectNode();editorNode.put("id", "canvas");editorNode.put("resourceId", "canvas");// 创建一个stencilset节点ObjectNode stencilSetNode = objectMapper.createObjectNode();stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");editorNode.set("stencilset", stencilSetNode);// 添加模型repositoryService.saveModel(modelData);repositoryService.addModelEditorSource(modelData.getId(),editorNode.toString().getBytes(StandardCharsets.UTF_8));response.sendRedirect("/static/activiti/modeler.html?modelId=" + modelData.getId());}}}
-
ModelSaveRestResource
注意:此方法,请求方式修改为POST
package cn.git.workflow.modeler;import cn.git.common.exception.ServiceException; import cn.git.common.result.Result; import cn.git.common.util.LogUtil; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.activiti.editor.constants.ModelDataJsonConstants; import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.Model; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*;import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets;/** * @description: 控制器类用于保存activiti模型* @program: bank-credit-sy* @author: lixuchun* @create: 2024-11-21*/ @Api(value = "model流程设计器编辑保存", tags = "model流程设计器编辑保存") @RestController public class ModelSaveRestResource implements ModelDataJsonConstants {protected static final Logger LOGGER = LoggerFactory.getLogger(ModelSaveRestResource.class);@Autowiredprivate RepositoryService repositoryService;/*** Jackson ObjectMapper*/@Autowiredprivate ObjectMapper objectMapper;/*** 模型修改,通过jsno格式保存信息** @param modelId 模型 ID* @param name 模型名称* @param description 模型描述* @param json_xml JSON 格式的模型数据* @param svg_xml SVG 格式的模型数据*/@ApiOperation(value = "模型修改,通过jsno格式保存信息", notes = "模型修改,通过jsno格式保存信息")@RequestMapping(value = "/model/{modelId}/save", method = RequestMethod.POST)@ResponseStatus(value = HttpStatus.OK)public Result<String> saveModel(@PathVariable String modelId, String name, String description, String json_xml, String svg_xml) {// 创建一个 ByteArrayOutputStream 对象,用于存储转换后的 PNG 图像try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {// 获取指定 ID 的模型Model model = repositoryService.getModel(modelId);// 将模型的元数据转换为 JSON 对象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);// 将 JSON 格式的模型数据保存到模型编辑源repositoryService.addModelEditorSource(model.getId(), json_xml.getBytes(StandardCharsets.UTF_8));// 将 SVG 格式的模型数据转换为 PNG 图像InputStream svgStream = new ByteArrayInputStream(svg_xml.getBytes(StandardCharsets.UTF_8));TranscoderInput input = new TranscoderInput(svgStream);PNGTranscoder transcoder = new PNGTranscoder();// 设置输出流TranscoderOutput output = new TranscoderOutput(outStream);// 执行转换transcoder.transcode(input, output);final byte[] result = outStream.toByteArray();// 将转换后的 PNG 图像保存到模型编辑源额外数据repositoryService.addModelEditorSourceExtra(model.getId(), result);return Result.ok("保存成功!");} catch (Exception e) {throw new ServiceException(StrUtil.format("模型保存失败,具体失败原因为[{}]", LogUtil.getStackTraceInfo(e)));}} }
-
StencilsetRestResource
package cn.git.workflow.modeler;import java.io.InputStream;import com.alibaba.fastjson.JSONObject; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.activiti.engine.ActivitiException; import org.apache.commons.io.IOUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;/*** @description: 在线模板中文转义文件获取* @program: bank-credit-sy* @author: lixuchun* @create: 2024-11-20*/ @Api(value = "model流程设计器加载中文模板文件", tags = "model流程设计器加载中文模板文件") @RestController public class StencilsetRestResource {/*** 在线编辑器中文模板文件*/public static final String EDITOR_CH_FILE = "stencilset.json";/*** 获取在线编辑器中文模板文件** @return*/@ApiOperation(value = "加载stencilset.json中文文件", notes = "加载stencilset.json中文文件")@RequestMapping(value = "/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")public @ResponseBody Object getStencilset() {InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(EDITOR_CH_FILE);try {assert inputStream != null;return JSONObject.parseObject(IOUtils.toString(inputStream, "utf-8"));} catch (Exception e) {throw new ActivitiException("获取流程在线编辑器中文模板失败!", e);}} }
我们的项目是springboot前后端分离项目,我们整合的流程在线编辑器与前端项目需要进行 iframe
嵌套,所以有跨域问题。需要开放允许跨域。具体代码如下:
- GlobalCrossConfig
package cn.git.workflow.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter;/*** 通用跨域设置,允许跨域访问本项目* @program: bank-credit-sy* @author: lixuchun* @create: 2021-06-01*/ @Configuration public class GlobalCrossConfig {/*** 设置跨域配置信息*/@Beanpublic CorsFilter corsFilter() {CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*");corsConfiguration.setAllowCredentials(true);corsConfiguration.addAllowedMethod("*");corsConfiguration.addAllowedHeader("*");corsConfiguration.addExposedHeader("*");UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();configSource.registerCorsConfiguration("/**", corsConfiguration);return new CorsFilter(configSource);}}
除此之外,还需要开启允许进行iframe嵌套,需要禁用X-Frame-Options
头。X-Frame-Options
是一个HTTP响应头,用于控制浏览器是否允许在一个 <frame>
, <iframe>
, 或 <object>
中加载页面。默认情况下,这个头会设置为 DENY 或 SAMEORIGIN,以防止点击劫持(Clickjacking)攻击。
调用 disable() 方法会禁用 X-Frame-Options 头,即不在响应中发送这个头。这样,页面可以在任何域的 <frame>
或 <iframe>
中加载,具体操作代码如下
- SecurityConfig
package cn.git.workflow.config;import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/*** @description: 禁用activiti-rest引入后的security主动校验功能* @program: bank-credit-sy* @author: lixuchun* @create: 2024-06-11*/ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 禁用activiti-rest引入后的security主动校验功能* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {// 关闭securityhttp.httpBasic().disable().csrf().disable()// 允许iframe嵌套.headers().frameOptions().disable();http.authorizeRequests().anyRequest().permitAll();} }
我们还需要设置静态资源信息,提供我们在线编辑器的再跳转功能,具体设置如下:
-
WebMvcConfig
package cn.git.workflow.config;import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @description: 添加fastjson转换器* @program: bank-credit-sy* @author: lixuchun* @create: 2024-11-21*/ @Configuration class WebMvcConfig implements WebMvcConfigurer {/*** 添加静态资源文件,外部可以直接访问地址** @param registry*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 添加外部静态资源registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");// 添加模板信息registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");} }
2.4 前端文件设置
具体替换文件 activiti-webexplore\Activiti-5.x\modules\activiti-webapp-explorer2\src\main\webapp
下的是哪个文件
以及 activiti-webexplore\Activiti-5.x\modules\activiti-webapp-explorer2\src\main\resource
目录下的文件标签文件
最终复制文件到项目中,目录格式如下
注意:我们也可以替换
stencilset.json
,将流程页面展示信息变更为中文提示信息,此为中文文件地址
修改 app-cfg.js
文件,将contextRoot 修改为空
var ACTIVITI = ACTIVITI || {};
ACTIVITI.CONFIG = {'contextRoot' : '',
};
修改 /editor-app/configuration/toolbar-default-actions.js
,update方法修改请求方式为POST,对应前面ModelSaveRestResource
请求方式修改为POST
3. 进行测试
测试分为两个部分,一个是流程服务自己测试,一个是使用iframe嵌套到前端项目中,进行测试
3.1 流程项目自己测试
启动服务,我们便可以进行简单测试了,访问 http://3.1.19.13:11203/modeler/save?modelName=测试啦999&modelKey=jackAoteMan&modelDescription=杰克奥特曼
进行新增
访问 http://3.1.19.13:11203/modeler/save?modelId=9fb167fa2434422697090a8ea56467e8
可以进行修改编辑
3.2 前端嵌入iframe测试
我们公司则是将其嵌入到了公司的前端项目中,我们通过公司前端进行访问,访问我们新增的流程model信息
点击修改按钮,进行简单修改
点击部署按钮,进行部署,之后此流程便可以正常使用了
除了部署修改,还有移除,下载xml文件等操作,此处便不多赘述,部署之后,可以在已部署目录中查看已部署信息,包含历史全部部署文件信息,可以分版本下载查看
本文参考此篇博文