flowable流程设计器抽离,及源码改造原理分析

业务场景:进行流程开发的时候,经常需要流程设计器进行Bpmn.xml设计,flowable官方也只是提供了war下载,并且需要基于本身的用户权限,这对于已有系统的集成是不方便的,所以对于流程设计器的权限模块剥离就很有必要,本文描述了如何对流程设计器的剥离以及核心模块的功能分析。

环境
springboot:2.2.0.RELEASE
flowable:6.4.2

git地址:https://github.com/oldguys/flowable-modeler-demo.git
flowable 官方git地址:https://github.com/flowable/flowable-engine/releases
下一篇:《flowable流程设计器集成,整合mybatis-plus,从modeler 中 部署流程定义 》

本地启动后测试路径:http://localhost:8081/flowable-modeler/#/processes

01.png

流程设计器抽离demo

  1. pom文件
  2. 静态资源及配置文件
  3. 重写 flowable-modeler 默认的类加载
  4. 重写 spring-security 相关模块

Step 1:pom 文件

flowable-modeler 模块:从官方源码可以分析得出,基本的modeler模块的相关引用 此处 版本是 flowable 6.4.2
mysql数据源:默认是h2数据库,此处引用的是mysql数据库。

        <!-- flowable-modeler 核心 -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-conf</artifactId>
            <version>${flowable-version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-rest</artifactId>
            <version>${flowable-version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-ui-modeler-logic</artifactId>
            <version>${flowable-version}</version>
        </dependency>

        <!-- 替换数据源 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

Step 2: 静态资源及配置文件

  1. 复制相关的静态资源及配置文件
静态原件及配置文件

在源码中可以看到,这些文件为前端页面的静态页面,需要完成转移到剥离出来的新项目之中。

2.编写项目的自定义配置文件,及springboot本身项目的 application.yml 文件。

logging:
  level:
    org.flowable.ui.modeler.rest.app: debug
#    root: debug
server:
  port: 8081
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/flowable-modeler-demo?characterEncoding=UTF-8
    driver-class-name: com.mysql.jdbc.Driver

PS: 由于springboot 项目的配置文件是逐层覆盖的,最外层项目的变量 可以直接覆盖掉原始的配置变量,所以可以直接在此处 配置数据源遍可以替换掉内部的默认数据源变量。

Step 3:重写 flowable-modeler 默认的类加载

源码 中的默认类加载

从源码中看到入口为2个类,本处需要对org.flowable.ui.modeler.conf.ApplicationConfiguration 进行相应的改造。
剔除不需要的相关依赖,添加缺少的依赖,改造结果如下:

  1. 抽出自定义需要的全局配置类
package com.example.oldguy.modules.modeler.configurations;

import org.flowable.ui.modeler.conf.DatabaseConfiguration;
import org.flowable.ui.modeler.properties.FlowableModelerAppProperties;
import org.flowable.ui.modeler.servlet.ApiDispatcherServletConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;


@Import(
        value = {
                DatabaseConfiguration.class,
        }
)
@Configuration
@EnableConfigurationProperties(FlowableModelerAppProperties.class)
@ComponentScan(basePackages = {
        "org.flowable.ui.modeler.repository",
        "org.flowable.ui.modeler.service",
        "org.flowable.ui.common.repository",
        "org.flowable.ui.common.tenant",

        "org.flowable.ui.modeler.rest.app",
        "org.flowable.ui.modeler.rest.api"
    }
)
public class MyAppConfiguration {

    @Bean
    public ServletRegistrationBean modelerApiServlet(ApplicationContext applicationContext) {
        AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
        dispatcherServletConfiguration.setParent(applicationContext);
        dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class);
        DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration);
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/api/*");
        registrationBean.setName("Flowable Modeler App API Servlet");
        registrationBean.setLoadOnStartup(1);
        registrationBean.setAsyncSupported(true);
        return registrationBean;
    }

}

PS: org.flowable.ui.modeler.conf.DatabaseConfiguration 为flowable源码中的mybatis配置,需要引入。

  1. 重写类 org.flowable.ui.common.rest.idm.remote.RemoteAccountResource,这个类是流程设计器获取用户相关信息的接口,此处的处理方式是,在新建项目中编写相同的restful接口覆盖,然后不加载原始的类。
package com.example.oldguy.modules.modeler.controllers;

import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.model.UserRepresentation;
import org.flowable.ui.common.security.FlowableAppUser;
import org.flowable.ui.common.security.SecurityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName: RemoteAccountResource
 * @Author: ren
 * @Description:
 * @CreateTIme: 2020/1/25 0025 下午 10:37
 **/
@RestController
public class RemoteAccountResource {

    @GetMapping("app/rest/account")
    public UserRepresentation getAccount() {

        FlowableAppUser appUser = SecurityUtils.getCurrentFlowableAppUser();
        UserRepresentation userRepresentation = new UserRepresentation(appUser.getUserObject());
        if (appUser.getUserObject() instanceof RemoteUser) {
            RemoteUser temp = (RemoteUser) appUser.getUserObject();
            userRepresentation.setPrivileges(temp.getPrivileges());
        }
        return userRepresentation;
    }


}

  1. 重写 org.flowable.ui.common.service.idm.RemoteIdmService ,此处在 flowable-ui-modeler-rest 模块中多次依赖注入,本处项目暂时没有使用到,所以这里采用重写空实现。
package com.example.oldguy.modules.modeler.services;

import org.flowable.ui.common.model.RemoteGroup;
import org.flowable.ui.common.model.RemoteToken;
import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.service.idm.RemoteIdmService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @ClassName: MyRemoteServiceImpl
 * @Author: ren
 * @Description:
 * @CreateTIme: 2020/1/25 0025 下午 11:27
 **/
@Service
public class MyRemoteServiceImpl implements RemoteIdmService {

    private Logger LOGGER = LoggerFactory.getLogger(MyRemoteServiceImpl.class);

    @Override
    public RemoteUser authenticateUser(String username, String password) {
        LOGGER.debug("MyRemoteServiceImpl:authenticateUser");
        return null;
    }

    @Override
    public RemoteToken getToken(String tokenValue) {
        LOGGER.debug("MyRemoteServiceImpl:getToken");
        return null;
    }

    @Override
    public RemoteUser getUser(String userId) {
        LOGGER.debug("MyRemoteServiceImpl:getUser");
        return null;
    }

    @Override
    public List<RemoteUser> findUsersByNameFilter(String filter) {
        LOGGER.debug("MyRemoteServiceImpl:findUsersByNameFilter");
        return null;
    }

    @Override
    public List<RemoteUser> findUsersByGroup(String groupId) {
        LOGGER.debug("MyRemoteServiceImpl:findUsersByGroup");
        return null;
    }

    @Override
    public RemoteGroup getGroup(String groupId) {
        LOGGER.debug("MyRemoteServiceImpl:getGroup");
        return null;
    }

    @Override
    public List<RemoteGroup> findGroupsByNameFilter(String filter) {
        LOGGER.debug("MyRemoteServiceImpl:findGroupsByNameFilter");
        return null;
    }
}

Step 4:重写 spring-security 相关模块

在源码中 org.flowable.ui.modeler.conf.SecurityConfiguration 为流程设计器的权限核心控制,所以需要 进行改造。本处直接基于spring-security 的运行机制重写相关的实现类,不引用原始配置的相关信息

  1. 自定义认证过滤器:com.example.oldguy.modules.modeler.security.MyFilter
package com.example.oldguy.modules.modeler.security;

import org.flowable.ui.common.security.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ClassName: MyFilter
 * @Author: huangrenhao
 * @Description:
 * @CreateTime: 2020/1/19 0019 下午 4:04
 * @Version:
 **/
@Component
@WebFilter(urlPatterns = {"/app/**", "/api/**"})
public class MyFilter extends OncePerRequestFilter {

    private Logger LOGGER = LoggerFactory.getLogger(MyFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if (skipAuthenticationCheck(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        LOGGER.debug("MyFilter:doFilterInternal:" + request.getRequestURL());

        if (StringUtils.isEmpty(SecurityUtils.getCurrentUserId())) {

            LOGGER.debug("MyFilter:doFilterInternal:校验......");
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("admin", "");
            SecurityContextHolder.getContext().setAuthentication(token);

        } else {
            LOGGER.debug("MyFilter:doFilterInternal:校验通过.......");
        }

        filterChain.doFilter(request, response);
    }

    protected boolean skipAuthenticationCheck(HttpServletRequest request) {
        return request.getRequestURI().endsWith(".css") ||
                request.getRequestURI().endsWith(".js") ||
                request.getRequestURI().endsWith(".html") ||
                request.getRequestURI().endsWith(".map") ||
                request.getRequestURI().endsWith(".woff") ||
                request.getRequestURI().endsWith(".png") ||
                request.getRequestURI().endsWith(".jpg") ||
                request.getRequestURI().endsWith(".jpeg") ||
                request.getRequestURI().endsWith(".tif") ||
                request.getRequestURI().endsWith(".tiff");
    }

}

PS: 经过试验,过滤器必须继承于 org.springframework.web.filter.OncePerRequestFilter 而不能直接实现 javax.servlet.Filter ,不然就算注入到 spring-security容器中,也不能触发本身的权限校验,具体原理还有待研究。此处参考源码中的 org.flowable.ui.common.filter.FlowableCookieFilter进行改造

  1. 编写核心的校验类 com.example.oldguy.modules.modeler.security.MyUserDetailsService
package com.example.oldguy.modules.modeler.security;

import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.security.FlowableAppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

import static com.example.oldguy.modules.modeler.constants.FlowableConstants.FLOW_ABLE_MODELER_ROLES;

/**
 * @ClassName: MyUserDetailsService
 * @Author: huangrenhao
 * @Description:
 * @CreateTime: 2020/1/17 0017 下午 4:37
 * @Version:
 **/
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger LOGGER = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LOGGER.debug("MyUserDetailsService:loadUserByUsername:认证权限.....");
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();

        // 配置 flowable-modeler 权限
        FLOW_ABLE_MODELER_ROLES.parallelStream().forEach(obj -> {
            authorities.add(new SimpleGrantedAuthority(obj));
        });

        RemoteUser sourceUser = new RemoteUser();
        sourceUser.setFirstName("admin");
        sourceUser.setDisplayName("测试中文");
        sourceUser.setPassword("123456");
        sourceUser.setPrivileges(new ArrayList<>(FLOW_ABLE_MODELER_ROLES));
        sourceUser.setId("123456");
        FlowableAppUser user = new FlowableAppUser(sourceUser, "admin", authorities);
        return user;
    }
}

  1. 编写密码编译器 com.example.oldguy.modules.modeler.security.MyPasswordEncoder
package com.example.oldguy.modules.modeler.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @ClassName: MyPasswordEncoder
 * @Author: ren
 * @Description:
 * @CreateTIme: 2020/1/25 0025 下午 7:11
 **/
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    private Logger LOGGER = LoggerFactory.getLogger(MyPasswordEncoder.class);

    @Override
    public String encode(CharSequence charSequence) {
        LOGGER.debug("MyPasswordEncoder:encode:" + charSequence);
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence frontPsw, String sourcePsw) {
        LOGGER.debug("MyPasswordEncoder:matches:" + frontPsw + "\t sourcePsw:" + sourcePsw);
        return true;
    }
}
  1. 编写spring-security 核心容器
package com.example.oldguy.modules.modeler.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;

/**
 * @ClassName: WebSecurityConfig
 * @Author: huangrenhao
 * @Description:
 * @CreateTime: 2020/1/19 0019 下午 3:16
 * @Version:
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyFilter myFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .addFilterAfter(myFilter, SecurityContextPersistenceFilter.class)
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and().csrf().disable();
    }

}

PS: csrf 要关掉,不然前端调用会被拦截。出现 Forbidden 一闪而过的情况。

以上就完成流程设计器抽离。


下面是对于其中的部分模块及原理的解析:

  1. org.flowable.ui.common.security.SecurityUtils 用户信息
  2. 模型设计器配置相应模块
  3. spring-security 过滤链
1. org.flowable.ui.common.security.SecurityUtils 用户信息
// org.flowable.ui.common.security.SecurityUtils
    public static FlowableAppUser getCurrentFlowableAppUser() {
        FlowableAppUser user = null;
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null && securityContext.getAuthentication() != null) {
            Object principal = securityContext.getAuthentication().getPrincipal();
            if (principal instanceof FlowableAppUser) {
                user = (FlowableAppUser) principal;
            }
        }
        return user;
    }
// com.example.oldguy.modules.modeler.security.MyUserDetailsService
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LOGGER.debug("MyUserDetailsService:loadUserByUsername:认证权限.....");
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();

        // 配置 flowable-modeler 权限
        FLOW_ABLE_MODELER_ROLES.parallelStream().forEach(obj -> {
            authorities.add(new SimpleGrantedAuthority(obj));
        });

        RemoteUser sourceUser = new RemoteUser();
        sourceUser.setFirstName("admin");
        sourceUser.setDisplayName("测试中文");
        sourceUser.setPassword("123456");
        sourceUser.setPrivileges(new ArrayList<>(FLOW_ABLE_MODELER_ROLES));
        sourceUser.setId("123456");
        FlowableAppUser user = new FlowableAppUser(sourceUser, "admin", authorities);
        return user;
    }
spring-security 官网的 SecurityContextHolder 描述

PS: 从官网中可以看到 用户是基于 ThreadLocal 的,所以是线程安全的,并且在 SecurityUtils 调用的时候 FlowableAppUser 才会返回,所以在重写 org.springframework.security.core.userdetails.UserDetailsService 实现类的时候,需要使用 FlowableAppUser 作为用户类。

另外,restful接口:http://localhost:8888/flowable-modeler/app/rest/account
默认返回值:

{
    "id": "admin",
    "firstName": "Test",
    "lastName": "Administrator",
    "email": "admin@flowable.org",
    "fullName": "Test Administrator",
    "tenantId": null,
    "groups": [],
    "privileges": ["access-idm", "access-rest-api", "access-task", "access-modeler", "access-admin"]
}

其中:["access-idm", "access-rest-api", "access-task", "access-modeler", "access-admin"]
分别对应流程设计器导航栏上面的各个功能,可以根据需要进行删减改造


image.png
2. 模型设计器配置相应模块

模型设计器中功能特别多,并不是所有功能都需要用到的,可以通过修改配置文件进行修改
资源位置:

stencilset_bpmn.json(流程设计器界面配置文件):D:\workspace\demo\flowable\flowable-engine-flowable-6.4.2-old\modules\flowable-ui-modeler\flowable-ui-modeler-logic\src\main\resources\stencilset_bpmn.json

StencilSetResource(流程设计器配置后端接口): org.flowable.ui.modeler.rest.app.StencilSetResource

zh-CN.json(可参考的汉化文件):static/i18n/zh-CN.json

配置文件的位置
配置接口位置
修改完配置结果
3. spring-security 过滤链

spring-security的过滤链 基于 org.springframework.security.config.annotation.web.builders.FilterComparator
debug的时候可以拿到数据(如下),所以配置过滤器的时候,必须优先于指定的节点,如LogoutFilter,如:CorsFilter=600,如果后于此过滤器,会出现没有权限,然后界面一闪而过。很难调试,所以需要通过过滤链顺序对于指定的权限拦截进行处理。

{
org.springframework.security.openid.OpenIDAuthenticationFilter=1600, 
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter=1300, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter=1800, 
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter=900, 
org.springframework.security.web.context.SecurityContextPersistenceFilter=400, 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor=3100, 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter=1700, 
org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter=1000, 
org.springframework.security.web.session.SessionManagementFilter=2900, 
org.springframework.security.web.authentication.logout.LogoutFilter=800, 
org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter=1100, 
org.springframework.security.web.jaasapi.JaasApiIntegrationFilter=2500, 
org.springframework.security.web.authentication.switchuser.SwitchUserFilter=3200, 
org.springframework.security.web.access.channel.ChannelProcessingFilter=100, 
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter=2600, 
org.springframework.security.web.session.ConcurrentSessionFilter=1900, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter=2200, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter=2700, 
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter=2100, 
org.springframework.security.web.csrf.CsrfFilter=700, 
org.springframework.security.cas.web.CasAuthenticationFilter=1200, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter=2400, 
org.springframework.security.web.access.ExceptionTranslationFilter=3000, 
org.springframework.security.web.authentication.www.DigestAuthenticationFilter=2000,
 org.springframework.web.filter.CorsFilter=600, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter=2300, 
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter=2800, 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter=1400,
org.springframework.security.web.header.HeaderWriterFilter=500, 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter=300
}

获取已保存的 bpmn.xml

源码:org.flowable.ui.modeler.rest.app.ModelBpmnResource
调用接口:127.0.0.1:8081/flowable-modeler/app/rest/models/9f545d3b-40ac-11ea-9aaf-283a4d3b99a3/bpmn20

效果图
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
    <process id="test" name="test" isExecutable="true">
        <startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
        <userTask id="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" name="测试" flowable:formFieldValidation="true"></userTask>
        <sequenceFlow id="sid-8E539945-3C77-4630-90D1-984D585F8AE9" sourceRef="startEvent1" targetRef="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E"></sequenceFlow>
        <endEvent id="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F"></endEvent>
        <sequenceFlow id="sid-64160218-43FF-4F5A-B51A-8C11213ACC8D" sourceRef="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" targetRef="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F"></sequenceFlow>
    </process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_test">
        <bpmndi:BPMNPlane bpmnElement="test" id="BPMNPlane_test">
            <bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
                <omgdc:Bounds height="30.0" width="30.0" x="100.0" y="163.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" id="BPMNShape_sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E">
                <omgdc:Bounds height="80.0" width="100.0" x="214.39999999999998" y="144.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F" id="BPMNShape_sid-C633FA15-0F2F-4739-B576-EE0C2072B40F">
                <omgdc:Bounds height="28.0" width="28.0" x="407.4" y="170.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNEdge bpmnElement="sid-8E539945-3C77-4630-90D1-984D585F8AE9" id="BPMNEdge_sid-8E539945-3C77-4630-90D1-984D585F8AE9">
                <omgdi:waypoint x="129.93799288040108" y="178.5999272073778"></omgdi:waypoint>
                <omgdi:waypoint x="214.39999999999998" y="181.99196787148597"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="sid-64160218-43FF-4F5A-B51A-8C11213ACC8D" id="BPMNEdge_sid-64160218-43FF-4F5A-B51A-8C11213ACC8D">
                <omgdi:waypoint x="314.35" y="184.0"></omgdi:waypoint>
                <omgdi:waypoint x="407.4" y="184.0"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
        </bpmndi:BPMNPlane>
    </bpmndi:BPMNDiagram>
</definitions>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容