Liferay7 BPM门户开发之9: 流程表单数据动态映射体系
设计目的:
每个流程表单涉及不同的表单变量。比如请假流程有3个任务节点,分别是
- Task1:开始流程,填写请假人、请假原因、请假开始时间、请假结束时间;
- Task2:上级审批,填写是否同意,审批意见;
- Task3:HR审批,填写是否同意,审批意见;
这里不处理消假;
那么各任务周期的变量分别是:
- Task1:initiator、reason、startDatetime、endDatetime;
- Task2: isApprove、remark;
- Task3:isApprove、remark;
在开发中,针对请假流程表单对应的流程变量数据,我们可以定一个独立的VO实体类、独立的DAO数据操作、定义独立的表,比如叫Leave Table,然后JPA持久化流程表单实例数据,通过这样的方式定义一个portlet,实现Liferay 和Activiti的业务集成。
持久化的过程类似这样:
TaskService taskService = …… Leave leave = new Leave (); leave.setId(20); leave.setInitiator ("张三"); leave.setReason ("想请假休息2天"); …… taskService.setVariable(taskId, "Leave1.1", leave);
那么又有一个新流程,叫报销流程,有三个任务:
- Task1:开始流程,填写请款人、请款原因、请款金额;
- Task2:上级审批,填写是否同意,审批意见;
- Task3:HR审批,填写是否同意,审批意见;
那么各任务周期的变量分别是:
- Task1:initiator、reason、amount;
- Task2: isApprove、remark;
- Task3:isApprove、remark;
在传统开发中,又要针对请款流程表单对应的变量数据,又要定一个独立的VO实体类、独立的DAO数据操作、定义独立的表,比如叫BorrowMoneyTable,然后JPA持久化流程表单实例数据。通过这样的方式定义又一个portlet,实现Liferay 和Activiti的业务集成。
…… 循环往复,生生不息。
这种做法比较传统,也没有风险,但存在效率问题,需要不断地新建portlet,以及VO、DAO…;
实际上,Activiti有完整的变量记录,提供了4种历史级别:
- none: 不保存任何历史记录,可以提高系统性能;
- activity:保存所有的流程实例、任务、活动信息;
- audit:也是Activiti的默认级别,保存所有的流程实例、任务、活动、表单属性;
- full: 最完整的历史记录,除了包含audit级别的信息之外还能保存详细,例如:流程变量。
如果要得到完整历史记录,只需要修改配置:
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="history" value="full"> </property></bean>
数据存在两个表中:
- act_ru_variable (运行时暂存)
- act_hi_varinst (历史数据)
查询:
List<historicvariableinstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstanceId).list(); for (HistoricVariableInstance variableInstance : list) { System.out.println("variable: " + variable.getVariableName() + " = " + variable.getValue()); }
表单字段存在另一个表:ACT_HI_DETAIL
读取表单字段
List<historicdetail> formProperties = historyService.createHistoricDetailQuery().processInstanceId(processInstance.getId()).formProperties().list(); for (HistoricDetail historicDetail : formProperties) { HistoricFormProperty field = (HistoricFormProperty) historicDetail; System.out.println("field id: " + field.getPropertyId() + ", value: " + field.getPropertyValue()); }
一个改良的设计方法(表单数据映射体系):
需要最简便的使用activiti:formProperty属性定义(即内置动态表单),这样可以在开始事件(Start Event)和Task上设置变量,而且支持变量自动替换,即利用表达式,语法就是UEL。
简化和强大完美的结合!
第一步:先建立一个中间表ACT_LIFE_ BRIDGE,只有3个字段:
- ProcessInstID
- ExecutionID
- TaskID
通过这三个字段可以获取到BPM的变量数据映射。
第二步:依然要新建portlet,但只需要新建jsp,不再需要vo、dao…
表单类似:
... <form:form action="<portlet:actionURL/>" modelAttribute="DynamicBean" method="post"> <div id = "task1"> <form:input path="initiator"/> <form:input path="reason"/> <form:input path="startDatetime"/> <form:input path="endDatetime"/> </div> <div id = "task2"> <form:select path="task2_isApprove"> <option value="1">同意</option> <option value="0">不同意</option> </form:select> <form:input path="task2_remark"/> </div> <div id = "task3"> <form:select path="task3_isApprove"> <option value="1">同意</option> <option value="0">不同意</option> </form:select> <form:input path="task3_remark"/> </div> <div id = "submit"> <input type="submit"/> </div> </form> ...
第三步,开发动态模型类
其中ProcessInstID在流程启动的时候建立,TaskID和ExecutionID在流程运行时建立,流程表单对应的变量在提交时注入。
最重点的实现是通过运行时创建动态实体类,模型是DynamicBean,实现的思路是:
import java.util.Iterator; import java.util.Map; import java.util.Set; import net.sf.cglib.beans.BeanGenerator; import net.sf.cglib.beans.BeanMap; public class DynamicBean { private Object object = null;//动态生成的类 private BeanMap beanMap = null;//存放属性名称以及属性的类型 public DynamicBean() { super(); } @SuppressWarnings("rawtypes") public DynamicBean(Map propertyMap) { this.object = generateBean(propertyMap); this.beanMap = BeanMap.create(this.object); } /** * 给bean属性赋值 * @param property 属性名 * @param value 值 */ public void setValue(Object property, Object value) { beanMap.put(property, value); } /** * 通过属性名得到属性值 * @param property 属性名 * @return 值 */ public Object getValue(String property) { return beanMap.get(property); } /** * 得到该实体bean对象 * @return */ public Object getObject() { return this.object; } /** * @param propertyMap * @return */ @SuppressWarnings("rawtypes") private Object generateBean(Map propertyMap) { BeanGenerator generator = new BeanGenerator(); Set keySet = propertyMap.keySet(); for (Iterator i = keySet.iterator(); i.hasNext();) { String key = (String) i.next(); generator.addProperty(key, (Class) propertyMap.get(key)); } return generator.create(); } }
测试类
import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class DyBean { public static void main(String[] args) throws ClassNotFoundException { System.out.println("Generate JavaBean ..."); // 设置类成员属性 Map properties = new HashMap(); properties.put("id", Class.forName("java.lang.Integer")); properties.put("name", Class.forName("java.lang.String")); properties.put("address", Class.forName("java.lang.String")); // 生成动态 Bean DynamicBean bean = new DynamicBean(properties); System.out.println(" OK!"); System.out.println("Set values ..."); // 给 Bean 设置值 bean.setValue("id", new Integer(123)); bean.setValue("name", "454"); bean.setValue("address", "789"); System.out.println(" OK!"); System.out.println("Get values"); // 从 Bean 中获取值,当然了获得值的类型是 Object System.out.println(" >> id = " + bean.getValue("id")); System.out.println(" >> name = " + bean.getValue("name")); System.out.println(" >> address = " + bean.getValue("address")); System.out.println("Class name"); // 查看动态 Bean 的类名 Class clazz = bean.getObject().getClass(); System.out.println(" >> " + clazz.getName()); System.out.println("Show all methods"); // 查看动态 Bean 中声明的方法 Method[] methods = clazz.getDeclaredMethods(); for(int i = 0; i < methods.length; i++) { System.out.println(" >> " + methods[i].getName()); } System.out.println("Show all properties"); // 查看动态 Bean 中声明的字段 Field[] fields = clazz.getDeclaredFields(); for(int i = 0; i < fields.length; i++) { System.out.println(" >> " + fields[i].getName()); } } }
或者
import java.util.HashMap; import java.util.Map; import org.apache.commons.beanutils.BasicDynaClass; import org.apache.commons.beanutils.DynaBean; import org.apache.commons.beanutils.DynaProperty; import org.apache.commons.beanutils.PropertyUtils; public class ddBean { public static void main(String[] args) throws Exception { //定义动态属性 DynaProperty[] props = new DynaProperty[]{ new DynaProperty("username", String.class), new DynaProperty("address", java.util.Map.class) }; //动态类 BasicDynaClass dynaClass = new BasicDynaClass("person", null, props); //动态bean DynaBean person = dynaClass.newInstance(); person.set("username", "jhlishero");//设置值 Map<String, String> maps = new HashMap<String, String>(); maps.put("key1", "value1"); maps.put("key2", "value2"); person.set("address",maps);//设置值 person.set("address", "key3", "value3");//第二种方法设置map中的值 System.err.println(person.get("username"));//获取字符串值 System.err.println(person.get("address", "key1"));//获取map中值 System.err.println(person.get("address", "key2")); System.err.println(person.get("address", "key3")); //使用PropertyUtils工具获取属性值 System.out.println(PropertyUtils.getSimpleProperty(person, "username")); } }
第四步,开发属性关联 (通过XML解析器)
属性表不需要再新建XML,直接就用Activiti流程定义文件XML来获取变量名称,
类似于:
<startEvent id="request" activiti:initiator="initiator"> <extensionElements> <activiti:formProperty id="startDatetime" name="请假开始时间" datePattern="yyyy-MM-dd hh:mm" type="date" required="true" /> <activiti:formProperty id="endDatetime" name="请假结束时间" datePattern="yyyy-MM-dd hh:mm" type="date" required="true" /> <activiti:formProperty id="reason" name="请假事由" type="string" /> </extensionElements> </startEvent> <userTask id="task2" name="上级审批" > <documentation> ${initiator} 申请休假 </documentation> <extensionElements> <activiti:formProperty id="task2_isApprove" name="是否同意" type="enum" required="true"> <activiti:value id="true" name="Approve" /> <activiti:value id="false" name="Reject" /> </activiti:formProperty> <activiti:formProperty id="task2_remark" name="审批意见" type="string" /> </extensionElements> </userTask> <sequenceFlow id="flow1" sourceRef="request" targetRef="task2" /> <sequenceFlow ...
还需要再开发一个XML解析器,用于属性注入。
实现代码略。
目前维护的开源产品:https://gitee.com/475660