利用JAVA探针(Java agent)与 Byte Buddy 绘制动态函数调用图 (dynamic call graph)

利用JAVA探针(Java agent)与 Byte Buddy 绘制动态函数调用图 (dynamic call graph)

  • 先跑起来
  • (非常)简单的背景
  • 代码的一些实现

最近在学习spring的源码,感觉有个函数调用图就好了,于是研究了下怎么自动生成,并没有找到开箱即用的工具。尝试过使用cglib库来实现,发现cglib 不能拦截super调用和private方法。后来发现JAVA探针与Byte Buddy包可以简单的实现动态函数调用图的绘制。不过悲催的是即便只看org.springframework下的类,生成的call graph也过于复杂,这里把方法简单记录一下。

  • 参考
    1. https://www.youtube.com/watch?v=tlcF8awgUEE&t=2871s
    2. https://bytebuddy.net/#/
    3. https://github.com/shelajev/callspy
    4. https://github.com/MingxuanHu/callspy

先跑起来

  • 下载项目
    $ git clone https://github.com/MingxuanHu/callspy.git
    
    1. 这是个gradle项目。
    2. 这个项目是我从上面的参考3 fork 过来的,参考1是介绍参考3的视频。我主要是删除了多余的例子,增加了一些控制和配置方法,并增加将生成的文本形式的call graph tree解析后再生成Gephi可导入的文件。
  • 进入项目根目录,并使用gradle编译项目
    $ cd callspy/
    callspy$ gradle build
    
    1. 我用的是Gradle 3.4.1,版本应该不太老都行。
  • 运行java,指定java agent和要生成动态函数调用图的主类
    java -javaagent:build/libs/callspy-0.1.jar com.zeroturnaround.callspy.CallspyDrawer -f call-tree.txt -g gephi.gexf -c org.example.simple.Class1
    
    1. Java agent通过-javaagent:build/libs/callspy-0.1.jar来指定。
    2. 调用的方法为com.zeroturnaround.callspy.CallspyDrawer
    3. -f call-tree.txt 是说将call graph tree导出到文件call-tree.txt中。
    4. -g gephi.gexf 是说解析call graph tree并生成Gephi文件gephi.gexf。
    5. -c org.example.simple.Class1 是说这里生成org.example.simple.Class1的主方法的call graph。
    6. 完整的调用说明如下:
      usage: java -javaagent:build/libs/callspy-0.1.jar
                  com.zeroturnaround.callspy.CallspyDrawer [-m] [-o] [-l
                  manual/in.txt manual/out.txt] [-f output/call/tree.txt] [-g
                  output/Gephi/graph.xml] -c main.class.name [-a
                  arg1,arg2,arg3,...]
       -a,--arguments    The argument(s) of the main class if it has any,
                              separated by comma without space.
       -c,--class        The main class of which a call graph will be
                              generated.
       -f,--file         Output the call tree to file.
       -g,--gephi        Use the call tree to generate a Gephi xml file,
                              must be used together with -f.
       -l,--load    Load the manual choosing process from a file (can
                              be an empty file), and write your choosing history
                              to another file.
       -m,--manual            Manually choose which method to be logged in call
                              graph.
       -o,--console           Set if you want the call tree to be printed to
                              console.
      
  • 关于生成的文件:call-tree.txt
    org.example.simple.Class1.main([Ljava.lang.String;): void
     org.example.simple.Class1.(): void
     org.example.simple.Class1.method11(): void
      org.example.simple.Class1.method12(): void
       org.example.simple.Class2.(): void
       org.example.simple.Class2.method21(int,boolean,java.lang.String): void
        org.example.simple.Class2.method22(): void
         org.example.simple.Class1.(): void
         org.example.simple.Class1.method13(): void
      org.example.simple.Class1.method14(): void
       org.example.simple.package1.Class4.(): void
        org.example.simple.package1.Class3.(): void
       org.example.simple.package1.Class4.method41(): void
        org.example.simple.package1.Class4.method42(): void
         org.example.simple.package1.Class4.method31(): void
        org.example.simple.package1.Class4.method43(): void
         org.example.simple.package1.Class4.method32(): void
        org.example.simple.package1.Class4.method44(): void
         org.example.simple.package1.Class3.method32(): void
    
    1. Class1的位置在callspy/src/main/java/org/example/simple/Class1.java,是一个简单的toy example。
    2. 这就是整个函数的动态调用过程,是一个树结构。
  • 关于生成的文件:gephi.gexf
    1. 实际上是一个xml文件,可以用Gephi打开。
    2. 打开后需要手动调整节点的位置,我手残的调整了一下,效果如下:
      利用JAVA探针(Java agent)与 Byte Buddy 绘制动态函数调用图 (dynamic call graph)_第1张图片
      从上图可以更加直观的看到整个Class1主方法的动态调用过程。
  • 其他说明
  1. 可以用-m来指定手动选择要记录在call graph中的方法,当指定为不记录时,所有该方法下的子方法都会被忽略,这比较适用于大型项目,比如spring IoC容器的启动。
  2. 如果要生成自己程序的call graph,最直接的方式是在org.example包下写你的代码,然后设置-c参数为你的主类名即可。
  3. 如果是第三方的库,可以用gradle载入,具体例子参考org.example.spring.ioc.DemoFileSystemXmlApplicationContext,但是要修改callspy/config/callspy-drawer.yml:在startWith:下添加你想调用的第三方库,这其实是为了避免生成太多不必要的调用,比如java下的一些方法。其他参数类似设置。
  4. 如果想把自己的程序放在其他地方,或者已经有编译好的jar包,同样先修改callspy/config/callspy-drawer.yml,然后在启动java时把jar包的包路径用参数加到class path中,其他参数类似,应该可以。
  5. 最新的更新请参考:https://github.com/MingxuanHu/callspy/blob/master/README.md 。增加了将手动选择过程输出成文件,并可载入选择过程的功能。
  6. 没有验证多线程情况,肯定会乱。

(非常)简单的背景

  • Java 探针 (Java agent)
    相当于连接你的Java byte code和JVM之间的一层,我也是刚接触,还请自行查询。推荐观看参考1,如果有条件。贴个图意思一下:
    利用JAVA探针(Java agent)与 Byte Buddy 绘制动态函数调用图 (dynamic call graph)_第2张图片
    出处: 参考1(https://www.youtube.com/watch?v=tlcF8awgUEE&t=2871s )
  • Byte Buddy
    Byte Buddy是一个不需要编译器就可以在Java应用的运行时刻创建或修改Java类的库。这里主要用AgentBuilder类来创建一个Java agent。

代码的一些实现

  • callspy/build.gradle
    manifest {
            attributes(
                    'Premain-Class': 'com.zeroturnaround.callspy.Agent',
                    'Agent-Class': 'com.zeroturnaround.callspy.Agent',
                    'Can-Redefine-Classes': 'true',
                    'Can-Retransform-Classes': 'true',
                    'Can-Set-Native-Method-Prefix': 'true',
                    'Implementation-Title': "CallSpy",
                    'Implementation-Version': rootProject.version,
                    'Built-By': System.getProperty('user.name'),
                    'Built-Date': new Date(),
                    'Built-JDK': System.getProperty('java.version')
            )
        }
    
    1. Java agent 需要用manifest来指定Agent-ClassPremain-Class,这个在gradle中完成。Premain-Class会在主函数执行前被调用,这样你才有机会对java byte code进行修改。
  • callspy/src/main/java/com/zeroturnaround/callspy/Agent.java
    public class Agent {
    
    	public static void premain(String args, Instrumentation instrumentation) {
    		AgentBuilder agentBuilder = createAgentBuilder();
    		agentBuilder.installOn(instrumentation);
    	}
    
    	private static AgentBuilder createAgentBuilder() {
    		return new AgentBuilder.Default()
    				.type(ElementMatchers.nameMatches(CallspyConfig.getStartWithRegex()))
    				.transform((builder, typeDescription, classLoader) ->
    						builder.visit(Advice.to(MyAdvice.class).on(ElementMatchers.any())));
    	}
    }
    
    1. 这个就是java agent,以及需要的 premain 方法。首先用createAgentBuilder()得到一个 Byte Buddy 的 AgentBuilder,然后调用installOn(instrumentation)将agent与Instrumentation绑定。
    2. createAgentBuilder()中主要用了 Byte Buddy 的 Default Agent Builder,对callspy/config/callspy-drawer.yml的修改在.type(ElementMatchers.nameMatches(...))中生效,主要逻辑在MyAdvice.class中。
    3. MyAdvice.class中用@Advice.OnMethodEnter@Advice.OnMethodExit标记了在进入方法和退出方法前,应该执行的java agent代码。逻辑很简单,就是进入方法时Stack.push(),退出时Stack.pop(),这样就生成了call tree。
  • com.zeroturnaround.callspy.gephi
    1. 生成Gephi的逻辑相对独立,在这个包内,主要是将call tree文件读入并解析成树的数据结构,再按照gephi的要求生存xml文件。

你可能感兴趣的:(java)