开源一个优雅的类图生成工具

前言

首先必须声明,这不是一篇广告或者标题党。 而是我开源了一个工具,可以优雅的为Java or Kotlin 项目生成 Class Diagram

我推测列为读者会进来阅读,原因无非以下两点:

  • 获得一个生成类图的工具,并通过文章快速了解是否 方便好用
  • 了解一下我是如何折腾的

仅关心如何使用的,可以移步 使用示例

我们将按照下面的脑图顺时针展开,揭开这一工具的诞生过程

class-diagram-guide.png

主要问题与方案

背景

背景:笔者今年换了份工作,所在的公司属于 医疗器械 下的细分领域,而相比于 纯互联网行业 领域,医疗器械 领域所属的配套软件,
都有明确的文档要求,并非 可有可无 ,而且公司管理层比较重视细节(核心产品为颅内、体内植入的医疗器械,确实需要非常认真仔细)。

毋庸置疑,准确的关键的 算法流程图,时序图,组件图,状态图,类图等, 对于产品本身的维护及发展具有很大帮助!

对于研发工作者而言,高度概括流程、设计、算法等的专业工具图对工作有极大帮助。既然需要审核的文档中也需要这些内容,又对工作有帮助,何不做的好一些呢

上文提到的各类UML图中, 类图 Class-Diagram 是非常特殊的, 它表述的是 类之间的关系基于源码文件分析可以得出准确的结果
流程图时序图状态图组件图 等则不行。

问题

随着行业的发展,软件开发也演变为 以迭代的方式,依次实现最重要的功能,持续性交付,顺其自然的,我们已经不再像几十年前的前辈们那样:代码未动,文档与UML图先行。
一般概要设计后,方案可行便进行编码了。

根据我的实际情况,复杂的功能一般在草稿纸上画画草图,简单的就脑子里想想,难以留下存档

在这一工作模式下,笔者也遇到了一些问题:

  • 业务迭代后或者代码改进后,文档(uml图)未及时更新
  • 手动维护耗时耗力

如果这件事情可以交给机器来做,那显然是极好的!而让机器维护类图是最容易实现的!

综上所述:我们需要一款工具或者插件,可以直接基于源码生成类图 (或者中间产物,例如:plant-uml文件) ,能够配合其他工具链,直接进行归档。

当然,最重要是免费, 这省去了说服公司进行购买

留有类图的好处:

  • 方便向他人介绍业务和代码
  • 项目庞大或者复杂时,更容易找到需求对应的关注点,重新维护时日久远的业务时,状态来的快
  • 图比代码亲切而且保护隐私🤣

解决方案

众所周知,Intellij-Idea的官方插件可以分析出类图,但是Idea是收费软件,付费支持官方插件是一个省时省力的方案,这是个兜底方案。最终没辙时,我们再考虑它。

编码时分析

仿照官方插件的思路,基于源码文档树进行分析,在Intellij的支持下,基于 PSI和uast 即可分析出类之间的关系。这需要一定的PSI、uast知识基础。

编译时分析

在整个编译环节中,有一些切面应对特定问题,例如:“注解处理” 、 “Gradle Transformer” ,在此切面处,我们可以基于编译中间产物,间接分析出类之间的关系。

最简单的是 注解处理阶段 介入,这只需要对 Element 和 TypeMirror 有一定的知识基础即可

运行时反射分析

显然这不是一个太好的切入点,直接pass。


考虑到PSI方面的知识体系掌握地不太完善,Intellij跨越大版本时,会有较大变更,
而注解处理方面的知识还过的了关,搞个类图生成问题不大。

PS: AndroidStudio 基于Intellij核心二次开发,PSI插件跟随Idea大版本进行适配

所以最终方案为:从注解处理阶段入手,分析编译中间产物,最终生成类图


问题分治与解决

分治1 – 简化输出产物

确定了大方向之后,我们需要再思考下整个问题的方方面面。生成类图有两大问题需要解决:

  • 源码、或者编译的中间产物中分析出类关系; ps:我们已经确定了要从编译中间产物出发
  • 将类关系转变为图

显然,“开发一个用来生成图的引擎”,这件事情成本过大且没有必要。所幸的是,UML不是一个新生物,业内也有大名鼎鼎的PlantUml。

PlantUml 基于 Graphviz , Graphviz 本身使用Dot语法描述元素与元素关系,
直接使用 Graphviz 比较朴素,PlantUml通过自定义语法,使得内容可阅读性提升,且无须关注转换图片时进行各类装饰问题

于是,我们可以将问题转化为:从编译的中间产物中分析出类关系,将关系按照PlantUml语法生成puml文件,它的内容是纯文本。

分治2 – 确定分析的起始点

如果从最终结果看,我们得到的是一个 有方向的图,那么 按照图本身的起始点出发 比较符合习惯。

也就是说,我们将在起始点所对应的类上添加注解,作为注解处理的目标起始点

例如:

img.png

Cat 和 Dog 将作为起始点。

因为只需要标记类,我们约定注解:

@Target(AnnotationTarget.CLASS)
annotation class GenerateClassDiagram {
   }

在代码上,将表现为:

class Animal

@GenerateClassDiagram
class Dog : Animal()

@GenerateClassDiagram
class Cat : Animal()

在示例中,当我们处理 GenerateClassDiagram 时,可以扫描获得 Cat 以及 Dog 类对应的
javax.lang.model.element.Element 示例,下文简称 Element

几点可能存在的疑惑:

  • 为何不 “双向” 分析:继承和实现关系,双向分析会带来额外的复杂度,且在使用上规则不清晰,依赖关系难以双向分析。 但是,如果使用规则上可以做到清晰明了,这一点值得实现
  • 为何不标注在Animal上,进行反向分析: 如果高层级的类在库包中,则需要修改库包,这不利于日常管理与维护
  • 如果只标注了Cat而没有标注Dog,Dog将不会体现在图中?:是的
  • 如果全部标注了,是否产生不良影响 :不会,但是没有必要

分治3 – 确定关系的分析方法

继承&实现

因为注解的标记对象是类 或者接口,我们理应得到 TypeElement,基于 Element 的访问者模式实现,这一点并不难。

public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
   

    TypeMirror getSuperclass();

    List<? extends TypeMirror> getInterfaces();

    //其他无关代码略去
}

不言自明,我们可以通过 TypeMirror getSuperclass(); 得到继承关系,通过 List<? extends TypeMirror> getInterfaces(); 得到实现关系

注意,此处可以细分,接口和枚举仅需要分析实现关系即可,通过 Element#getKind():ElementKind 可以判断类型

依赖&关联&聚合&组合

这四个关系非常的类似但又不同,先降低复杂度,均认为是依赖关系,在后续迭代中,可以进一步增加功能,将关系细化

进一步降低复杂度,我们仅从类的属性出发,分析依赖关系,忽略掉 方法声明 (可分析)方法体 (无法分析)静态块 (无法分析) 中 所包含的关系。

public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
   

    List<? extends Element> getEnclosedElements();
    
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值