获取流程图的方式
- 通过flowable提供的jar包,直接连接flowable数据库,调用flowable的api(diagram)生成流程图(直接连接了flowable数据库,微服务中最好不要这样)
- 运行flowable提供的rest-api的war包,调用restful api接口返回流图
runtime/process-instances/{processInstanceId}/diagram
这种方式,不能返回已经完成流程的流程图(官方规定不返回 :)) - 运行flowable-admin的war包,登录,并调用
app/rest/admin/process-instances/{processInstanceId}/model-json(或history-model-json)?processDefinitionId={processDefinitionId}
获取流程节点信息,再调用前端或后台进行节点绘制(由于业务要求,我只能用这种方式实现 …)
获取流程节点数据,后台java绘制
1、由于不是直接使用flowable提供的rest-api,而是调用上面第三点的api进行节点数据获取,会直接跳转到admin的登陆界面,不是普通的页面验证,于是先后台模仿登录,获取需要的cookie值:
/**
* flowable-admin登录认证
*
* @return cookie
* @throws IOException
*/
public String getAuthenticationSession() {
// http协议头+服务器ip+端口 + flowable-idm/app/authentication(admin的登陆请求)
String authenticationUrl = bpmApiProtocol + bpmApiHost + ":" + bpmApiPort + "/" + bpmApiIdmAuthPath;
URL url = null;
String responseCookie= "";
try {
url = new URL(authenticationUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 允许连接提交信息
connection.setDoOutput(true);
connection.setRequestMethod("POST");
// 请求参数
String content = "j_username=" + bpmApiIdmUser + "&j_password=" + bpmApiIdmPassword + "&_spring_security_remember_me=true&submit=Login";
// 提交请求数据
OutputStream os = connection.getOutputStream();
os.write(content.getBytes("utf8"));
os.close();
// 取到所用的Cookie, 认证
responseCookie = connection.getHeaderField("Set-Cookie");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return responseCookie;
}
2、通过获取的cookie,作为admin中的提供的api的cookie,进行数据获取
String modelpath = "model-json";
if (判断该流程实例是否已经结束) {
modelpath = "history-model-json";
}
URL url = null;
try {
// 获取响应的cookie 值
String responseCookie = getAuthenticationSession();
HttpURLConnection conn = null;
// 请求路径根路径
// http://服务器ip:端口/flowable-admin/app/rest/admin/process-instances/流程实例id/model-json?processDefinitionId=流程定义id
String modelUrl = bpmApiProtocol + bpmApiHost + ":" + bpmApiPort + "/" + bpmApiProcessPath;
url = new URL(modelUrl + processId + "/" + modelpath + "?processDefinitionId=" + instanceList.get(0).getProcessDefinitionId());
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
// 设置为之前登录获取的cookie
conn.setRequestProperty("Cookie", responseCookie);
conn.connect();
StringBuilder builder = new StringBuilder();
// 将数据读入StringBuilder;
int successCode = 200;
if (conn.getResponseCode() == successCode) {
InputStream inputStream = conn.getInputStream();
byte[] data = new byte[1024];
StringBuffer sb1 = new StringBuffer();
int length = 0;
while ((length = inputStream.read(data)) != -1) {
String s = new String(data, 0, length);
sb1.append(s);
}
builder.append(sb1.toString());
inputStream.close();
}
// 将图片以流的形式返回response
writeImage(builder.toString(), response);
} catch (IOException e) {
e.printStackTrace();
log.error(e.getMessage());
}
此处读取到的json数据
elements: 每个节点的信息(图片位置,节点类型,是否完成,当前节点)
flows:每条线的位置及线上对应的内容
其他相关信息
{
"elements": [
{
"completed": true,
"current": false,
"id": "startEvent1",
"name": null,
"incomingFlows": [ ],
"x": 170,
"y": 240,
"width": 30,
"height": 30,
"type": "StartEvent"
},
{
"completed": true,
"current": true,
"id": "sid-2DEC37F4-6DDA-4F9C-851E-253FEF5E2AEA",
"name": "业务主管审核",
"incomingFlows": [
"sid-B6657CA2-B886-4F41-84E3-144DB506732D",
"sid-CF900A33-E208-494D-BE2B-5DF6E9B98CEE",
"sid-09ACE55D-6012-4EB9-82B4-E2D82530A30F"
],
"x": 135,
"y": 330,
"width": 100,
"height": 80,
"type": "UserTask",
"properties": [ ]
},
...............................................
],
"flows": [
{
"completed": false,
"current": false,
"id": "sid-496BE5E8-5817-4DE0-ADF2-BA9DE52007BF",
"type": "sequenceFlow",
"sourceRef": "sid-2DEC37F4-6DDA-4F9C-851E-253FEF5E2AEA",
"targetRef": "sid-24C983CE-C446-455B-B4FC-90D5E7736F89",
"name": "放弃",
"waypoints": [
{
"x": 134,
"y": 370
},
{
"x": 27,
"y": 370
},
{
"x": 27,
"y": 655
},
{
"x": 180,
"y": 655
}
]
},
.............................................
],
"collapsed": [ ],
"diagramBeginX": 27,
"diagramBeginY": 240,
"diagramWidth": 460,
"diagramHeight": 695,
"completedActivities": [
"startEvent1",
"sid-B6657CA2-B886-4F41-84E3-144DB506732D",
"sid-2DEC37F4-6DDA-4F9C-851E-253FEF5E2AEA",
"sid-C50A9276-D20D-4278-9B76-F6A233ACB284",
"sid-CEB4D4E0-259A-47CB-BC70-C4D679794D59",
"sid-09ACE55D-6012-4EB9-82B4-E2D82530A30F",
"sid-2DEC37F4-6DDA-4F9C-851E-253FEF5E2AEA"
],
"currentActivities": [
"sid-2DEC37F4-6DDA-4F9C-851E-253FEF5E2AEA"
],
"completedSequenceFlows": [ ]
}
3、调用java 的 Graphics2D 绘制流程图,(可以直接使用,路由绘制未添加判断,后续添加即可)
考虑点:
- 节点的图形,通过节点类型判断;位置通过json数据解析
- 线条,位置通过json数据解析,动作名称统一设置在第二个点上,如果只有两个点,就放在两点之间
- 箭头指向(最烦画这个),这个需要判断方向,如果是斜着指向,需要通过旋转某一点来推断箭头三个点的位置;第一个点为线段的最后一个节点
/**
* 解析json数据,绘制流程图,并返回response
* @param procData 流程数据
* @param response response
* @return 是否成功
*/
public static boolean writeImage(String procData, HttpServletResponse response) {
JSONObject processData = JSONObject.parseObject(procData);
BufferedImage bi = new BufferedImage(460 + 20, 695 + 20, BufferedImage.TYPE_INT_BGR);
Graphics2D g = bi.createGraphics();
// 生成透明背景的画板,再重新生成画板
bi = g.getDeviceConfiguration().createCompatibleImage(460 + 20, 695 + 20, Transparency.TRANSLUCENT);
g.dispose();
g = bi.createGraphics();
// 节点信息位置
JSONArray elements = (JSONArray) processData.get("elements");
// 连线坐标
JSONArray flows = (JSONArray) processData.get("flows");
g.setColor(new Color(0, 0, 0));
// 画线
for (int i = 0; i < flows.size(); i++) {
JSONObject el = (JSONObject) flows.get(i);
System.out.println(el);
String name = (String) el.get("name");
JSONArray points = (JSONArray) el.get("waypoints");
for (int j = 0; j < points.size() - 1; j++) {
JSONObject po1 = (JSONObject) points.get(j);
int x1 = ((BigDecimal) po1.get("x")).intValue();
int y1 = ((BigDecimal) po1.get("y")).intValue();
JSONObject po2 = (JSONObject) points.get(j + 1);
int x2 = ((BigDecimal) po2.get("x")).intValue();
int y2 = ((BigDecimal) po2.get("y")).intValue();
g.setColor(new Color(105, 105, 105));
g.setStroke(new BasicStroke(2.0f));
g.drawLine(x1, y1, x2, y2);
g.setStroke(new BasicStroke(5.0f));
// 箭头绘制
// 1、先求出最后一条线的长度
double pow = Math.pow(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2), 1.0f / 2.0f);
// 2、设置箭头的长度,占该线的长度比
double rate = 9.0 / pow;
// 3、按比例获取该线上,长度为箭头长度的点,该处的nx,ny取矢量值,即保留正负
int nx = (int) ((x2 - x1) * rate);
int ny = (int) ((y2 - y1) * rate);
int mx = x2 - nx;
int my = y2 - ny;
// 4、以上面获取的矢量值,进行90°旋转;这里以最后一个点为原点进行旋转
Double vx = (nx) * Math.cos(1.57079632679489661923f) - (ny) * Math.sin(1.57079632679489661923f);
Double vy = (ny) * Math.cos(1.57079632679489661923f) + (nx) * Math.sin(1.57079632679489661923f);
// 5、获取旋转的点
Double px1 = mx + vx / 2;
Double py1 = my + vy / 2;
// 6、通过上面获取的点,推出最后一个点
Double px2 = mx + mx - px1;
Double py2 = my + my - py1;
// 7、对不同方向的箭头进行判断;由于有些位置有画笔粗细导致偏移问题,单独进行了位置的偏移
if (j == (points.size() - 2)) {
// 左上
if (x2 < x1 && y2 < y1) {
int[] xPoints = {x2, px1.intValue(), px2.intValue()};
int[] yPoints = {y2, py1.intValue(), py2.intValue()};
g.drawPolygon(xPoints, yPoints, 3);
}
// 左下
if (x2 < x1 && y2 > y1) {
int[] xPoints = {x2 + 2, px1.intValue() + 2, px2.intValue() + 2};
int[] yPoints = {y2 - 2, py1.intValue() - 2, py2.intValue() - 2};
g.drawPolygon(xPoints, yPoints, 3);
}
//正左
if (x2 < x1 && y2 == y1) {
int[] xPoints = {x2 + 5, x2 + 15, x2 + 15};
int[] yPoints = {y2, y2 - 5, y2 + 5};
g.drawPolygon(xPoints, yPoints, 3);
}
// 右上
if (x2 > x1 && y2 < y1) {
int[] xPoints = {x2, px1.intValue(), px2.intValue()};
int[] yPoints = {y2, py1.intValue(), py2.intValue()};
g.drawPolygon(xPoints, yPoints, 3);
}
// 右下
if (x2 > x1 && y2 > y1) {
int[] xPoints = {x2, px1.intValue(), px2.intValue()};
int[] yPoints = {y2, py1.intValue(), py2.intValue()};
g.drawPolygon(xPoints, yPoints, 3);
}
// 正右
if (x2 > x1 && y2 == y1) {
int[] xPoints = {x2 - 5, x2 - 15, x2 - 15};
int[] yPoints = {y2, y2 - 5, y2 + 5};
g.drawPolygon(xPoints, yPoints, 3);
}
// 正上
if (x2 == x1 && y2 < y1) {
int[] xPoints = {x2, x2 - 5, x2 + 5};
int[] yPoints = {y2 + 5, y2 + 15, y2 + 15};
g.drawPolygon(xPoints, yPoints, 3);
}
// 正下
if (x2 == x1 && y2 > y1) {
int[] xPoints = {x2, x2 - 5, x2 + 5};
int[] yPoints = {y2 - 5, y2 - 15, y2 - 15};
g.drawPolygon(xPoints, yPoints, 3);
}
}
// 执行动作
if (j == 0 && name != null) {
g.setColor(new Color(95, 158, 160));
g.setFont(new Font("SansSerif", Font.ITALIC, 15));
if (points.size() == 2) {
g.drawString(name, (x2 + x1) / 2, (y2 + y1) / 2);
} else {
g.drawString(name, x2, y2);
}
}
}
}
// 画图
for (int i = 0; i < elements.size(); i++) {
JSONObject el = (JSONObject) elements.get(i);
boolean completed = (Boolean) (el.get("completed"));
boolean current = (Boolean) (el.get("current") == null ? false : el.get("current"));
String name = (String) el.get("name");
int x = ((BigDecimal) el.get("x")).intValue();
int y = ((BigDecimal) el.get("y")).intValue();
int width = ((BigDecimal) el.get("width")).intValue();
int height = ((BigDecimal) el.get("height")).intValue();
String type = (String) el.get("type");
System.out.println("=========================");
// 运行到当前节点
if (current) {
g.setColor(new Color(220, 20, 60));
g.setStroke(new BasicStroke(5.0f));
} else if (completed) {
// 当前节点已完成
g.setColor(new Color(102, 170, 102));
g.setStroke(new BasicStroke(3.0f));
} else {
// 默认未执行的节点
g.setColor(new Color(0, 0, 0));
g.setStroke(new BasicStroke(3.0f));
}
// 正方形
if ("UserTask".equals(type)) {
RoundRectangle2D roundedRectangle = new RoundRectangle2D.Float(x, y, width, height, 10, 10);
g.draw(roundedRectangle);
}
// 粗圆
if ("StartEvent".equals(type)) {
g.drawOval(x, y, width, height);
}
// 细圆
if ("EndEvent".equals(type)) {
g.setStroke(new BasicStroke(5.0f));
g.drawOval(x, y, width, height);
}
// 后续添加路由相关的判断
if (name != null) {
g.setFont(new Font("Serif", Font.BOLD, 12));
g.drawString(name, x + 10, y + (height) / 2);
}
}
g.dispose();
boolean val = false;
try (
ServletOutputStream out = response.getOutputStream();
) {
val = ImageIO.write(bi, "png", out);
} catch (IOException e) {
e.printStackTrace();
log.error(e.getMessage());
}
return val;
}
前端获取效果图:
非要后端绘制流程图,又不能连数据库,太坑了:)