diff --git a/README.md b/README.md index 21d08e7e..8949dd0e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,33 @@ + java-callgraph: Java Call Graph Utilities -========================================= + +# 项目背景 + +笔者试图阅读一些开源项目的源码,发现几个问题: + +- 代码量过于庞大,不知从何看起。 +- 一行行阅读代码无法看到整体结构。 +- 下断点一行行跟踪代码费事费力。 + +此项目解决源码阅读问题,让您快速掌握软件的整体结构,帮助理解软件运行机制。 + +# 使用方法 + +先把代码拉下来,然后执行编译: + + +mvn package -DskipTests + + +这时产生一个文件`javacg-0.1-SNAPSHOT-dycg-agent.jar`,用于做JavaAgent。 + +然后运行需要研究的项目,加上如下参数: + +-javaagent:javacg-dycg-agent.jar="incl=mylib.*,mylib2.*,java.nio.*;excl=java.nio.charset.*" + +运行完成后将会得到一个Xmind文件,用Xmind打开,就可以看到运行的整个过程。 + +# 原始文档 A suite of programs for generating static and dynamic call graphs in Java. @@ -12,7 +40,7 @@ A suite of programs for generating static and dynamic call graphs in Java. #### Compile -The java-callgraph package is build with maven. Install maven and do: +The java-callgraph package is build with maven. Install maven and do: mvn install diff --git a/assembly-dyn.xml b/assembly-dyn.xml index 5c342dcf..5094d853 100644 --- a/assembly-dyn.xml +++ b/assembly-dyn.xml @@ -14,6 +14,7 @@ true javassist:javassist + com.alibaba:fastjson provided diff --git a/assembly-test.xml b/assembly-test.xml new file mode 100644 index 00000000..95cbba51 --- /dev/null +++ b/assembly-test.xml @@ -0,0 +1,21 @@ + + + test + + jar + + false + + + + + gr/gousiosg/javacg/test/*.class + + target/classes + / + + + diff --git a/pom.xml b/pom.xml index 21046a41..62808177 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,12 @@ 3.12.1.GA provided + + com.alibaba + fastjson + 1.2.8 + provided + @@ -37,6 +43,7 @@ assembly-dyn.xml assembly-st.xml + assembly-test.xml diff --git a/process_trace.rb b/process/process_trace.rb similarity index 100% rename from process_trace.rb rename to process/process_trace.rb diff --git a/process/to_xmind.py b/process/to_xmind.py new file mode 100644 index 00000000..68fcd9fe --- /dev/null +++ b/process/to_xmind.py @@ -0,0 +1,121 @@ +import sys +import json + +file = sys.argv[1] + + +def main(): + # read traces + traces = list(read_file()) + + # separate threads + thread_traces = separate_threads(traces) + + # make tree for every thread + root = TraceNode('') + for thread_id, traces in thread_traces: + t = make_tree(str(thread_id), traces) + root.add(t) + + # output as xmind + print(xmind(root)) + + +def xmind(node): + return '\n'.join(xmind_lines(node)) + + +def xmind_lines(node): + lines = [] + lines.append(node.text) + for e in node.children: + for line in xmind_lines(e): + lines.append('\t' + line) + return lines + + +def make_tree(name, traces): + root = TraceNode(name) + if not traces: return root + target_depth = traces[0].depth + next_level = [] + for e in traces: + if e.direction == 'in': continue + if e.depth != target_depth: + next_level.append(e) + continue + if e.depth == target_depth: + name = simple_method_name(e.clazz, e.method) + node = make_tree(name, next_level) + root.add(node) + next_level = [] + continue + return root + + +def simple_method_name(full_class, method_name): + dot = full_class.rindex('.') + simple_class = full_class[dot + 1:] + if '$' not in simple_class: return simple_class + '.' + method_name + dollar = simple_class.rindex('$') + nest_class = simple_class[dollar + 1:] + return nest_class + '.' + method_name + + +def separate_threads(traces): + # all threads + threads = [] + for e in traces: + if e.thread not in threads: + threads.append(e.thread) + + # every thread: + result = [] + for e in threads: + thread_traces = [] + for e2 in traces: + if e2.thread == e: + thread_traces.append(e2) + result.append((e, thread_traces)) + return result + + +def read_file(): + with open(file) as f: + for e in f: + e = e.strip() + if not e: continue + yield parse_trace(e) + + +def parse_trace(trace): + trace = json.loads(trace) + result = Trace() + result.time = trace['@timestamp'] + result.method = trace['method'] + result.clazz = trace['class'] + result.depth = trace['depth'] + result.thread = trace['thread'] + result.direction = trace['direction'] + return result + + +class Trace: + direction = None + thread = None + depth = None + clazz = None + method = None + time = None + + +class TraceNode: + def __init__(self, text): + self.text = text + self.children = [] + + def add(self, node): + self.children.append(node) + + +main() diff --git a/src/main/java/gr/gousiosg/javacg/dyn/CallTrace.java b/src/main/java/gr/gousiosg/javacg/dyn/CallTrace.java new file mode 100644 index 00000000..a3cbfbe2 --- /dev/null +++ b/src/main/java/gr/gousiosg/javacg/dyn/CallTrace.java @@ -0,0 +1,47 @@ +package gr.gousiosg.javacg.dyn; + +import com.alibaba.fastjson.JSONObject; + +import java.util.Stack; + +/** + * Created by caipeichao on 16/12/10. + */ +public class CallTrace { + + private static ThreadLocal threadLocal = new ThreadLocal() { + @Override + public CallTrace initialValue() { + return new CallTrace(); + } + }; + + private Stack stack = new Stack(); + + public static void push(String className, String method) { + threadLocal.get().localPush(className, method); + } + + public static void pop() { + threadLocal.get().localPop(); + } + + private void localPush(String className, String method) { + JSONObject json = new JSONObject(); + json.put("@timestamp", System.currentTimeMillis()); + json.put("thread", Thread.currentThread().getId()); + json.put("class", className); + json.put("method", method); + json.put("direction", "in"); + json.put("depth", stack.size()); + stack.push(json); + LogFile.log(json); + } + + private void localPop() { + JSONObject json = stack.pop(); + json.put("@timestamp", System.currentTimeMillis()); + json.put("direction", "out"); + LogFile.log(json); + } +} diff --git a/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java b/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java index 3aee29b8..ce4fa192 100644 --- a/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java +++ b/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java @@ -43,6 +43,8 @@ import javassist.CtBehavior; import javassist.CtClass; import javassist.NotFoundException; +import javassist.expr.ExprEditor; +import javassist.expr.MethodCall; public class Instrumenter implements ClassFileTransformer { @@ -102,7 +104,7 @@ public static void premain(String argument, Instrumentation instrumentation) { } public byte[] transform(ClassLoader loader, String className, Class clazz, - java.security.ProtectionDomain domain, byte[] bytes) { + java.security.ProtectionDomain domain, byte[] bytes) { boolean enhanceClass = false; String name = className.replace("/", "."); @@ -169,10 +171,8 @@ private void enhanceMethod(CtBehavior method, String className) if (method.getName().equals(name)) methodName = ""; - - method.insertBefore("gr.gousiosg.javacg.dyn.MethodStack.push(\"" + className - + ":" + methodName + "\");"); - method.insertAfter("gr.gousiosg.javacg.dyn.MethodStack.pop();"); + method.insertBefore(String.format("gr.gousiosg.javacg.dyn.CallTrace.push(\"%s\",\"%s\");", className, methodName)); + method.insertAfter("gr.gousiosg.javacg.dyn.CallTrace.pop();", true); } private static void err(String msg) { diff --git a/src/main/java/gr/gousiosg/javacg/dyn/LogFile.java b/src/main/java/gr/gousiosg/javacg/dyn/LogFile.java new file mode 100644 index 00000000..a1db5966 --- /dev/null +++ b/src/main/java/gr/gousiosg/javacg/dyn/LogFile.java @@ -0,0 +1,61 @@ +package gr.gousiosg.javacg.dyn; + +import com.alibaba.fastjson.JSONObject; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + +/** + * Created by caipeichao on 16/12/10. + */ +public class LogFile { + private static final LogFile INSTANCE = new LogFile(); + + public static void log(JSONObject json) { + INSTANCE.internalLog(json); + } + + private final BufferedWriter writer; + + public LogFile() { + // 初始化日志文件 + FileOutputStream fout = null; + try { + fout = new FileOutputStream("calltrace.json"); + } catch (FileNotFoundException e) { + } + this.writer = new BufferedWriter(new OutputStreamWriter(fout, Charset.forName("utf8"))); + + // 退出时关闭日志文件 + this.closeWhenExit(); + } + + public void internalLog(JSONObject json) { + try { + String x = json.toJSONString(); + this.writer.write(x); + this.writer.newLine(); + } catch (IOException e) { + } + } + + public void close() { + try { + writer.close(); + } catch (IOException e) { + } + } + + private void closeWhenExit() { + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + close(); + } + }); + } + +} diff --git a/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java b/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java deleted file mode 100644 index 90cc7a0f..00000000 --- a/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2011 - Georgios Gousios - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package gr.gousiosg.javacg.dyn; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Stack; - -public class MethodStack { - - private static Stack stack = new Stack(); - private static Map, Integer> callgraph = new HashMap, Integer>(); - static FileWriter fw; - static StringBuffer sb; - static long threadid = -1L; - - static { - Runtime.getRuntime().addShutdownHook(new Thread() { - public void run() { - try { - fw.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - //Sort by number of calls - List> keys = new ArrayList>(); - keys.addAll(callgraph.keySet()); - Collections.sort(keys, new Comparator() { - public int compare(Object o1, Object o2) { - Integer v1 = callgraph.get(o1); - Integer v2 = callgraph.get(o2); - return v1.compareTo(v2); - } - }); - - for (Pair key : keys) { - System.out.println(key + " " + callgraph.get(key)); - } - } - }); - File log = new File("calltrace.txt"); - try { - fw = new FileWriter(log); - } catch (Exception e) { - e.printStackTrace(); - } - sb = new StringBuffer(); - } - - public static void push(String callname) throws IOException { - if (threadid == -1) - threadid = Thread.currentThread().getId(); - - if (Thread.currentThread().getId() != threadid) - return; - - if (!stack.isEmpty()) { - Pair p = new Pair(stack.peek(), callname); - if (callgraph.containsKey(p)) - callgraph.put(p, callgraph.get(p) + 1); - else - callgraph.put(p, 1); - } - sb.setLength(0); - sb.append(">[").append(stack.size()).append("]"); - sb.append("[").append(Thread.currentThread().getId()).append("]"); - sb.append(callname).append("=").append(System.nanoTime()).append("\n"); - fw.write(sb.toString()); - stack.push(callname); - } - - public static void pop() throws IOException { - if (threadid == -1) - threadid = Thread.currentThread().getId(); - - if (Thread.currentThread().getId() != threadid) - return; - - String returnFrom = stack.pop(); - sb.setLength(0); - sb.append("<[").append(stack.size()).append("]"); - sb.append("[").append(Thread.currentThread().getId()).append("]"); - sb.append(returnFrom).append("=").append(System.nanoTime()).append("\n"); - fw.write(sb.toString()); - } -} \ No newline at end of file diff --git a/src/main/java/gr/gousiosg/javacg/test/TestException.java b/src/main/java/gr/gousiosg/javacg/test/TestException.java new file mode 100644 index 00000000..df488366 --- /dev/null +++ b/src/main/java/gr/gousiosg/javacg/test/TestException.java @@ -0,0 +1,26 @@ +package gr.gousiosg.javacg.test; + +/** + * Created by caipeichao on 16/12/10. + */ +public class TestException { + + public static void main(String[] argv) { + new TestException().run(); + } + + public void run() { + System.out.println("Hello world"); + foo(); + bar(); + } + + public void foo() { + System.out.println("Foo"); + throw new RuntimeException("Bad times"); + } + + public void bar() { + System.out.println("Bar"); + } +} diff --git a/src/main/java/gr/gousiosg/javacg/test/TestOneThread.java b/src/main/java/gr/gousiosg/javacg/test/TestOneThread.java new file mode 100644 index 00000000..7180aff1 --- /dev/null +++ b/src/main/java/gr/gousiosg/javacg/test/TestOneThread.java @@ -0,0 +1,25 @@ +package gr.gousiosg.javacg.test; + +/** + * Created by caipeichao on 16/12/10. + */ +public class TestOneThread { + + public static void main(String[] argv) { + new TestOneThread().run(); + } + + public void run() { + System.out.println("Hello world"); + foo(); + bar(); + } + + public void foo() { + System.out.println("Foo"); + } + + public void bar() { + System.out.println("Bar"); + } +} diff --git a/test/test.sh b/test/test.sh new file mode 100644 index 00000000..305c3136 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,21 @@ +# check args +cd "`dirname "$0"`" +c="$1" +if [ "$c" == "" ] ; then + echo try: + ls ../src/main/java/gr/gousiosg/javacg/test | sed 's/\.java$//g' | xargs -n 1 echo 'bash' "$0" + exit 1 +fi + +# run test +cd .. +mvn package -DskipTests +cd target +export CLASSPATH=javacg-0.1-SNAPSHOT-test.jar +java -javaagent:javacg-0.1-SNAPSHOT-dycg-agent.jar="incl=gr.gousiosg.javacg.test.*;" gr.gousiosg.javacg.test.$1 +echo ------------------------ +echo Call trace is: +cat calltrace.json +echo ------------------------ +echo To xmind: +python3 ../process/to_xmind.py calltrace.json