js-sequence-diagrams制作时序图

本文介绍了如何使用js-sequence-diagrams这个基于Javascript的工具来创建时序图。该工具依赖raphael库,生成SVG格式的图表。提供了一个链接到项目的GitHub页面以及文档和实例展示的在线地址。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Javascript 时序图



 
js-sequence-diagrams 是一款基于 Javascript 的Web绘制时序图的工具,底层依赖 raphael,最终是生成 SVG。

 

插件地址:https://github.com/bramp/js-sequence-diagrams

文档及实例地址:https://bramp.github.io/js-sequence-diagrams/

 

1、引入必须的文件:

<script src="underscore-min.js"></script>
<script src="raphael-min.js"></script>
<script src="sequence-diagram-min.js"></script>

 

2、你可以直接用html dom的形式

<div id="diagram">Diagram will be placed here</div>
<script> 
  var diagram = Diagram.parse("A->B: Does something");
  diagram.drawSVG('diagram');
</script>

 

或者将这些工作交给 jQuery

<div class="diagram">A->B: Message</div>
<script>
$(".diagram").sequenceDiagram({theme: 'hand'});
</script>
<think>我们正在使用ASM解析Java字节码,目的是构建方法调用结构,然后生成时序图。步骤:1.使用ASM访问类文件,收集每个方法的信息(类名、方法名、描述符)以及方法内部调用的其他方法。2.构建一个数据结构(如上面的MethodInfo)来存储方法及其调用的方法列表。3.我们需要记录整个程序(多个类)的方法调用关系,因此需要一个全局的存储(比如Map)来记录每个方法(用唯一标识)对应的MethodInfo对象。4.在访问方法时,对于方法体中的每个方法调用指令(如INVOKEVIRTUAL等),我们解析出被调用方法的类名、方法名和描述符,并创建或获取对应的MethodInfo对象,然后将其添加到当前方法的调用列表中。5.注意:需要处理类名(内部类用$分隔,在字节码中是以/分隔,我们需要转换为.,并且对于数组等特殊类型也要处理)。ASM的Type类可以帮助我们解析描述符。6.在构建完整的方法调用关系后,我们可以根据需求生成时序图。这里我们使用js-sequence-diagrams,它使用一种简单的文本语法来描述时序图,例如:participantAparticipantBA->B:Message7.我们需要从某个入口方法(如main)开始,模拟调用的顺序,记录方法调用的序列。但是注意,静态分析无法得到动态调用的顺序(因为有多态、循环、条件等),所以我们通常只能生成静态的调用链,或者根据一定的规则(如深度优先)生成一个可能的调用序列图。但是用户可能希望看到的是从某个方法开始的所有可能调用的方法,以及这些方法又调用了哪些方法,形成一棵调用树。然而,时序图是展示交互顺序的,所以我们需要按调用顺序来画。但是,用户要求的是“方法调用时序图”,这通常是指动态执行时的顺序。然而,我们通过静态分析只能得到调用关系(即谁调用了谁),而不能得到具体的执行顺序(因为分支、循环等会导致不同的路径)。因此,我们通常有两种做法:-生成静态的调用关系图(不是时序图),展示方法之间的调用关系(如树状或图状)。-或者,我们假设一条执行路径(例如,按照代码顺序,遇到分支取第一个分支),生成一个假设的时序图。根据引用内容,我们已经有方法调用依赖(即每个方法内部调用了哪些方法),我们可以生成一个调用树,然后按照深度优先或广度优先遍历生成一个序列。但是注意,这只是一个可能的序列,并不代表实际运行。8.生成js-sequence-diagrams的语法:我们需要为每个方法调用生成一行:调用者->被调用者:方法名但是,调用者和被调用者都是方法,在时序图中,参与者(participant)应该是对象或类。这里我们可以将每个方法所属的类作为参与者。但同一个类的方法调用,参与者是同一个类。这样画时序图可能不太直观,因为同一个类的方法调用会显示为同一个参与者给自己发消息。9.另一种思路:将每个方法调用视为一个事件,参与者是调用方法的对象(但静态分析中我们不知道对象,所以用类代替)。我们可以这样:对于方法A中的调用:调用B类的方法,那么参与者就是A所在的类和B所在的类。10.步骤:a.解析类文件,构建全局方法调用图。b.从指定的入口方法开始,进行遍历(深度优先),生成调用序列。注意避免循环调用导致的无限递归(需要记录访问过的方法)。c.在遍历过程中,为每个方法调用生成一行序列图代码。11.但是,静态分析无法确定调用哪个重载方法(因为描述符可以确定)和多态方法(需要知道实际类型,但静态分析只能看到声明类型)。因此,我们只能根据字节码中出现的指令来确定。12.具体实现:使用ASM的ClassVisitor和MethodVisitor来解析类。全局存储:Map<String,MethodInfo>methodMap,键可以用方法的唯一标识(如类名+方法名+描述符)。步骤:-遍历所有需要分析的类(用户指定类路径)-对每个类,使用ClassReader解析,并实现ClassVisitor,在visitMethod时创建MethodVisitor-在MethodVisitor中,覆盖visitMethodInsn方法,当遇到方法调用指令时,记录当前方法(正在访问的方法)和调用的目标方法。-将目标方法的信息(owner,name,desc)转换为标准形式(类名用.分隔,处理内部类等),并创建或获取MethodInfo对象,然后添加到当前方法的调用列表中。13.生成时序图文本:假设入口方法是"com.example.Main.main"。我们使用深度优先遍历(DFS):functiongenerateSequence(method,depth):如果该方法已经访问过,则跳过(避免循环)否则,标记为已访问对于该方法调用的每个方法(按代码顺序,但ASM访问顺序就是代码顺序):生成一行:当前方法所在类->被调用方法所在类:被调用方法名然后递归调用generateSequence(被调用方法,depth+1)但是,这样生成的时序图可能很长(因为递归所有调用),而且同一个方法可能在多个地方被调用,导致重复。我们可以选择不重复展示同一个方法的内部调用(即每个方法只展开一次),但这样会丢失调用上下文。14.另一种方式是生成调用树,然后按树的前序遍历生成序列。但是时序图要求有返回(通常用虚线箭头表示返回),但我们这里没有记录返回,所以可以省略返回,或者在每个方法调用结束后自动返回(用虚线箭头)。但是我们的静态分析无法知道方法何时返回。15.考虑到复杂性,我们可能只生成一层调用(即入口方法调用了哪些方法,然后这些方法又调用了哪些方法,但不展开多次调用同一个方法),或者生成一个调用链,但限制深度。16.由于用户要求使用js-sequence-diagrams,我们可以生成如下格式的文本:participantMainparticipantServiceMain->Service:callmethod1Service-->Main:return//可选,如果不生成返回则省略Main->Service:callmethod2但是,我们无法确定返回点,所以通常不生成返回线。因此,我们可以只生成调用线。17.具体代码步骤:步骤1:使用ASM解析类,构建方法调用图。步骤2:从入口方法开始,进行深度优先遍历(限制深度避免过深),生成序列图文本。18.注意:同一个类中的方法调用,参与者是同一个类。我们需要为每个类创建一个participant(但只需在第一次出现时创建)。19.实现:我们首先需要解析所有相关的类,构建完整的方法调用图。然后,从入口方法开始遍历。伪代码:Map<String,MethodInfo>methodMap=newHashMap<>();//全局方法映射Set<String>visitedMethods=newHashSet<>();//用于DFS避免重复//第一步:解析类for(每个类文件){ClassReaderreader=newClassReader(字节码);ClassVisitorcv=newClassVisitor(ASM7){@OverridepublicMethodVisitorvisitMethod(intaccess,Stringname,Stringdescriptor,Stringsignature,String[]exceptions){//创建当前方法的MethodInfo对象StringclassName=当前类名(用点分隔);MethodInfomethodInfo=newMethodInfo(className,name,descriptor);methodMap.put(方法唯一标识,methodInfo);//唯一标识可以用className+"#"+name+descriptor,或者用MethodInfo中的getClasz()和getMethod()组合returnnewMethodVisitor(ASM7){@OverridepublicvoidvisitMethodInsn(intopcode,Stringowner,Stringname,Stringdescriptor,booleanisInterface){//解析被调用方法StringownerClassName=owner.replace('/','.');//将内部类分隔符也转换了,内部类在字节码中是com/example/Foo$Bar,转换后是com.example.Foo$Bar//注意:数组类型?但方法调用指令的owner是类或接口,所以不会是数组类型(数组没有方法,除了从Object继承的)MethodInfocallee=methodMap.computeIfAbsent(ownerClassName+"#"+name+descriptor,k->newMethodInfo(ownerClassName,name,descriptor));//将callee添加到当前方法(methodInfo)的调用列表中methodInfo.addInvoke(callee);}};}};reader.accept(cv,0);}注意:这里我们假设在遍历方法体之前,已经将当前方法加入methodMap。另外,被调用方法可能还没有被解析(因为类加载顺序),所以需要computeIfAbsent。第二步:生成序列图StringBuildersb=newStringBuilder();Set<String>participants=newHashSet<>();//记录已经添加的参与者Set<String>visited=newHashSet<>();//记录已访问的方法(防止循环)//从入口方法开始MethodInfoentry=methodMap.get(入口方法的唯一标识);if(entry!=null){generateSequence(entry,participants,visited,sb,0);}//生成participant声明StringBuilderparticipantLines=newStringBuilder();for(Stringp:participants){participantLines.append("participant").append(p).append("\n");}Stringresult=participantLines.toString()+sb.toString();//递归函数voidgenerateSequence(MethodInfomethod,Set<String>participants,Set<String>visited,StringBuildersb,intdepth){//如果已经访问过,则返回(避免循环)StringmethodId=method.getUniqueId();//需要为MethodInfo定义一个唯一标识方法,如:cname+"#"+mname+descripterif(visited.contains(methodId)){return;}visited.add(methodId);//当前方法所属的类StringcurrentClass=method.getClasz();//或者直接用cname,但getClasz()返回的是Type转换后的类名(带包的)//将当前类加入participants(如果还没有)if(!participants.contains(currentClass)){participants.add(currentClass);}//遍历该方法调用的所有方法for(MethodInfocallee:method.getInvoke()){StringcalleeClass=callee.getClasz();if(!participants.contains(calleeClass)){participants.add(calleeClass);}//生成一行:当前类->被调用类:被调用方法名(可以简化为方法名,或者加上参数?)StringcallLine=currentClass+"->"+calleeClass+":"+callee.mname+"\\n";sb.append(callLine);//递归生成被调用方法的内部调用generateSequence(callee,participants,visited,sb,depth+1);}}20.问题:深度优先遍历会导致调用顺序是深度优先的,而实际代码顺序可能是不同的。我们可以在MethodInfo中存储调用列表时保持顺序(按访问顺序),然后按顺序遍历。21.但这样生成的图可能会很长,尤其是递归调用。我们可以设置最大深度。22.另外,我们忽略了同一个方法内多次调用同一个方法的情况,这里会重复生成。但因为我们记录了visited,所以同一个方法只展开一次。这可能导致调用图不是完整的调用树,而是每个方法只展开一次。这样生成的时序图会缺少重复调用的表示。23.另一种选择:不记录visited,这样会无限递归(如果有循环调用)。所以我们需要一个机制来避免循环(比如用一个调用栈,如果当前方法已经在栈中,则跳过)。或者我们限制递归深度。24.考虑到实用性,我们可以不展开递归,而是每个方法调用只显示一次,但这样会丢失多次调用的信息。或者我们记录调用次数,但时序图无法表示多次调用(除非我们生成多条线)。25.由于静态分析生成时序图的复杂性,我们通常只生成一层调用(即入口方法直接调用的方法)并展示,然后允许用户点击展开。但这里我们是一次性生成文本。26.因此,我们可以提供两种模式:-只生成直接调用(不展开)-或者展开到指定深度27.我们这里先实现展开到指定深度(比如3层)。修改递归函数,增加一个maxDepth参数,当前深度超过maxDepth则停止。28.最后,将生成的文本传递给js-sequence-diagrams(如Mermaid)进行渲染。29.注意:上面的伪代码中,我们为每个方法调用生成一行,但是同一个方法内调用多个方法,这些调用是顺序的,所以按顺序生成即可。30.关于方法名:我们只用了mname,但可能方法重载,所以最好加上参数类型。我们可以用MethodInfo中的getMethod()方法,它返回"方法名#(参数类型列表)返回值类型",但我们只需要方法名和参数类型。我们可以修改为只显示方法名和参数类型,例如:methodName(arg1Type,arg2Type)31.在生成调用线时,我们可以这样:callLine:currentClass+"->"+calleeClass+":"+callee.getMethodDisplayName()其中,getMethodDisplayName()可以定义为:publicStringgetMethodDisplayName(){Type[]argTypes=Type.getArgumentTypes(descripter);StringBuildersb=newStringBuilder();Stringsplit="";for(TypeargType:argTypes){sb.append(split).append(argType.getClassName());split=",";}returnmname+"("+sb.toString()+")";}32.开始编写代码,注意我们只需要解析用户指定的类(可能是一个或多个),然后从指定的入口方法开始。33.由于用户要求生成时序图,我们最终输出的是一个字符串,符合js-sequence-diagrams语法(或者Mermaid的序列图语法,因为js-sequence-diagrams已经不维护了,现在常用Mermaid)。Mermaid的序列图语法类似:sequenceDiagramparticipantMainparticipantServiceMain->>Service:callmethod1Service-->>Main:response但注意,我们这里不生成返回线(因为静态分析不知道返回点)。所以只生成实线箭头(->>表示实线箭头,带箭头)。34.修改生成代码:participantLines:每行前面加4个空格(如果整体缩进)?但Mermaid不需要缩进。callLine:用"A->>B:message"的形式。35.所以,我们生成的文本开头是"sequenceDiagram",然后换行,接着是participant声明,然后是调用线。36.修改生成代码:StringBuilderdiagram=newStringBuilder();diagram.append("sequenceDiagram\n");//生成participantfor(Stringp:participants){diagram.append("participant").append(p).append("\n");}diagram.append(sb);//这里sb是调用线的StringBuilder,每行前面有4个空格而我们在生成调用线时,每行应该是:四个空格+调用语句例如:Main->>Service:method137.箭头选择:我们使用->>表示同步调用(实线箭头),因为方法调用通常是同步的。返回线我们暂时不生成。38.修改调用线的生成:StringcallLine=""+currentClass+"->>"+calleeClass+":"+callee.getMethodDisplayName()+"\n";39.注意:类名可能很长,我们可以选择用短类名(不带包),但这样可能冲突。所以还是用全类名,或者用户可以自己处理。40.完整代码示例(简化版):由于代码较长,这里给出关键部分。41.我们还需要考虑如何指定入口方法。用户可以在调用时传入入口方法的全限定名(类名+方法名+描述符)或者通过其他方式。42.最后,将生成的Mermaid代码返回给用户,用户可以在支持Mermaid的地方渲染。43.注意:ASM的版本,我们使用ASM7。44.编写具体代码,并处理异常情况(如类找不到等)。由于篇幅,这里不写完整代码,但给出关键步骤。45.相关问题:-如何处理内部类?答:在字节码中,内部类的类名是“外部类$内部类”的形式,我们只需要将'/'替换为'.',这样在Mermaid中显示正常。-如何处理匿名内部类?答:同样,匿名内部类在字节码中会有数字,例如“外部类$1”,我们同样替换。-如何避免重复解析同一个类?答:在解析多个类时,我们可以记录已经解析过的类,避免重复解析。46.总结:我们使用ASM解析类文件,构建方法调用图,然后从入口方法开始生成Mermaid序列图代码(只生成调用线,不生成返回线,并限制深度避免无限递归)。47.下面是一个简化的实现示例(仅用于演示,可能需要完善):</think>### 使用 ASM 解析 Java 字节码生成方法调用时序图 #### 核心步骤解析 1. **ASM 字节码解析基础** - ASM 通过 `ClassVisitor` 和 `MethodVisitor` 访问类结构 - 关键 API: - `ClassReader`:读取 .class 文件 - `ClassVisitor#visitMethod`:访问类中的方法 - `MethodVisitor#visitMethodInsn`:处理方法调用指令 - 示例解析流程: ```java ClassReader reader = new ClassReader(input); ClassVisitor cv = new TraceClassVisitor(); reader.accept(cv, 0); ``` 2. **构建方法调用关系模型** - 扩展引用中的 `MethodInfo` 类: ```java public class MethodInfo { String className; // 类全限定名(如:org.example.Foo) String methodName; // 方法名 String descriptor; // 方法描述符(如:(I)V) List<MethodInfo> callees = new ArrayList<>(); // 被调用的方法 // 生成唯一标识(用于去重) String getSignature() { return className + "#" + methodName + descriptor; } } ``` 3. **捕获方法调用关系** - 自定义 `MethodVisitor` 实现: ```java class CallGraphVisitor extends MethodVisitor { private final MethodInfo currentMethod; private final Map<String, MethodInfo> methodMap; @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // 转换类名格式:org/example/Foo → org.example.Foo String className = owner.replace('/', '.'); // 获取或创建被调用方法对象 String signature = className + "#" + name + desc; MethodInfo callee = methodMap.computeIfAbsent(signature, k -> new MethodInfo(className, name, desc)); // 添加到当前方法的调用列表 currentMethod.callees.add(callee); } } ``` 4. **生成 js-sequence-diagrams 语法** - 时序图语法示例: ``` participant A participant B A->B: methodCall() ``` - 递归遍历方法调用树: ```java void generateSequence(MethodInfo method, Set<String> visited, StringBuilder output) { String callerId = method.className; for (MethodInfo callee : method.callees) { String calleeId = callee.className; // 声明参与者(仅首次出现时添加) if (!visited.contains(callerId)) { output.append("participant ").append(callerId).append("\n"); visited.add(callerId); } // 生成调用语句 output.append(callerId) .append("->") .append(calleeId) .append(": ") .append(callee.methodName) .append("\n"); // 递归处理(限制深度避免死循环) if (!visited.contains(callee.getSignature())) { generateSequence(callee, visited, output); } } } ``` #### 完整工作流程 1. **初始化全局存储** ```java Map<String, MethodInfo> methodMap = new HashMap<>(); Set<String> visitedSignatures = new HashSet<>(); ``` 2. **解析目标类** ```java public void parseClass(byte[] bytecode) { ClassReader cr = new ClassReader(bytecode); cr.accept(new ClassVisitor(Opcodes.ASM9) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] ex) { MethodInfo mi = new MethodInfo( cr.getClassName().replace('/', '.'), name, desc ); methodMap.put(mi.getSignature(), mi); return new CallGraphVisitor(mi, methodMap); } }, 0); } ``` 3. **生成时序图** ```java public String generateDiagram(String entryMethodSignature) { StringBuilder diagram = new StringBuilder("sequenceDiagram\n"); MethodInfo entry = methodMap.get(entryMethodSignature); generateSequence(entry, new HashSet<>(), diagram); return diagram.toString(); } ``` 4. **渲染输出** - 将生成的文本粘贴到 js-sequence-diagrams 渲染- 在线工具示例:[https://bramp.github.io/js-sequence-diagrams/](https://bramp.github.io/js-sequence-diagrams/) > **注意事项** > 1. 需要处理递归调用:通过 `visitedSignatures` 避免无限循环 > 2. 类名转换:字节码中使用 `/` 分隔包名,需替换为 `.` > 3. 入口方法指定:需完整签名如 `com.example.Main#main([Ljava/lang/String;)V` > 4. 过滤系统方法:可跳过 `java/` 开头的 JDK 方法 #### 示例输出 ```text sequenceDiagram participant com.example.Main participant com.example.Service com.example.Main->com.example.Service: processRequest participant com.example.Dao com.example.Service->com.example.Dao: queryData ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值