一、SSH整合JBPM
JBPM基础见http://www.cnblogs.com/kuangdaoyizhimei/p/4981551.html
现在将要实现SSH和JBPM的整合。
1.添加jar包
(1)jbpm项目/lib目录下的所有jar包和根目录下的jbpm.jar包放入/WEB-INF/lib文件夹下,同时删除tomcat服务器/lib文件夹中的el-api.jar包。
注意:必须删除el-api.jar包,该jar包和jbpm项目中需要使用到的三个jar包冲突了:juel-api.jar、juel-engine.jar、juel-impl.jar
最好的解决方法就是将tomcat服务器根目录下的/lib文件夹中的el-api.jar包使用上述的三个jar包替换掉,这样也能够运行其他的服务器程序。
也就是说以上的三个jar包的功能能够代替el-api.jar包的功能,但是反之则不可以。
(2)hibernate的相关jar包不需要添加了。jbpm项目中已经添加了hibernate相关的jar包。这里推荐使用jbpm项目中提供的hibernate支持jar包。否则可能会出现一些莫名其妙的问题。
(3)mysql的驱动jar包也不需要添加了,因为该jar包也已经在jbpm项目中提供了。推荐使用jbpm项目中提供的mysql的驱动jar包。原生的更靠谱,但是亲测使用Mysql官方网站提供的驱动jar包也没有问题。
(4)添加spring的相关jar包。
(6)添加struts2的相关jar包。
(7)项目中会使用到ajax的相关功能,前端ajax和struts2交互使用struts2提供的jar包更方便(自定义结果集也能够实现,但是比较麻烦)
(7)推荐将使用到的jar包分门别类的放到一起,方便管理。
ext文件夹存放了代替el-api.jar包的三个jar包;jstl文件夹用不着,我使用的是java EE 6.0的版本,自带jstl的相关支持jar包;junit文件夹中存放单元测试的相关jar包,该jar包也不需要下载,myeclipse自带,直接右键项目->Build Path->Add Libraries->JUnit也能够添加,注意选用4.0的版本。
2.配置文件的配置
建议将配置文件单独放在一个source文件夹中,如下图所示
SSH整合过程见
http://www.cnblogs.com/kuangdaoyizhimei/p/4855034.html
只说几处和SSH和JBPM整合的关键地方:
(1)hibernate.cfg.xml配置文件被jbpm.hibernate.cfg.xml配置文件取代。两个文件中的内容合并到一起,最后在spring中声明配置文件的位置即可(jpbm.hibernate.cfg.xml)
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:jbpm/jbpm.hibernate.cfg.xml"></property>
</bean>
(2)jbpm相关的配置
jbpm最重要的一个对象就是ProcessEngine对象,所有的一切流程动作都需要从此对象出发,创建该对象的任务也需要交给spring,注意创建方式为工厂方式。
<!-- 配置jbpm相关 -->
<bean id="springHelper" class="org.jbpm.pvm.internal.processengine.SpringHelper">
<property name="jbpmCfg" value="jbpm/jbpm.cfg.xml"></property>
</bean>
<bean id="processEngine" factory-bean="springHelper" factory-method="createProcessEngine"></bean>
(3)其余配置过程就是SSH的整合过程了,不赘述。
3.SSH整合JBPM的过程主要还是SSH整合的过程,所以SSH是基础,必须熟练掌握。
二、项目演示
项目工程源代码:https://github.com/kdyzm/sshAndJBPM
在项目运行之前,首先运行com.kdyzm.init.Init类,初始化用户数据。
1.浏览器上输入http://localhost:8080/jbpmAndSSH,出现登陆界面
输入预置的用户名张三,密码xiaozhang(所有的用户密码统一为xiaozhang)登陆
2.登陆后主界面
3.首先画一张流程图,打包成zip压缩文件,单击部署流程定义文档进行部署。
单击确定按钮之后:
查看流程图:
至此,流程部署已经成功。
4.表单模板管理
该项功能可以根据现有的流程定义新建流程模板等。
添加新模板:上传一个模板文件
上传成功之后:
5.发起申请
单击第一项:请假流程表单模板
自动跳转到“我的申请”查询页面
6.申请查询
单击查询状态,会显示“我”的所有申请入上图所示
单击查看流转记录图,可以查看当前流程的审批进程
以上的流程图是比较复杂的流程图,之前的笔记中提到的fork/join节点在这里使用到了。现在的流程实例需要项目组长和项目副组长共同同意流程才能流向总经理审批节点。
红色方框是怎么动态显示出来的是一个非常有意思的事情,使用CSS+DIV块可以精确定位,对border设置样式 即可。
7.审批
流程流向了项目组长审批(张三)和项目副组长(李四)审批的任务节点。当前账户是张三,由张三第一个审批。
单击审批处理查看当前有权限处理的所有审批任务
单击审批处理:
直接点击同意,返回到审批列表,这时任务已经结束,所以审批列表中已经没有任务了。
8.再次查看我的申请,查看流程状态
9.登陆李四(副组长)的账户,进行审批
和上面一样如法炮制,点击同意,切换回张三的账户,查看审批进度,发现流程已经流转到总经理审批的地方了
登陆王五(总经理)的账户,审批同意,再次返回张三的账户查看审批进度
流程已经流转到总裁审批的任务节点了。
接下来使用赵六(总裁)的账户登陆系统,进行审批,审批过后使用张三的账户登录系统,再次查看流程状态,这时已经查看不了流程图了,因为申请状态已经标志为“完成申请”
对比未完成的状态,当前状态发生了改变,同时不能查看流转记录图了:
10.该小项目的主要功能就这么多。
三、技术点分析
1.文件上传
写了一个文件上传的工具类解决文件上传的问题
package com.kdyzm.utils; import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID; import org.apache.commons.io.FileUtils;
import org.apache.struts2.ServletActionContext; /**
* 文件上传的工具类
* @author kdyzm
*
*/
public class FileUploadUtils {
/**
* 保存文件到指定的位置
* 注意保存的方法的问题
* @param sourceFile
* @param infactFileName
* @return
*/
public static File saveUploadFileToDestDir(File sourceFile, String infactFileName){
SimpleDateFormat sdf=new SimpleDateFormat("/yyyy/MM/dd");
Date date=new Date();
File dir=new File(ServletActionContext.getServletContext().getRealPath("/upload")+sdf.format(date));
if(!dir.exists()){
dir.mkdir();
}
String []arrFileNames=infactFileName.split("\\."); String lastFileName=arrFileNames[arrFileNames.length-1];
File destFile=new File(dir,UUID.randomUUID().toString().replaceAll("-", "")+"."+lastFileName);
try {
FileUtils.copyFile(sourceFile, destFile);
} catch (IOException e) {
System.out.println("保存文件失败!");
}
return destFile;
}
}
文件上传到了项目根目录下的/upload/年/月/日文件夹下。
2.文件下载
需要注意的事项:
(1)使用URLEncoder类对文件名进行编码,如果是英文名倒不要紧,如果是中文名,则必须使用该类
String fileName=URLEncoder.encode(formTemplate.getName(),"utf-8");
(2)设置响应头信息,告诉浏览器返回的内容是需要下载的
response.setContentType("application/force-download");
response.setHeader("Content-Disposition","attachment;filename="+fileName);
(3)方法返回值为NULL,在struts2中,如果返回值不是NULL,则会报异常,但是并不影响下载。
return null;
(4)下载表单模板完整事例
/**
* 下载指定formTemplateId指向的文件的方法
*/
public String downloadFormTemplateById() throws Exception{
HttpServletRequest request=ServletActionContext.getRequest();
HttpServletResponse response=ServletActionContext.getResponse();
String formTemplateId=request.getParameter("formTemplateId");
FormTemplate formTemplate=formTemplateService.getFormTemplateById(formTemplateId);
File file=new File(formTemplate.getUrl());
String fileName=URLEncoder.encode(formTemplate.getName(),"utf-8"); response.setContentType("application/force-download");
response.setHeader("Content-Disposition","attachment;filename="+fileName);
FileInputStream fis=new FileInputStream(file);
OutputStream os=response.getOutputStream();
int length=-1;
byte buff[]=new byte[1024];
while((length=fis.read(buff))!=-1){
os.write(buff, 0, length);
}
os.close();
fis.close();
return null;
}
3.如何发起申请
发起申请是整个流程中最为复杂的一个部分。
分析发起申请的几个步骤:
* 第一步:上传申请表单
* 第二步:开启流程实例
虽然只有两步,但是每一步还有一些细节需要处理
(1)上传申请表单
表单对象Form的属性设置是非常重要的,它有如下几个非常重要的属性:
* FormTemplate :该表单使用了什么表单模板
* Applicator :申请人是谁
* String state :当前的申请状态
* String url :上传之后表单存储的位置
* String name :存储的文件的名字和真正的名字是不相同的,使用该字段保存真正的名字
* String processId :绑定的流程实例ID,一个表单对象对应着一次申请流程,对应着一个流程实例;这里不能绑定任务对象。因为任务对象会变,维护起来比较麻烦,而且对于fork/join形式的流程,无法绑定。
以上的几个属性中,除了processId属性之外,其余属性在上传申请表单的过程中都要赋值。
//不仅仅是上传表单,还有更重要的流程控制的问题
public String uploadForm() throws Exception{
Form form=new Form();
HttpServletRequest request=ServletActionContext.getRequest();
String formTemplateId=request.getParameter("formTemplateId");
FormTemplate formTemplate=formTemplateService.getFormTemplateById(formTemplateId);
Date date=new Date();
String dateString=DateUtils.dateToString(date);
User user=(User) request.getSession().getAttribute("user");
//先部署流程实例 form.setApplicateTime(dateString);
form.setApplicator(user.getUserName()); //申请人设置,谁登陆系统的就是谁申请的
form.setDate(dateString);
form.setFormTemplate(formTemplate);
form.setState(FormState.SHENQINGZHONG); //一开始的时候申请状态就是申请中
form.setTitle(formTemplate.getProcessKey()+"-"+user.getUserName()+"-"+dateString);
File file=FileUploadUtils.saveUploadFileToDestDir(formFile, formFileFileName);
form.setUrl(file.getAbsolutePath());
form.setName(formFileFileName); formService.saveForm(form);
jbpmProcessManagementService.startProcessByKey(formTemplate.getProcessKey(),form);
//发起申请之后应当跳转到我的所有的申请表上
return "toListAllApplicationAction";
}
开启流程实例的入口方法
(2)开启流程实例
这里开启流程实例使用最高版本的流程定义来实现,使用pdkey来开启流程实例。
另外还有一个非常重要的设计思想就是将form对象作为流程变量放置到流程实例中。
同时,还需要将流程实例id属性设置到form属性processId中,并更新form表。
/**
* 完成开启流程实例的关键的地方就是将form变量绑定到流程实例
*/
@Override
public void startProcessByKey(String processKey,Form form) {
//在开启流程实例的时候将form对象绑定到流程实例上
Map<String,Object> variables=new HashMap<String,Object>();
variables.put("form", form);
User user=(User) ServletActionContext.getRequest().getSession().getAttribute("user");
variables.put("shenqingren", user.getUserName());
ProcessInstance processInstance=processEngine.getExecutionService()
.startProcessInstanceByKey(processKey,variables); //下一步完成任务,也就是说自己批准自己申请的那一步骤
Task task=this.processEngine
.getTaskService()
.createTaskQuery()
.executionId(processInstance.getId()) //根据流程实例id获取为一个的一个Task对象
.uniqueResult(); form.setProcessId(processInstance.getId()); //将form对象和p流程实例对象绑定在一起
formDao.updateEntry(form);
//完成任务
this.processEngine.getTaskService()
.completeTask(task.getId());
}
开启流程实例的真正方法
(3)为什么要将form对象作为流程变量放置到流程实例中?
这种方法是将Form对象和Task对象关联到一起的一种方法,在显示所有审批列表的时候,急需要Form的信息,也需要Task的信息,但是Form表中并没有关联到Task对象,如果直接关联到Task对象,维护Form表就变得相当麻烦了,所以采取了“迂回战术”解决这个问题:将form对象放到流程实例中这样只要有了流程实例对象就能找到form对象;流程实例对象可以根据Task对象来获得;Task对象可以根据当前登录人来获得(保存到了Session中),这样绕了一圈好像挺费劲,但是实际上降低了Form对象和Task对象的耦合性,这是极佳的设计模式。
新建类TaskView,只有两个属性,一个是Task,一个是Form,这样返回Task对象列表就可以解决问题了。
//查询当前所有任务列表的方法
@Override
public Collection<TaskView> getAllTaskViews() {
User user=(User) ServletActionContext.getRequest().getSession().getAttribute("user");
//首先查找到当前用户的的所有任务列表
List<Task> tasks=this.processEngine
.getTaskService()
.createTaskQuery()
.assignee(user.getUserName())
.list();
List<TaskView>taskViews=new ArrayList<TaskView>();
//根据任务列表信息
for(Task task:tasks){
TaskView taskView=new TaskView();
Form form=(Form) this.processEngine.getExecutionService().getVariable(task.getExecutionId(), "form");
taskView.setForm(form);
taskView.setTask(task);
taskViews.add(taskView);
}
return taskViews;
}
4.如何批准申请
所谓批准申请实际上就是完成任务的一个过程。由于在这里完成任务需要将流程变量繁殖到流程实例中,但是并不明确是否是最后一个任务,所以一律采用先把流程变量放置到流程实例中,再完成任务,防止出错(在JBPM基础中,曾经说过,如果完成任务的时候使用重载方法completeTask(String taskId,Map variables)的方法,如果当前完成的任务是流程实例中的最后一个任务,则会报错,这是JBPM4.4中的一个BUG)。
这样批准申请的过程就变成了:获取流程变量->将流程变量放入流程实例->结束任务->改变Form对象状态(只有当完成的任务是最后一个任务的时候才改变Form对象的状态)。
在完成批准申请的时候有一个重要的东西需要特别注意:如何判断当前任务是最后一个任务?
(1)对于单线程的流程来说,流程实例只有一个,直接使用
this.processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(task.getExecutionId()).uniqueResult()==null
进行判断即可,然而这种方式只能处理单线程的流程图
(2)对于fork/join的流程来说,并不是只有一个流程实例表中并非只有一个流程实例,还有“子流程实例”,使用task.getExecutionId()的方法获取到的流程实例很有可能是“子流程实例”,如果任务完成了,那么“自流程实例”的状态就会被置为“inactive”,即非活跃的状态,这时候再使用(1)中的方法查询到的结果一定是null;解决方法就是使用“主流程实例”的id来判断,无论“子流程实例”的状态怎么变化,只要“主流程实例”没有结束,那么当前流程实例就没有结束。该id保存到了Form对象中,所以使用如下方式判断流程实例是否已经结束更为准确(1)中的方法废弃不用:
this.processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(form.getProcessId()).uniqueResult()==null
完整处理的代码:
HttpServletRequest request=ServletActionContext.getRequest();
String fid=request.getParameter("fid");
String taskId=request.getParameter("taskId");
String comment=request.getParameter("comment");
Form form=formService.getFormByFormId(fid);
Task task=processEngine.getTaskService().getTask(taskId); Map<String,String> variables=new HashMap<String,String>();
variables.put("comment_"+task.getAssignee(), comment);
processEngine.getTaskService().setVariables(taskId, variables);
processEngine.getTaskService().completeTask(taskId);
this.message="1";
//怎么判断是否还有后续任务
ExecutionService executionService=processEngine.getExecutionService();
ProcessInstanceQuery processInstanceQuery=executionService.createProcessInstanceQuery();
//这里根据form的processId来判断比较好
ProcessInstanceQuery processInstanceQuery2=processInstanceQuery.processInstanceId(form.getProcessId());
List<ProcessInstance> processInstances=processInstanceQuery2.list();
System.out.println("得到的流程数量:"+processInstances.size());
task.getExecutionId();
if(this.processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(form.getProcessId()).uniqueResult()==null){
form.setState(FormState.WANCHENGSHENQING);
formService.updateForm(form);
}
return "ajax";
5.如何显示流程图(不包括当前任务节点)
比如在显示所有流程定义的时候,使用到了“查看流程图”的链接,单击该连接,就弹框显示出了一张流程图。
思路:单击超链接->触发单击事件->调用window.open方法显示弹框->弹框页面有img标签,动态请求后台图片->后台从数据库中获取图片返回输出流->显示图片
(1)整个过程中有一个参数在各个页面中一直传递着,流程实例ID,使用processId来表示,该参数不断的在前台和后台之间传递。
(2)前台触发点击事件处理代码:
$().ready(function(){
$("a").each(function(){
if($(this).text()=="查看流程图"){
$(this).unbind("click");
$(this).bind("click",function(){
var left=(screen.width-800)/2;
var top=(screen.height-500)/2;
window.open("PDManagementAction_showImageUI?"+$(this).attr("href").split("?")[1],"_blank","height=500,width=800,left="+left+",top="+top+",scrollbars=1",false);
return false;
});
}
});
});
(3)经过Action转发,跳转到showImageUI.jsp页面:
<img alt="流程定义图片" src="PDManagementAction_showProcessImageByPdid.action?processId=<s:property value='%{#processId}'/>">
(4)请求了PDManagementAction的showProcessImageByPdid方法,方法中根据processId获取图片的输入流,并经过stream拦截器写回到输出流中,这一步由struts2完成,我只需要获取到输入流即可。
//显示流程图的方法
public String showProcessImageByPdid() throws Exception{
HttpServletRequest request=ServletActionContext.getRequest();
String processId=request.getParameter("processId");
InputStream is=this.pdManagementService.getImageInputStreamByPDID(processId);
this.is=is;
return "stream";
}
注意,Action中一定要提供一个输入流InputStream属性,并且提供getInputStream方法和setInputStream方法,方法名一定要完全一致,不能是getIs和setIs!(通常会写成InputStream is,使用快捷见生成的get、set方法就会变成getIs和setIs)
注意,Action的配置文件一定是这种形式:
<!-- 类型为stream,由流拦截器来处理后续事项,result标签中没有内容 -->
<result name="stream" type="stream"></result>
(5)显示图片
6.如何显示流程实例审批到哪里了
这个功能是一个非常有意思的功能,显示流程图的过程和5中的过程一模一样,关键是怎么使用红色矩形标识出当前的任务节点,值得一提的是,当前任务节点可能是一个,也有可能是多个,在做的时候必须都考虑到。
画图使用的技术十分简单,就是CSS,使用CSS的绝对定位功能+DIV块即可
(1)分析过程:单击显示流程周转图超链接->触发事件单击事件->调用window.open方法打开新窗口,这时候会同时发生两件事情
* CSS绘制矩形到当前的任务节点
* img标签发起请求请求流程图。
然后最后显示两步叠加的图片,如下图所示:
(2)首先标签超链接需要传递processId,即流程实例标识号参数
<s:if test="%{state!='完成申请'}">
<s:a action="FormAction_showCurrentActivityImage.action">
<s:param name="processId" value="%{processId}"></s:param>
查看流转记录图
</s:a>
</s:if>
(3)单击响应方法:打开了一个新窗口
$().ready(function(){
$("a").each(function(){
if($(this).text()=="查看流转记录图"){
$(this).unbind("click");
$(this).bind("click",function(){
var left=(screen.width-800)/2;
var top=(screen.height-500)/2;
window.open("FormAction_showImageUI.action?"+$(this).attr("href").split("?")[1],"_blank","height=500,width=800,left="+left+",top="+top+",scrollbars=1",false);
return false;
});
}
});
});
(4)在打开新窗口之前首先经过Action预配置数据,这里是获取活动节点的数量、位置、长度、宽度的关键;这里使用了一个自定义的Rectangle将这些数据进行了封装,最终返回了一个封装了这些数据的List结合。
public String showImageUI() throws Exception{
HttpServletRequest request=ServletActionContext.getRequest();
String processId=request.getParameter("processId");
ProcessInstance processInstance=processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(processId).uniqueResult();
Set<String> names=processInstance.findActiveActivityNames();
List<Rectangle>rectanglelist=new ArrayList<Rectangle>();
for(String name:names){
ActivityCoordinates ac=this.processEngine.getRepositoryService().getActivityCoordinates(processInstance.getProcessDefinitionId(), name);
Rectangle rectangle=new Rectangle();
rectangle.setX(ac.getX());
rectangle.setY(ac.getY());
rectangle.setHeight(ac.getHeight());
rectangle.setWidth(ac.getWidth());
rectanglelist.add(rectangle);
System.out.println(rectangle);
}
ActionContext.getContext().put("processId", processId); //请求流程图使用
ActionContext.getContext().put("rectanglelist", rectanglelist); //绘制活动节点矩形框使用
return "showImageUI";
}
(5)数据被转发到了新窗口,新窗口同时进行两个任务
* 绘制活动节点矩形框:这里使用struts2标签进行遍历创建DIV块,并使用CSS的定位功能对DIV的卫士进行了设定;设置DIV的边框为3px 红色。
<s:iterator value="%{#rectanglelist}">
<div style="position:absolute;border:3px solid red;left:<s:property value="%{x+10}"/>;top:<s:property value="%{y+10}"/>;height:<s:property value="%{height-10}"/>;width:<s:property value="%{width-10}"/>;"> </div>
</s:iterator>
* 请求流程图
<img alt="显示活动的节点图片" src="FormAction_showCurrentActivityImage.action?processId=<s:property value='%{#processId}'/>">
该页面的完整代码为:
<body>
<img alt="显示活动的节点图片" src="FormAction_showCurrentActivityImage.action?processId=<s:property value='%{#processId}'/>">
<s:iterator value="%{#rectanglelist}">
<div style="position:absolute;border:3px solid red;left:<s:property value="%{x+10}"/>;top:<s:property value="%{y+10}"/>;height:<s:property value="%{height-10}"/>;width:<s:property value="%{width-10}"/>;"> </div>
</s:iterator>
</body>
(6)显示审批的进度流程图。
7.如何显示最高版本的流程定义
使用ProcessDefinitionQuery的orderAsc方法进行升序排序,然后使用Map对象依次添加即可。低版本的最终会被高版本的“冲掉”
//查找最高版本的流程定义是关键
@Override
public Set<ProcessDefinition> listAllPDs() {
List<ProcessDefinition> processDefinitions=processEngine.getRepositoryService()
.createProcessDefinitionQuery()
.orderAsc(ProcessDefinitionQuery.PROPERTY_VERSION) //根据Version进行升序排序
.list();
Map<String,ProcessDefinition> map=new HashMap<String,ProcessDefinition>();
for(ProcessDefinition processDefinition:processDefinitions){
map.put(processDefinition.getKey(), processDefinition);
System.out.println(processDefinition.getId());
}
Set<ProcessDefinition> processDefinitions2=new HashSet<ProcessDefinition>(map.values());
return processDefinitions2;
}
JBPM整合SSH的项目实战结束。