如何整合Flowable-modeler到自己的项目中

几年没做工作流了,时过进迁,时不时忆起当年,还时那般激情热血。牢骚完,感慨闭,几年后的今天又要开始做自定义工作流的开发,今天首先分享一下我的第一步,整合Web流程设计器官方demo自带的modeler。这里要说几句题外话,本身原计划还时打算继续使用activiti的,想当年我还是基于5.13实现过自定义工作流,现在几年过去了怎么也该6.0了吧,结果不了解不知道一了解吓一跳,activiti核心成员全部出走以Activiti5.22为基础另外创立了Flowable,并且已经发布了6.0 releace,而Activiti这边的6.0依然还在beta版本。了解到这里自不用多讲,肯定选择Flowable咯。

当年我在5.13的时候整合modeler的时候,那可是相当的简单啊,整个过程也就几十分钟的事儿,从官方demo里扣出editor文件夹,然后给自己的系统添加rest框架支持同时添加modeler需要的几个rest路由就搞定。但是这次整合Flowable的modeler着实把我恶心了一把,首先官方demo就搞得无比复杂,使用了太多前沿技术,例如Spring security、angularJs、freemarker等,另外正因为大量使用freemarker导致页面极度碎片化,对于新人研究demo代码带了极大的困难。当然,这些还难不倒我。

开始正题

第一步

观察官方demo,发现现在的demo分成了多个项目,其中关键的就是flowable-idm、flowable-modeler,实际使用发现idm主要负责人员和权限管理,modeler里包含的流程模型管理、app管理和form管理,其中app管理是一个新的概念,实际意义其实就是对流程模型进行了一个分类,和流程模型属于一对多的关系。
大致关系明白之后,开始阅读源码,过程有多坑爹就不多说了,没注释,页面极度碎片化,反正挺痛苦,最后总结是modeler项目可独立使用,可配置jdbc访问指定数据库,但是因为使用了Spring security权限框架做了统一的权限管理和单点登录,故而想要不做修改直接放入自己项目则必须同时使用idm和modeler两个项目,而且idm里面使用的还是ACT_ID_*系列表,无法使用自己系统的用户体系。那么我们可以得出一个结论,要么修改idm让其使用我们自己的用户体系,要么修改modeler取消使用Spring security,不依赖idm,从而使用我们的用户体系。权衡利弊后,我选择了后者。

第二步

确定方案后,提取modeler的源码,使用IDE导入maven项目,开始寻找修改的方案。
首先找到web.xml ,发现如下配置。

1
2
3
<listener>
<listener-class>org.flowable.app.servlet.WebConfigurer</listener-class>
</listener>

进入这个类后,又发现如下代码。

1
2
3
4
5
6
7
8
9
/**
* Initializes Spring Security.
*/
private void initSpringSecurity(ServletContext servletContext, EnumSet<DispatcherType> disps) {
log.debug("Registering Spring Security Filter");
FilterRegistration.Dynamic springSecurityFilter = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy());

springSecurityFilter.addMappingForUrlPatterns(disps, false, "/*");
}

毫无疑问,就是通过这里添加了Spring Security的拦截器,拦截所有请求,进行的登录验证,遂将其注释之,重新运行测试,果然跳过登录验证。但是又发现顶部导航条没有显示出来,打开控制发现:

1
2
3
4
Request URL:http://localhost:8080/flowable-modeler/app/rest/account
Request Method:GET
Status Code:404 Not Found
Remote Address:[::1]:8080

翻找源码后发现,是app.js里293行发出的这个请求。

1
2
3
4
5
6
$http.get(FLOWABLE.CONFIG.contextRoot + '/app/rest/account')
.success(function (data, status, headers, config) {
$rootScope.account = data;
$rootScope.invalidCredentials = false;
$rootScope.authenticated = true;
});

追踪到java里发现这个请求实际上就是找Spring Security获取当前登录者的信息。好嘛,那我们继续注视掉这个请求,直接把success里面的代码复制出来,给$rootScope.account写死一个json。

1
2
3
$rootScope.account = {id:1, firstName:"xxx", lastName:"xxx", email:"xxx", fullName:"xxx"};
$rootScope.invalidCredentials = false;
$rootScope.authenticated = true;

再测试发现一切正常了,不过右上角显示的当前登录人的名称变成了xxx。好嘛,原来上面这个请求获取用户的意义就在这里。那好办了,改写上面的请求,把url换成我们的,把我们的用户重新封装成Modeler里的User类型,再返回即可搞定。

继续往下测试,创建个模型试试。呵呵,空指针。。。查看后发现是ModelService里的createModel方法报出的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Model createModel(ModelRepresentation model, String editorJson, User createdBy) {
Model newModel = new Model();
newModel.setVersion(1);
newModel.setName(model.getName());
newModel.setKey(model.getKey());
newModel.setModelType(model.getModelType());
newModel.setCreated(Calendar.getInstance().getTime());
newModel.setCreatedBy(createdBy.getId());
newModel.setDescription(model.getDescription());
newModel.setModelEditorJson(editorJson);
newModel.setLastUpdated(Calendar.getInstance().getTime());
newModel.setLastUpdatedBy(createdBy.getId());

persistModel(newModel);
return newModel;
}

