如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装

在实习公司最后接触的一个项目(有关于溯源问题)中碰到了一个问题,是在后端开发中生成一张Prov图。从概念的理解,到寻找解决方法,到最后解决问题,花了较长的时间。今天在这里总结一下。

本文按照逻辑顺序编排,也就是我解决问题的步骤,列举了多个我尝试过的方法。
所以可以跳过【Prov-ToolBox】(弃用)直接看【最终解决方案】,也就是我认为当前最佳的解决方案。

什么是Prov

我们在百度上搜,发现还搜不到prov。然后去文献数据库中找寻一些资源,从中国海洋大学刘兵,徐建良前辈的文献【基于PROV的大洋样品数据溯源】中可以发现以下解释:

  • 数据溯源是对数据来源进行追踪然后将数据的历史状态重现出来。国外的数据溯源研究较为流行,数据溯源模型也很多,较为流行的有OPM模型和PROV模型。
  • PROV是一个由W3C定义的有关溯源的标准文档集合(PROV Family)。
    • PROV作为一个抽象模型,并不针对具体的应用环境,而只是对于各领域下数据起源过程的一般化表达。
    • PROV数据模型(PROV-DM)是PROV标准家族的核心,其为溯源数据的表述提供了通用的术语概念。PROV模型作为一个上层溯源模型独立于具体领域,从抽象层次
      上描述了溯源过程,其核心包括实体( Entity)活动( Activity)代理( Agent)
    • 实体和实体间的变化和流动通过属性 prov: was-DerivedFrom 来表示; 实体和活动之间的关系有 prov: usedprov:wasGeneratedBy,分别表示实体被活动所使用和实体由活 动 产 生; 活动和活动之间利用属性 prov: wasInformedBy 表示时间次序关系; 代理之间的关系使用 prov: actedOnBehalfOf 属性表示; 代理与实体存在的关联使用属性 prov: wasAttributedTo 表达; 代理与活动的关联使用属性prov: wasAssociatedWith 表达。这三个比较核心的概念及其相互关系如下图所示。
      如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第1张图片

可能不太准确的总结:
Prov可以理解成一个具有复杂关系的有向图。

  • 如果这样理解,其实问题就化解成了如何用Java绘制一张流程图。
    • 只不过这个流程图比较复杂,里面有实体、代理、used、wasAttributedTo这些关系,我们将这些内容标注在流程图上就可以了。

Grapviz

  • Graphviz是一款非常优秀的作图软件,在查阅资料时,我发现很多场景下都采用Grapviz来绘制有向图。

什么是Graphviz

Graphviz由贝尔实验室开发,通过特定dot脚本绘制图形,并执行布局引擎来完成自动布局。它的主要特点:
自动排版,布局美观
具备多种可供调整的有用特性包括颜色、字体、表格节点布局、线条样式、超连接、自定义形状等;
生成图片支持bmp、emf、eps、gif、jpg、pdf、png、ps、svg、tif等多种格式;支持windows、linux、mac等多种操作系统。

那么问题又来了

什么是dot

  • dot其实就是一种脚本语言,可以定义图形中的大多数属性。
  • 也就是说我们用dot语言来定义prov图中的节点及其关系。

  • 这里给几个链接,展示了一些.dot语法。
    • 链接一 ,这个是GitHub大佬总结的,很详细。
    • 链接二,几个中文案例。

所以总结一下,Grapviz利用.dot文件生成流程图。

Graphviz的安装及基本使用

  • 这里我分享一个我觉得比较好用的版本
    链接:https://pan.baidu.com/s/1lLfGNnfogipKSF_szo89MA
    提取码:8ytg

  • Graphviz的安装是傻瓜式的,一直下一步就行。然后这里有个gvedit.exe是一个可视化页面。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第2张图片

  • 注意,安装完成后,还需要配置环境变量,你才可以在idea中调用它,教程如下。

    • https://jingyan.baidu.com/article/020278115032461bcc9ce598.html

Graphviz使用示例

  • 接下来我们试着用Graphviz简单画个图。
    新建一个文件,写一段最简单的dot语法并且保存(这里保存成.gv,但也可以保存为.dot文件)。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第3张图片

  • 来到文件目录,调用cmd指令窗口,执行dot graph1.gv -Tng -o test.png生成图片。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第4张图片

  • 生成图片
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第5张图片

  • 你可能会觉得每次生成图片再去看我生成的图片有没有问题很浪费时间,我们可不可以即时生成图片呢?

  • 当然可以!

  • 相信大家都有安装Visual Studio Code,这里有一个非常好用的插件Graphviz Preview,当你安装完成后,点击我标注的这个放大镜符号就可以实时看到我们生成的图片了,非常方便。然后右上角Export可以用来导出我们的图片。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第6张图片

问题暴露-中文问题

之前我们都是用英文画图,但在中文作图时会出现乱码,在即时显示是不会有乱码,但是一旦导出就会有乱码。我们来看下面这个案例。

  • 它的实时显示是没问题的。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第7张图片
  • 但你一旦导出,发现它全是乱码
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第8张图片
  • 如何解决呢?
    在.dot文件中指定字体,像这样
digraph G {
node[fontname="Microsoft Yahei"];edge[fontname="Microsoft Yahei"]
    rankdir = RL
	大哥 -> 小弟 [label = "认识"]
	大哥 -> 小小弟 [label = "好朋友"]
	小弟 -> 飞镖客 [label = "结仇"]
    飞镖客 -> 大魔王 [label = "结义"] 
}
  • 搞定
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第9张图片

走到这里,咱们其实已经掌握了生成Prov图的基本步骤了,但是如何将他整合到我们的idea项目呢?

Prov-Toolbox

这个前辈代码写的非常好,将关于grapviz和.dot的操作封装,然后提供我们使用,并且写了博客介绍如何使用。
但问题是太久没更新,中文支持性差,稳定性不好(在项目中出现了有时成功有时失败的情况)。
研究了很久才学会如何将这个嵌入我们的项目。

  • 前辈的博客:http://lucmoreau.github.io/ProvToolbox/
  • 前辈的GitHub地址:https://github.com/lucmoreau/ProvToolbox
  • 前辈的博客中有五个案例,我觉得研究1和2就差不多够了,1是通过它封装好的方式去定义关系然后生成图。2是通过读取系统文件来生成图。其实通过翻看它的源码,发现底层就是通过dot -o 6.png -Tpng xx.dot语句来生成图片,相当于调用了系统底层的指令来生成图片。前辈的源码写的很漂亮,但是中文问题就比较难解决了。
    • 我下面提供了一种解决中文问题的办法,先指定关系,然后生成.dot文件,然后向其中插入一行指定字体的语句,然后通过runtime.exec生成图片。
    • 这个方法的实现花费了较长时间,但是最终还是弃用了,原因如下。
      • 在自己写的Demo中能百分百成功,但在idea实际项目中有一定几率失败。
      • 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链接)!这个请大家根据需求自己研究。
    如何用Java绘制Prov图|graphviz-java|Java调用Graphviz|Prov-ToolBox|Graphviz老版本|Graphviz安装_第10张图片

随便说说我是怎么将它插入到我的项目中的。

  • 在插入到自己项目中的时候,先插入依赖。
        <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图片的最佳方法了,但在高并发情况下任然有可能发生问题,希望能在未来的学习中解决这个问题。
如果有大佬路过,也麻烦帮忙分析、解答一下这个问题。

你可能感兴趣的:(重大问题攻关,java)