在实习公司最后接触的一个项目(有关于溯源问题)中碰到了一个问题,是在后端开发中生成一张Prov图。从概念的理解,到寻找解决方法,到最后解决问题,花了较长的时间。今天在这里总结一下。
本文按照逻辑顺序编排,也就是我解决问题的步骤,列举了多个我尝试过的方法。
所以可以跳过【Prov-ToolBox】(弃用)直接看【最终解决方案】,也就是我认为当前最佳的解决方案。
我们在百度上搜,发现还搜不到prov。然后去文献数据库中找寻一些资源,从中国海洋大学刘兵,徐建良前辈的文献【基于PROV的大洋样品数据溯源】中可以发现以下解释:
prov: was-DerivedFrom
来表示; 实体和活动之间的关系有 prov: used
和 prov:wasGeneratedBy
,分别表示实体被活动所使用和实体由活 动 产 生; 活动和活动之间利用属性 prov: wasInformedBy
表示时间次序关系; 代理之间的关系使用 prov: actedOnBehalfOf
属性表示; 代理与实体存在的关联使用属性 prov: wasAttributedTo
表达; 代理与活动的关联使用属性prov: wasAssociatedWith
表达。这三个比较核心的概念及其相互关系如下图所示。可能不太准确的总结:
Prov可以理解成一个具有复杂关系的有向图。
Graphviz由贝尔实验室开发,通过特定dot脚本绘制图形,并执行布局引擎来完成自动布局。它的主要特点:
自动排版,布局美观
具备多种可供调整的有用特性包括颜色、字体、表格节点布局、线条样式、超连接、自定义形状等;
生成图片支持bmp、emf、eps、gif、jpg、pdf、png、ps、svg、tif等多种格式;支持windows、linux、mac等多种操作系统。
那么问题又来了
dot
其实就是一种脚本语言,可以定义图形中的大多数属性。所以总结一下,Grapviz利用.dot文件生成流程图。
这里我分享一个我觉得比较好用的版本
链接:https://pan.baidu.com/s/1lLfGNnfogipKSF_szo89MA
提取码:8ytg
注意,安装完成后,还需要配置环境变量,你才可以在idea中调用它,教程如下。
接下来我们试着用Graphviz简单画个图。
新建一个文件,写一段最简单的dot语法并且保存(这里保存成.gv
,但也可以保存为.dot
文件)。
你可能会觉得每次生成图片再去看我生成的图片有没有问题很浪费时间,我们可不可以即时生成图片呢?
当然可以!
相信大家都有安装Visual Studio Code,这里有一个非常好用的插件Graphviz Preview,当你安装完成后,点击我标注的这个放大镜符号就可以实时看到我们生成的图片了,非常方便。然后右上角Export
可以用来导出我们的图片。
之前我们都是用英文画图,但在中文作图时会出现乱码,在即时显示是不会有乱码,但是一旦导出就会有乱码。我们来看下面这个案例。
digraph G {
node[fontname="Microsoft Yahei"];edge[fontname="Microsoft Yahei"]
rankdir = RL
大哥 -> 小弟 [label = "认识"]
大哥 -> 小小弟 [label = "好朋友"]
小弟 -> 飞镖客 [label = "结仇"]
飞镖客 -> 大魔王 [label = "结义"]
}
走到这里,咱们其实已经掌握了生成Prov图的基本步骤了,但是如何将他整合到我们的idea项目呢?
这个前辈代码写的非常好,将关于grapviz和.dot的操作封装,然后提供我们使用,并且写了博客介绍如何使用。
但问题是太久没更新,中文支持性差,稳定性不好(在项目中出现了有时成功有时失败的情况)。
研究了很久才学会如何将这个嵌入我们的项目。
dot -o 6.png -Tpng xx.dot
语句来生成图片,相当于调用了系统底层的指令来生成图片。前辈的源码写的很漂亮,但是中文问题就比较难解决了。
runtime.exec
这种系统级别的指令在win和linux下的指令有所不同, 在后期部署和测试阶段会造成诸多不便。//需要插入的依赖
<dependency>
<groupId>org.openprovenance.prov</groupId>
<artifactId>prov-model</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.openprovenance.prov</groupId>
<artifactId>prov-interop</artifactId>
<version>0.7.0</version>
</dependency>
public class Little {
public static final String PROVBOOK_NS = "http://www.provbook.org";
public static final String PROVBOOK_PREFIX = "provbook";
public static final String JIM_PREFIX = "jim";
public static final String JIM_NS = "http://www.cs.rpi.edu/~hendler/";
private final ProvFactory pFactory;
private final Namespace ns;
public Little(ProvFactory pFactory) {
this.pFactory = pFactory;
ns=new Namespace();
ns.addKnownNamespaces();
ns.register(PROVBOOK_PREFIX, PROVBOOK_NS);
ns.register(JIM_PREFIX, JIM_NS);
}
public QualifiedName qn(String n) {
return ns.qualifiedName(PROVBOOK_PREFIX, n, pFactory);
}
public Document makeDocument() {//生成既定关系
Entity quote = pFactory.newEntity(qn("a-little-provenance-goes-a-long-way"));
quote.setValue(pFactory.newValue("A little provenance goes a long way",
pFactory.getName().XSD_STRING));
Entity original = pFactory.newEntity(ns.qualifiedName(JIM_PREFIX,"LittleSemanticsWeb.html",pFactory));
Agent paul = pFactory.newAgent(qn("阿珍"), "Paul Groth");
Agent luc = pFactory.newAgent(qn("阿强"), "Luc Moreau");
WasAttributedTo attr1 = pFactory.newWasAttributedTo(null,
quote.getId(),
paul.getId());
WasAttributedTo attr2 = pFactory.newWasAttributedTo(null,
quote.getId(),
luc.getId());
WasDerivedFrom wdf = pFactory.newWasDerivedFrom(quote.getId(),
original.getId());
Document document = pFactory.newDocument();
document.getStatementOrBundle()
.addAll(Arrays.asList(new StatementOrBundle[] { quote,
paul,
luc,
attr1,
attr2,
original,
wdf }));
document.setNamespace(ns);
return document;
}
public void doConversions(Document document, String file) {
InteropFramework intF=new InteropFramework();
intF.writeDocument(file, document);
intF.writeDocument(System.out, ProvFormat.XML, document);
}
public void closingBanner() {
System.out.println("");
System.out.println("*************************");
}
public void openingBanner() {
System.out.println("*************************");
System.out.println("* Converting document ");
System.out.println("*************************");
}
public static void main(String [] args) throws Exception {
// if (args.length!=1) throw new UnsupportedOperationException("main to be called with filename");
String file="2.dot";//(本来是没有这个文件的)通过设定关系->->产生的文件,这里指定生成一个.dot文件
Little little=new Little(InteropFramework.newXMLProvFactory());
little.openingBanner();
/**
* 通过手动设定的方式生成关系
*/
Document document = little.makeDocument();//生成既定关系
little.doConversions(document, file);//生成可视化图片
little.closingBanner();
/**
* 想通过指令将3.dot生成为图片
*/
File file1 = new File("E:\\sufering\\Java\\ProvToolBoxDemo\\3.dot");
/**
* 先修改文件
* 1.修改字体
* 2.
*/
little.insertStringInFile(file1,2,"node[fontname=\"Microsoft Yahei\"];edge[fontname=\"Microsoft Yahei\"];");
/**
* 执行
*/
Runtime runtime = Runtime.getRuntime();
// runtime.exec("dot -o 6.png" + " -Tpng " + "E:\\sufering\\Java\\ProvToolBoxDemo\\3.dot");
runtime.exec("dot -o 6.png" + " -Tpng " + file1);
}
public void insertStringInFile(File inFile, int lineno, String lineToBeInserted) throws Exception {//在某个文件里插入一行数据
// 临时文件
File outFile = File.createTempFile("name", ".tmp");
// 输入
FileInputStream fis = new FileInputStream(inFile);
BufferedReader in = new BufferedReader(new InputStreamReader(fis));
// 输出
FileOutputStream fos = new FileOutputStream(outFile);
PrintWriter out = new PrintWriter(fos);
// 保存一行数据
String thisLine;
// 行号从1开始
int i = 1;
while ((thisLine = in.readLine()) != null) {
// 如果行号等于目标行,则输出要插入的数据
if (i == lineno) {
out.println(lineToBeInserted);
}
// 输出读取到的数据
out.println(thisLine);
// 行号增加
i++;
}
out.flush();
out.close();
in.close();
// 删除原始文件
inFile.delete();
// 把临时文件改名为原文件名
outFile.renameTo(inFile);
}
}
同样是前人的成果,不行陈功率、稳定性高,并且原生解决了中文问题。
先上GitHub地址:https://github.com/nidi3/graphviz-java
我们可以先把前人的项目下载到本地研究一下
它给出了8个案例,我们对比着图片和代码就能知道如何去绘制我们的图片了(也可以直接去看上面GitHub链接)!这个请大家根据需求自己研究。
随便说说我是怎么将它插入到我的项目中的。
<dependency>
<groupId>guru.nidi</groupId>
<artifactId>graphviz-java</artifactId>
<version>0.18.1</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>20.0.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
//prov引擎必不可少
Graphviz.useEngine(new GraphvizV8Engine());
Graphviz.fromString("graph {a--b}").render(SVG).toString();
//需要使用的节点
//直接通过数据库查询出来我们需要填充到框架中的数据
//假如这里查到了A B C D E
//构建节点和连线(构建框架)
final Node
node1 = node(A),
node2 = node(B),
node3 = node(C);
final Graph g = graph("ex1").directed().with(
node1.link(to(node2).with(Label.of(D))),
node2.link(to(node3).with(Label.of(E)))
);
Graphviz.fromGraph(g).width(900).render(Format.PNG).toFile(new File("src/main/resources/prov/ex1.png"));
//高并发情况下可能存在的问题
//这里使用Thread.sleep是迫不得已而为之,因为生成图片需要 一段缓存时间。
//假如我们使用异步任务,只能判定这句话是否执行,而不能判断图片是否生成完毕。
//所以只能假定一个生成时间为3秒。
//而new ClassPathResource可以读取相对位置路径 并自动转变为绝对位置路径,假如3秒后没读到图片,就会爆出异常,我们catch异常,再睡眠一段时间,一般都没问题了。
Thread.sleep(3);
ClassPathResource classPathResource = null;
try {
classPathResource = new ClassPathResource("prov/" + "ex1.png");
} catch (Exception e) {
Thread.sleep(2);
classPathResource = new ClassPathResource("prov/" + "ex1.png");
}
//通过ClassPathResource读取到文件后,上传至minio服务器,并可以返回给前端图片的下载地址(网上有很多这样的minio逻辑,请大家自行查阅)
File file = classPathResource.getFile();
return minioService.uploadFile(getMultipartFile(file));
个人认为这是当前利用java生成prov图片的最佳方法了,但在高并发情况下任然有可能发生问题,希望能在未来的学习中解决这个问题。
如果有大佬路过,也麻烦帮忙分析、解答一下这个问题。