===========记下怕忘了,作个归档,内容会慢慢增加==============:
1、 条件判断节点对应的函数名quotationMain_workflow_1252923340932称一般格式为:Node_NULL_随机数字
2、 一旦流程中的全局变量为null的话,则这个值将不再被存储到Map中,也就是说参数中将再找不到此参数了,
所以在任何时候给参数的赋值,如果出现null值的话,最好改为""(很重要,一旦某个全局参数被设置为null了,
并且在前台输入参数中再没有设置的话,这个值就再也不会出现在Map中了)
3、 在实际生产系统中,一定要采用数据源的数据库连接模式,不能采用直连方式。在直连方式下,连接经常不释放,尤其是在打开studio时,后台连接增长的很快,数据库连接进程很快就被消耗完,原因待分析!
4、joinwork目前默认注册了4大监听器,包括 :
a、net.joinwork.bpm.engine.execute.ExecuteSubProcess 子流程执行监听器
b、net.joinwork.bpm.task.Dispatcher 工作任务分发器(创建工作任务)
c、net.joinwork.bpm.engine.timer.TimerManager 定时管理器(QuartZ)
d、net.joinwork.bpm.engine.message.MessageListener 消息监听器
5、 在Dispatcher(工作任务分发监听器)中,在自动指派(分配)工作任务时,如果后选的人(工作任务候选人)多于要求的人(=工作任务的数量),则随机选取(注意:是随机选取,而不是顺序选取)
6、在流程定义中,如果指定的参与者是由变量指定的,则可以同时指定多个不定长的用户,用 “;”隔开,用变量构造多个用户的格式为:
userid1;userid2 ;userid3 ;userid4
这样的话,动态指派多个用户就可以不用使用子流程了!
具体的处理流程可以参考:Dispatcher中的“autoAssign ”==>“getUserList ”方法
另外,还有很重要的一点就是:如果参与者使用变量指定的,一定要注意“类型”处室“角色”,还是“个人”,如果你的变量指向的是用户,但“类型”处又是角色的话,系统会按角色来处理这个变量的,这样就会产生错误了,同样的道理,如果变量是指定的角色,但“类型”确设置为“个人”的话,也会导致错误!
7、 如果节点的处理人是静态指定的(静态指定了部门、角色、用户;而不是通过变量来生成得到的),则在第一次调用Dispatcher 的“getUserList ”方法得到候选人的列表后,会把这些人的列表都存储到(缓存中)本节点定义的“candidateList ”(候选人)变量中的,这样的话,下次再调用查找节点人员的时候,就不用再重新计算了,可以提高效率。但如果节点候选人是通过变量来获得的话,则不存。
8、我们在查看joinwork的后台数据库表的时候,经常会发现,run_case等表的主键是数值型的,但编号经常不连续,为什么呢? 这实际上和joinwork的主键生成获取机制有关系。为了跨库的通用性,joinwork的主键都为数值型,而且采用的是“assign”手工指定的方式,所有的主键都是通过TableKey来获得主键的。 获得主键并不是每次都从后台数据库中取,而是从数据库中取一次,然后将数据库中的值加上步长KEY_SKIP(目前默认是200)存储起来,例如,当前数据库中的值为100,则 取出100作为当前主键值,再将数据库中的值变更为(100+KEY_SKIP)=300,这样,下次再取本表的主键值时,再不从数据库中取了,而是从缓存中取,就这样101、102、103...的取下去,一直取到200的时候,再从数据库中取一遍!这样取200个主键值只需要1次数据库查询,大大提高了效率!
但也造成了另外一个问题,就是由可能导致主键值的不连续!如果中途停机,下次再构建主键的时候就是从数据库中取新的值了,比如当取到130的时候,停机重启应用了,下次取的数据库中的主键值是300,则131~299的值就不可用了!
9、 joinwork在XML配置文件加载的设计中存在严重性能瓶颈(设计失误?) joinwork的配置文件基本上都采取xml文件的方式,加载时,除了流程定义模板外,其它的xml配置文件的读写都是采用的xstream,基本模式是,用BufferedReader将xml文件一行一行的读成一个String,并利用这个string来进行xstream的映射!这中间,最大的瓶颈就在于读的方式,它目前采用的是拼字符串,在文件比较小的情况下是看不出来区别的,但文件一旦大了(尤其是DataType 文件),这种拼字符串的方式在每读取一行的时候就会构造一个String的对象,一旦文件上万行,就要构造上万个String对象,严重影响效率。在我们实际应用中,数据组件一般会很多,DataType 文件很容易就上几万行,这时,尤其是在系统启动实例化流程引擎的时候,读DataType 文件就成了最大的性能损耗了!我们在一台旧一些的机器上部署的应用,系统启动加载就加载了10几分钟。而经过优化(采用StringBuffer)的代码,启动不到30秒!
10、joinwork中为了并发性能的提升,大量使用了缓冲技术,包括流程模板缓存,组织机构缓存等等! 目前在组织机构管理中,对group和user等对象的增、删、改都要及时通知到对应的缓存,否则会导致缓冲和永久存储中的不一致,目前采用的是注册插件,通知插件的方式来进行的消息的传递!
PartyManager(组织机构管理器)和ProcessPool(运行中流程模板池)目前都实现了OrgChangeListner 的onChanged 方法,当组织机构发生变化的时候,他们的onChanged 方法都会被调用。PartyManager 会将其针对整个域的组织机构的缓存清空,并在下一次访问的时候重新构建整个组织机构模型;ProcessPool 则会遍历其中的所有流程模板中的所有节点,并调用setCandidateList (null)和setAcl (null)方法(参考6),将所有的候选人全部清空,这样,在下一次在调用本流程节点的时候,就会从新从数据库中查找(计算)候选人,以保证和永久存储的一致!
另外,在实际应用中,一般业务系统在进行人员管理的时候,都要通知joinwork进行缓存同步,下面给出了一段通用方法:
try {
|
11、joinwork_console应用(流程管理控制台)登录的时候,目前默认使用的账号是admin,密码为空,但在正式系统上这样肯定不行的,如何修改密码呢? 其实很简单,手工用ultraedit打开engine_home目录,修改engine.config文件,在“<password></password>”中将你的密码添加进去,并重启应用,这样,再登录的时候就可以采用新的密码了!另外,千万不要修改管理员的账号,也就是“<admin>admin</admin>”中的admin的值,否则,你就等着哭吧!目前系统很多地方默认要采用admin这个账号,并且硬编码在代码中了!
另外,如果你觉得明文密码不安全,可以自己修改代码,将代码在比对之前进行MD加密,这样,在engine.config文件中添加的就是加密后的密码了,这样相对安全一些!
12、 在系统初始化或修改流程保存的时候,在读取流程定义文件并进行实例化后,系统都会调用ProcessDefManagerFileImpl 类中的setProcessInDevReady 方法,其主要进行如下两个操作:
a、检查流程中所有的状态节点,如果节点是定时启动的,则给它创建定时任务(通过调用TimerManager.getTimerManager().addProcess(domain, newProcess) 来实现)。
b、检查流程中的所有状态节点,如果节点是消息启动的,则把它添加到消息消费者的队列中(存到数据库中)
13、joinwork的消息机制: 在流程引擎启动初始化的时候,在调用MessageQueueManager(消息队列管理器)的init初始化方法的时候,会通过调用TimerManager.getTimerManager().startMessageJob()来实例化Quartz定时对象,并在30秒后启动针对【消息】处理的定时任务JobDetail job = new JobDetail(MSG_JOB_NAME, MESSAGE_GROUP,MessageWorker .class );从这里我们可以看到,MessageWorker就是 定时处理消息的任务类了,net.joinwork.bpm.engine.message.MessageWorker.class 的主要任务就是从数据库中查出所有的消息消费者,所有的未被完全消费的消息,在逐个配对进行匹配,如果消息的触发条件和消息消费者一致,则运行消息消费者中对应的方法!下面给出一个使用消息的例子:
需要注意的是,在消息中,目前系统已经定义死了两个变量,一个是“text”,一般用英语消息触发条件的比对,另外一个是“custom”,这个一般用于封装数据,custom被声明为一个Object,所以可以用它来描述任何对象,如下例所示:
创建消息:
Map msgDataMap = new java.util.HashMap(); msgDataMap.put("text", "hello"); applyForm apply = new applyForm(); apply.reason = "some text"; msgDataMap.put("custom", apply); Message.sendMessage("message1", 1, 60, msgDataMap); |
消息比对(触发条件):
text.equals("hello") |
事件执行:
System.out.println(text); applyForm myApply = (applyForm) custom; System.out.println(myApply.reason); |
14、Joinwork漏洞 。由于实际使用中暴露的一些问题,通过工具所做的jvm监控中,我们发现了joinwork的两个可能导致内存泄露的bug。一个是jalopy 的使用,joinwork在初始化流程定义模板后或者重新加载保存后的流程模板的时候,在生成新的流程执行类的时候,会调用jalopy来进行代码文本的格式化,具体调用了JavaCodeFormat 中的format 方法,问题就出在这个方法中,这里通过
Jalopy jalopy = new Jalopy();
来创建一个jalopy的新实例,由于jalopy的构造函数中会创建一些静态内存,即使format方法执行完后,这些静态内容也无法被jvm 的回收器回收,这样就直接导致了内存的不断增加,直至最后OutOfMemory 了。
解决办法可以将jalopy 作为类的全局静态变量,这样就不用每次使用重新实例化了,全局只要保持一个jalopy的实例就可以了!代码如下:
:
... private static Jalopy jalopy = null; //新添加 |
15、 如果某个环节没有处理人,并且在“任务属性”中的“任务数量”中设置为“和符合条件的申请人一致”,则流程将自动finish这个Activity的环节,自动进行下一个环节!但如果静态设置了工作任务的数量N的话,则就算没有处理人,流程还是会生成N个工作任务,只是这N个工作任务会处于悬空的状态(没有执行人、负责人)!
16、 joinwork中,Process_running表中对应的都是流程实例中使用到的流程模板,以String对象的形式将流程模板定义的内容(xml)存储到processobject对象中,一旦其被第一次使用的时候,都会先被加载到一个内存中的一个Map中(runningProcessPool)
18、 替换一个流程实例的模板的时候,其实只是替换了流程实例表(case_run)中对应的流程实例记录中的processId,ProcessIndex,processName3个字段的信息;同时,基于插件的触发方法onCaseReplaceProcess来替换工作任务表(workitem_run)中本流程实例对应的所有工作项的processId,ProcessIndex,processName3个字段的信息。(但在这之前,系统会自动保证本流程模板在Process_running和对应的内存流程模板实例池(使用中模板)中有对应的记录和信息)。
全部替换的话,也是做的上面的操作!
19、 在使用joinwork的workdesk时又发现了一处bug:joinwork支持状态节点的跳转功能,在人工节点配置中,在节点属性配置页面“一般属性”页卡中,可以选择在本节点中需要跳转的节点。这样,在处理页面中,就会出来一个跳转到本状态节点的按钮!
在这种模式下使用,会发现,在点击跳转到状态节点的按钮时候,流程虽然能正常执行,也进行了正常的跳转作业,但在工作任务处理中的输入输出参数都不可用了!所有的输入都变成null。仔细检查代码,发现,在进行状态节点的跳转时,页面上有段代码:
frm.elements["nodeId"].value=statusId; //注意这里
frm.elements["workflow_act"].value="skip";
原来,joinwork在处理状态节点跳转的时候,直接将参数nodeId 置换为待跳转的状态节点的Id了。这种简单粗暴的做法虽然有效,但忽略了输入输出参数的处理。在后台的CaseWorker 的getDataFromRequest 方法中,进行输入输出参数的取值封装时,需要通过nodeId来获得本节点的参数列表的,但此时nodeId 已经被置换为状态节点 的Id了,而不是当前节点 的Id,所以这时候取的参数列表也就是待跳转状态节点的餐宿列表,而不是本节点的参数列表了,这是后取不到值就很正常了!
处理的办法有两种,一种是在待跳转的状态节点上也加上和当前节点一样的输入输出参数,不过这种方法治标不治本,当在多个人工节点都可以跳转的情况下,状态节点上的参数必须是多节点的合计,容易产生错误和混淆!
另外一种比较彻底的方法就是修改目前参数封装的方式,首先在页面上再加一个参数来表示当前节点:
<input type="hidden" name="curNodeId" value="<bean:write name='formdatas' property='nodeId' />"/>
然后在后台修改CaseWorker类,让它通过curNodeId来获得参数,而不是通过nodeId,这样就能够彻底解决这个问题了!
注意:如果同时指定了linkId和statusId,linkId优先。如果linkId和statusId都为null,执行缺省迁移.
20、 在workitem_run表中有2个字段batch 、itemnum ,存储的是一个数字,如果在一个流程节点上产生了多个工作任务,这些工作任务就会具有同样的batch号,流程引擎在执行的时候,就是根据这个batch 号来查找当前节点是否还有未执行的工作任务,itemnum 字段存储的是当前工作任务对应的流程节点实例对应了多少个工作任务项。
21、 如果是按比例完成的工作项,剩下不执行的工作项都会调用“工作任务取消”的事件(实际上执行够数量的工作项后,后续的工作项都是被取消的,所以自然要调用这个事件了)
22、 joinwork的另一个bug:如果在人工节点中设置了第一次或第二次时限消息,如果用户在规定时限内处理了工作任务,则找理说,时限消息事件(QuartZ任务)应该要被取消才对,但实际上joinwork并没有处理,也就是说,一旦时限消息被触发了,不管工作任务处理不处理,这个时限时间都会按时触发的。但是,如果流程被取消了,则其时限任务会被取消。不知道是joinwork故意这样处理呢?还是确实是疏忽了?
我个人觉得应该在工作任务被处理后,时限消息事件(定时任务)也应该被删除才对!如果要这么修改的话,可以参考TimerManager的onActivityRemove()事件来写一个
23、 joinwork目前采用BLOB字段来存储封装的相关数据,如果使用的是Oracle数据库的话,一定要非常小心Oracle驱动的版本问题,如果读写blob字段采用的是不同版本的classes12.jar文件的话,经常会出现EOF的问题(二进制流的结尾读取异常)。
24、 表case_run存储的是流程实例,其中流程运行中的相关参数都被封装到caseobject字段中了,这是一个blob字段,存储的是一个HashMap的java对象,它存储了如下几个参数:
<1>"status":存储的是流程中处于就绪的状态节点的队列.每个元素是一个String类型的文本,为状态节点的Id。
<2>"activity":存储的是流程中处于就绪的活动节点的队列.每个元素是一个ReadyNode对象,表示一个活动节点。
<3>"caseLog":存储的是流程实例的日志记录。每个元素是一个BPMCaseLog对象。
<4>"datamap":这个最重要了,是流程中定义的所有输入输出参数的值对。
<5>"subCase":子流程实例列表,如果没有子流程的话,为null。
<6>"subdatamap":存储的是节点变量数据