调试发现传入参数createdBy拿到的是个null,继续往上查找:

1
2
3
4
5
6
7
8
protected ModelRepresentation createNewModel(String name, String description, Integer modelType, String editorJson) {
ModelRepresentation model = new ModelRepresentation();
model.setName(name);
model.setDescription(description);
model.setModelType(modelType);
Model newModel = modelService.createModel(model, editorJson, SecurityUtils.getCurrentUserObject());
return new ModelRepresentation(newModel);
}

发现createBy来源于SecurityUtils.getCurrentUserObject(),好嘛,我们都绕过了登录验证没有登录,这里当然是空了,而且我们并不打算使用Spring security,那么我们就需要从我们自己的单点登录中获取用户即可,为了我们能够继续往下验证先手动new一个User放进去,再次测试,发现成功。继续测试后续其他功能,可以发现全部OK。

当测试到流程图复制功能时,出现了问题,复制成功后,modeler上看不到复制出来的流程图,但是数据库ACT_DE_MODEL表中却实实在在多了一条数据,为什么显示不出来呢,对比发现复制出来的新数据中model_type字段为null,正常的则为0,手动修改后刷新页面,果然就正常了,难道是官方的一个BUG?没有继续深究了,先解决它,找到请求发送的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$scope.ok = function () {

if (!$scope.model.process.name || $scope.model.process.name.length == 0 ||
!$scope.model.process.key || $scope.model.process.key.length == 0) {

return;
}

$scope.model.loading = true;

$http({method: 'POST', url: FLOWABLE.CONFIG.contextRoot + '/app/rest/models/'+$scope.model.process.id+'/clone', data: $scope.model.process}).
success(function(data) {
$scope.$hide();

$scope.model.loading = false;
$rootScope.editorHistory = [];
$location.path("/editor/" + data.id);
}).
error(function() {
$scope.model.loading = false;
$modal.$hide();
});
};

ajax提交的数据部分写的是data: $scope.model.process,那么在$scope.model.loading = true;后追加一句$scope.model.process.modelType = 0;强制把modelType设置为0。重新测试发现成功。至于$scope.model.process.modelType为什么会是null,到底是不是BUG,有兴趣的同学可自行往下研究。

第三步

开发接入自己的Session同步或者单点登录,加上自己的登录拦截,即可打工搞成。

首先在ApplicationConfiguration中配置拦截器的bean,因为modeler没有使用xml的配置方式,所以要在ApplicationConfiguration类中加入java代码来配置bean

1
2
3
4
@Bean
public static SessionFilter sessionFilter(){
return new SessionFilter();
}

SessionFilter就是一个普通的实现javax.servlet.Filter接口的拦截器java类

然后在WebConfigurer中新增一个自己的初始化方法,写入以下代码:

1
2
FilterRegistration.Dynamic sessionFilter = servletContext.addFilter("sessionFilter", new DelegatingFilterProxy());
sessionFilter.addMappingForUrlPatterns(disps, false, "/*");

这里为什么使用new DelegatingFilterProxy(),而不是直接new SessionFilter()呢?
因为这样写,在SessionFilter中才能够使用Spring的上下文环境。而直接new SessionFilter(),则没有被Spring管理,则无法使用Spring的上下文环境,不能使用Spring的上下文环境最直接的影响自然就是不能注入bean,那么你的service在里面就无法使用。

第三步中提到了所有的SecurityUtils.getCurrentUserObject()都要替换成我们的单点登录获取用户的代码。如果需要用到session的话,直接使用

1
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();

这句代码是无法获取到session的,直接空指针异常,原因是Modeler本身并没有加入RequestContextListener这个listener,解决办法自然是我们帮它加入。为了和整个modeler代码保持统一的编码风格,我们在web.xml中加入自然不好,于是我们还是找到WebConfigurer类,在initSpring方法最后加入一句代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Initializes Spring and Spring MVC.
*/
private void initSpring(ServletContext servletContext, AnnotationConfigWebApplicationContext rootContext) {
log.debug("Configuring Spring Web application context");
AnnotationConfigWebApplicationContext appDispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
appDispatcherServletConfiguration.setParent(rootContext);
appDispatcherServletConfiguration.register(AppDispatcherServletConfiguration.class);

log.debug("Registering Spring MVC Servlet");
ServletRegistration.Dynamic appDispatcherServlet = servletContext.addServlet("appDispatcher", new DispatcherServlet(appDispatcherServletConfiguration));
appDispatcherServlet.addMapping("/app/*");
appDispatcherServlet.setLoadOnStartup(1);

servletContext.addListener(RequestContextListener.class);

}

自此,modeler整合全部完成。Thanks for your reading。

文章目录
  1. 1. 开始正题
    1. 1.1. 第一步
    2. 1.2. 第二步
    3. 1.3. 第三步
,