随着ChatGPT爆火,网络上贩卖焦虑的声音此起彼伏,我等工程师、程序员似乎马上要被一锅端起,倒入历史涡流。笔者想说,莫慌,软件开发是一门极其复杂的学问,断然不是写两行代码那么简单。产品百万千万行的代码依然需要人来读懂和维护,架构设计、模式、扩展性依然是核心命题。UML是软件设计的利器,而PlantUML是一个很好的UML创作工具。
PlantUML简介
PlantUML是一个开源的UML绘图工具,目前在GitHub上有7.5K Stars,最早于2021年10月发布第一个正式版本,历经二十多个版本迭代,目前已非常成熟。它可用于快速创建UML(统一建模语言,Unified Modeling Language)图表,生成可视模型,帮助开发者将复杂的设计概念表示出来PlantUML 最与众不同的地方在于它提供了一套简单的纯文本语法来定义UML,堪称是UML界的Markdown,起到了桥梁和规范的作用。也就是说,你可以扫描代码来生成PlantUML,也可以解析PlantUML文本来生成代码。
PlantUML开源社区也很繁荣,其结果就是它丰富的插件,可与Visual Studio Code,JetBrains,Eclipse,NetBeans等系列IDE集成,以方便创作和预览。它能生成的视图支持包括SVG,PDF,PNG,EPS和LaTeX等丰富格式,基本上应有尽有。此外,PlantUML甚至还可以支持思维导图、甘特图、JSON、YAML的可视化,把影响力扩大到了UML之外。它已成为越来越多神级工程师、架构师、项目经理的必选工具。我在微软的开发团队出品的开源作品 Hydra Lab 就采用了PlantUML进行架构设计和协议规范可视化,用起来可谓得心应手。
关于PlantUML的细节用法,官方中文文档已经说得很清楚了,本文将不再赘述,而着重将篇幅放在具体使用经验的分享上。
小试牛刀
PlantUML是一个基于Java的工具,需要Java运行环境。本文将基于其当前最新版本1.2023.1进行实战讲解。不过,最快捷的试用PlantUML语言的方法莫过于直接使用官网的服务,各位可以尝试把一下内容粘贴进输入框看效果:
以下代码呈现了类之间关系的表达,学习UML时候最容易搞混的就是各种箭头之间的区别和联系:
@startuml class-relations-1
Postman -- Postbox : associate,(关联)
Driver -- Car : drives(关联) >
Car *-- Wheel : have 4 (包含,组合) >
Car -- Person : < owns(关联)
Folder *-- 字母F : starts with (composition,组合) >
Folder o-- File : contains (aggregation,聚合) >
Department o-- Employee : contains >
@enduml
下面的代码展现了或者扩展、实现、依赖的类间关系:
@startuml class-relations-2
Cat -up-|> Animal : extends >
Dog -up-|> Animal : extends >
class Cat {
-meow()
}
class Dog {
+bark()
}
iPhone ..|> SmartPhone : realize (实现) >
DBReader ..> DB : depends on (依赖) >
@enduml
PlantUML会自动处理图表的外观和布局,开发人员可以不用在意细节,同时他也支持一定程度的自定义。他的语法中使用@startuml和@enduml作为开头和结尾的声明,此外还有@startjson和@startyaml支持JSON\YAML数据结构的可视化,@startgantt用于画甘特图,@startmindmap用于绘制思维导图,@startmath(AsciiMath)或@startlatex(JLaTeXMath)用于绘制数学公式,功能可谓十分强大。这里仅展示一些示例,不再一一赘述,官方PDF写的很清楚了。下面是一段Spring Boot配置文件的可视化:
@startyaml yaml-preview-demo
server:
port: 9886
compression:
enabled: true
min-response-size: 102400
spring:
cache:
type: ehcache
application:
name: 'my-app'
datasource:
driver-class-name: org.sqlite.JDBC
@endyaml
以下是一个活动图(activity diagram)示例:
@startuml activity-diagram-preview-demo
start
if (Graphviz installed?) then (yes)
:process all\ndiagrams;
else (no)
:process only
__sequence__ and __activity__ diagrams;
endif
stop
@enduml
更多示例可以看这里:REAL WORLD PlantUML。
实际应用
在实际的开发场景中,我们可以运用PlantUML语法规范在代码库中进行撰写设计,以文本的形式输出内容到.puml文件中,然后调用PlantUML来生成可视化的图表,从而方便用来在Markdown技术文档当中引用,支持技术评审。那么,我们是否可以自动化这一流程,形成闭环?答案当然是可以的,PlantUML提供了Java依赖库,可以在代码中直接调用。这里演示一个小小的实例,利用Gradle创建构建任务,实现UML图片生成。
首先我们在项目中添加PlantUML依赖:
implementation 'net.sourceforge.plantuml:plantuml:1.2023.1'
接着,我们在Gradle中创建一个任务,扫描特定目录下的所有puml文件,将他们交给生成逻辑(UMLImageGenerator#generateUMLImageFromFile)进行处理:
task generateUMLImage(group: 'documentation') {
doFirst {
def scanningDirList = ['agent/doc/UML']
def outputDir = new File(projectDir, 'docs/images/UML')
def generator = new UMLImageGenerator()
scanningDirList.each {
fileTree(new File(projectDir, it)).filter { it.name.endsWith(".puml") }.files.each {
generator.generateUMLImageFromFile(it.absoluteFile, outputDir)
}
}
}
}
具体的从文件生成图片的逻辑如下,这里调用的核心类就是PlantUML提供的SourceFileReader API:
import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.GeneratedImage;
import net.sourceforge.plantuml.SourceFileReader;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class UMLImageGenerator {
public void generateUMLImageFromFile(File source, File outputDir) throws IOException {
generateUMLImageFromFile(source, outputDir, false);
}
public void generateUMLImageFromFile(File source, File outputDir, boolean svg) throws IOException {
if (!source.exists()) throw new RuntimeException(source.getAbsolutePath() + " file doesn't exist");
SourceFileReader reader = svg ?
new SourceFileReader(source, outputDir, new FileFormatOption(FileFormat.SVG)) :
new SourceFileReader(source, outputDir);
List<GeneratedImage> list = reader.getGeneratedImages();
System.out.printf("Successfully generated %d UML images.\n", list.size());
}
}
与上逻辑写好之后,我们在Gradle的任务列表documentation组中就可以看到这个新的任务了:
执行这个任务,就可以看到图片图片说出到了预期的目录下。本实战案例详细代码如下:
进一步探究
我们是否有可能根据UML设计来生成代码?那么首先我们需要读取出PlantUML的解析信息,顺着SourceFileReader的API我们可以进一步找到如下调用链:
Diagram diagram = reader.getBlocks().get(0).getDiagram();
if(diagram instanceof net.sourceforge.plantuml.classdiagram.ClassDiagram){
ClassDiagram classDiagram= (ClassDiagram) diagram;
classDiagram.getLeafsvalues().forEach(leaf -> System.out.println(leaf.getCodeGetName()));
}
这里的net.sourceforge.plantuml.baraye.ILeaf类型的对象就是解析后的叶子实体了,包含了代码生成所需要的信息,可以利用他来生成一些接口代码,从而进一步实现从代码设计到初始实现的自动化。