├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── com └── axt └── jvmcallgraph ├── BytecodeSource.java ├── CallGraph.java ├── CallGraphNode.java ├── CallGraphRequest.java ├── ClassHierarchy.java ├── ClasspathBytecodeSource.java ├── DirectoryBytecodeSource.java ├── JarBytecodeSource.java ├── Main.java ├── MethodCallCollector.java ├── MethodCollector.java ├── MethodInfo.java └── Util.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | .gradle 5 | bin 6 | build 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Attila Axt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jvm-callgraph 2 | Call graph generator for JVM bytecode 3 | 4 | ## Usage 5 | 6 | Get the immediate callers of `com.axt.jvmcallgraph.Callgraph.nextLevel()` 7 | ```java 8 | CallGraphRequest callGraphRequest = new CallGraphRequest.Builder() 9 | .addClasspathSource(Main.class.getClassLoader(), "com.axt") 10 | .addTargetMethod(method -> 11 | method.getClassName().equals("com/axt/jvmcallgraph/CallGraph") && 12 | method.getName().equals("nextLevel")) 13 | .build(); 14 | 15 | CallGraph callGraph = new CallGraph(callGraphRequest); 16 | callGraph.build(1); 17 | ``` 18 | 19 | 20 | Get the callgraph of all `native` functions in `rt.jar` 21 | ```java 22 | CallGraphRequest callGraphRequest = new CallGraphRequest.Builder() 23 | .addJarSource(PATH_TO_RT_JAR) 24 | .addTargetMethod(method -> method.isNative()) 25 | .stopCondition(method -> method.isPublic()) 26 | .prune(true) 27 | .build(); 28 | 29 | CallGraph callGraph = new CallGraph(callGraphRequest); 30 | callGraph.build(5); 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | description = """rtjar parser""" 2 | group = "com.axt" 3 | 4 | repositories { 5 | mavenCentral() 6 | } 7 | 8 | 9 | apply plugin: 'java' 10 | apply plugin: 'application' 11 | 12 | mainClassName = 'com.axt.jvmcallgraph.Main' 13 | 14 | 15 | dependencies { 16 | compile 'com.google.guava:guava:19.0' 17 | compile 'org.ow2.asm:asm-all:5.1' 18 | compile 'org.apache.xbean:xbean-finder:3.12' 19 | compile 'com.google.guava:guava:19.0' 20 | 21 | } 22 | 23 | task runJar(type: JavaExec, dependsOn: [':compileJava', ':processResources']) { 24 | main 'com.axt.jvmcallgraph.Main' 25 | doFirst { 26 | def cp = files(project.compileJava.destinationDir, project.processResources.destinationDir,) + project.configurations.compile 27 | classpath cp 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axt/jvm-callgraph/21a59911094627e3707808a1ee6d3b95f2f9f4c0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 08 00:59:42 CEST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-2.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='jvm-callgraph' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/BytecodeSource.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.stream.Stream; 6 | 7 | import org.objectweb.asm.ClassReader; 8 | 9 | public interface BytecodeSource { 10 | List getClassReaders() throws IOException; 11 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/CallGraph.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.Set; 13 | import java.util.function.Predicate; 14 | 15 | import org.objectweb.asm.ClassReader; 16 | import org.objectweb.asm.Opcodes; 17 | 18 | import com.google.common.collect.ImmutableList; 19 | 20 | public class CallGraph { 21 | 22 | private CallGraphRequest callGraphRequest; 23 | 24 | private List rootNodes; 25 | private Set activeMethods; 26 | private HashMap methodToNodeMap; 27 | 28 | private int depth; 29 | boolean built = false; 30 | 31 | public CallGraph(CallGraphRequest callGraphRequest) { 32 | this.callGraphRequest = callGraphRequest; 33 | } 34 | 35 | public void build(int deep) throws IOException { 36 | this.depth = deep; 37 | this.activeMethods = new HashSet<>(collectTargetMethods()); 38 | this.rootNodes = setRootNodes(this.activeMethods); 39 | this.methodToNodeMap = new HashMap<>(); 40 | for(CallGraphNode node: rootNodes) { 41 | this.methodToNodeMap.put(node.method, node); 42 | } 43 | for (int i=0; i < deep; i++) { 44 | boolean workleft = nextLevel(i==0); 45 | if(!workleft) break; 46 | } 47 | pruneGraph(); 48 | built = true; 49 | } 50 | 51 | public CallGraphNode getNode(MethodInfo method) { 52 | checkIsBuilt(); 53 | return methodToNodeMap.get(method); 54 | } 55 | 56 | public Collection getRootNodes() { 57 | checkIsBuilt(); 58 | return Collections.unmodifiableList(rootNodes); 59 | } 60 | 61 | public int getDepth() { 62 | return depth; 63 | } 64 | 65 | private void checkIsBuilt() { 66 | if (!built) 67 | throw new IllegalStateException("build() should be called before accessing query functions"); 68 | } 69 | 70 | private List collectTargetMethods() throws IOException { 71 | MethodCollector targetMethodCollector = new MethodCollector(callGraphRequest.getTargetMethods()); 72 | for(ClassReader cr : callGraphRequest.getClassReaders()) { 73 | targetMethodCollector.setClassName(cr.getClassName()); 74 | cr.accept(targetMethodCollector, 0); 75 | } 76 | return targetMethodCollector.getCollectedMethods(); 77 | } 78 | 79 | private List setRootNodes(Collection targetMethods) { 80 | List rootNodes = new ArrayList<>(); 81 | for(MethodInfo method : targetMethods) { 82 | rootNodes.add(new CallGraphNode(method)); 83 | } 84 | return rootNodes; 85 | } 86 | 87 | private boolean nextLevel(boolean top) throws IOException { 88 | MethodCallCollector methodCallCollector = new MethodCallCollector(activeMethods); 89 | for(ClassReader cr : callGraphRequest.getClassReaders()) { 90 | methodCallCollector.setClassName(cr.getClassName()); 91 | cr.accept(methodCallCollector, 0); 92 | } 93 | 94 | Set remainingMethods = new HashSet<>(); 95 | 96 | for(MethodInfo method: activeMethods) { 97 | Collection callees = methodCallCollector.getCallees(method); 98 | Predicate stopCondition = callGraphRequest.getStopCondition(); 99 | if (!top && stopCondition != null && stopCondition.test(method)) { 100 | continue; 101 | } 102 | CallGraphNode methodNode = methodToNodeMap.get(method); 103 | for (MethodInfo callee : callees) { 104 | CallGraphNode calleeNode = null; 105 | 106 | if (!methodToNodeMap.containsKey(callee)) { 107 | calleeNode = new CallGraphNode(callee); 108 | methodToNodeMap.put(callee, calleeNode); 109 | remainingMethods.add(callee); 110 | 111 | } else { 112 | calleeNode = methodToNodeMap.get(callee); 113 | } 114 | methodNode.addCallee(calleeNode); 115 | 116 | if (callGraphRequest.isFollowInterfaces() || callGraphRequest.isFollowSuper()) { 117 | Set> scope = this.callGraphRequest.toPredicates(ImmutableList.of(new CallGraphRequest.ExplicitTargetMethod(callee, callGraphRequest.isFollowSuper(), callGraphRequest.isFollowInterfaces()))); 118 | 119 | MethodCollector targetMethodCollector = new MethodCollector(new ArrayList(scope)); 120 | for(ClassReader cr : callGraphRequest.getClassReaders()) { 121 | targetMethodCollector.setClassName(cr.getClassName()); 122 | cr.accept(targetMethodCollector, 0); 123 | } 124 | 125 | for(MethodInfo callee2 : targetMethodCollector.getCollectedMethods()) { 126 | if (callee2 == callee) 127 | continue; 128 | if (!methodToNodeMap.containsKey(callee2)) { 129 | calleeNode = new CallGraphNode(callee2); 130 | methodToNodeMap.put(callee2, calleeNode); 131 | remainingMethods.add(callee2); 132 | } else { 133 | calleeNode = methodToNodeMap.get(callee2); 134 | } 135 | methodNode.addCallee(calleeNode); 136 | } 137 | } 138 | } 139 | } 140 | 141 | this.activeMethods = remainingMethods; 142 | return activeMethods.size() > 0; 143 | } 144 | 145 | boolean pruneCalleeNodes(CallGraphNode node, Set visited, int level) { 146 | if (node.calleeNodes.size() == 0 || level == depth+1) { 147 | if(!(callGraphRequest.getStopCondition() != null && callGraphRequest.getStopCondition().test(node.method))) 148 | return false; 149 | return true; 150 | } else { 151 | boolean keepNode = false; 152 | Iterator it = node.calleeNodes.iterator(); 153 | while(it.hasNext()) { 154 | Set visitedCurrent = new HashSet(visited); 155 | visitedCurrent.add(node); 156 | CallGraphNode callee = it.next(); 157 | if (visitedCurrent.contains(callee)) { 158 | it.remove(); 159 | continue; 160 | }; 161 | boolean keepCallee = pruneCalleeNodes(callee, visitedCurrent, level+1); 162 | if (keepCallee) { 163 | keepNode |= keepCallee; 164 | } else { 165 | it.remove(); 166 | } 167 | } 168 | return keepNode; 169 | } 170 | } 171 | 172 | private void pruneGraph() { 173 | if (this.callGraphRequest.getPrune()) { 174 | Iterator it = rootNodes.iterator(); 175 | while(it.hasNext()) { 176 | if(!pruneCalleeNodes(it.next(), new HashSet<>(), 0)) { 177 | it.remove(); 178 | } 179 | } 180 | } 181 | } 182 | 183 | private static void printCallGraph(StringBuilder sb, CallGraphNode node, int maxDepth) { 184 | printNode(sb, node, 0, maxDepth); 185 | } 186 | 187 | private static void printNode(StringBuilder sb, CallGraphNode node, int level, int maxDepth) { 188 | if (level > maxDepth) { 189 | return; 190 | } 191 | for(int i=0; i < level; i++) { 192 | sb.append("\t"); 193 | } 194 | sb.append("("); 195 | sb.append(level); 196 | sb.append(")"); 197 | sb.append(node.method); 198 | sb.append("\n"); 199 | for (CallGraphNode calleeNode : node.calleeNodes) { 200 | //if (!calleeNode.getVirtual() || calleeNode.size() > 0) 201 | printNode(sb, calleeNode, level+1, maxDepth); 202 | } 203 | } 204 | 205 | public String dump() { 206 | return dump(getDepth()); 207 | } 208 | 209 | public String dump(int maxDepth) { 210 | StringBuilder sb = new StringBuilder(); 211 | Collection rootNodes = this.getRootNodes(); 212 | for (CallGraphNode rootNode : rootNodes) { 213 | printCallGraph(sb, rootNode, maxDepth); 214 | } 215 | return sb.toString(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/CallGraphNode.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | public class CallGraphNode { 7 | MethodInfo method; 8 | 9 | Set calleeNodes; 10 | CallGraphNode parentNode; 11 | 12 | public CallGraphNode(MethodInfo method) { 13 | this.method = method; 14 | this.calleeNodes = new HashSet<>(); 15 | } 16 | 17 | public void addCallee(CallGraphNode callee) { 18 | this.calleeNodes.add(callee); 19 | } 20 | 21 | public void removeCallee(CallGraphNode callee) { 22 | this.calleeNodes.remove(callee); 23 | } 24 | 25 | public int size() { 26 | return calleeNodes.size(); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "CallGraphNode [method=" + method + ", calleeNodes=" 32 | + calleeNodes + ", parentNode=" + parentNode + "]"; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/CallGraphRequest.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Collection; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.function.Predicate; 10 | 11 | import org.objectweb.asm.ClassReader; 12 | import com.google.common.collect.ImmutableList; 13 | 14 | public class CallGraphRequest { 15 | 16 | private final List sources; 17 | private final List> targetMethods; 18 | private final Predicate stopCondition; 19 | private final boolean prune; 20 | 21 | private List cachedClassReaders = null; 22 | private ClassHierarchy classHierarchy = null; 23 | private final boolean followInterfaces; 24 | private final boolean followSuper; 25 | 26 | 27 | private CallGraphRequest(Builder builder) { 28 | this.sources = new ArrayList<>(builder.sources); 29 | Set> targetMethods = new HashSet<>(); 30 | targetMethods.addAll(builder.targetMethods); 31 | targetMethods.addAll(toPredicates(builder.explicitTargetMethods)); 32 | this.targetMethods = new ArrayList<>(targetMethods); 33 | this.stopCondition = builder.stopCondition; 34 | this.prune = builder.prune; 35 | this.followInterfaces = builder.followInterfaces; 36 | this.followSuper = builder.followSuper; 37 | } 38 | 39 | public Set> toPredicates(List explicitTargetMethods) { 40 | Set> predicates = new HashSet<>(); 41 | for (ExplicitTargetMethod etm : explicitTargetMethods) { 42 | List classNames = new ArrayList<>(); 43 | List workList = new ArrayList<>(); 44 | workList.add(etm.methodInfo.getClassName()); 45 | while(!workList.isEmpty()) { 46 | String className = workList.remove(0); 47 | classNames.add(className); 48 | if (etm.withSuper) { 49 | String superName = getClassHierarchy().getSuper(className); 50 | if (superName != null) { 51 | workList.add(superName); 52 | } 53 | } 54 | if (etm.withInterfaces) { 55 | for (String interfaceName : getClassHierarchy().getInterfaces(className)) { 56 | workList.add(interfaceName); 57 | } 58 | } 59 | } 60 | for (String className : classNames) { 61 | predicates.add(toPredicate(new MethodInfo(className, etm.methodInfo.getName(), etm.methodInfo.getDescription()))); 62 | } 63 | } 64 | return predicates; 65 | } 66 | 67 | private Predicate toPredicate(final MethodInfo methodInfo) { 68 | return new Predicate() { 69 | @Override 70 | public boolean test(MethodInfo t) { 71 | boolean matches = true; 72 | matches &= t.getName().equals(methodInfo.getName()); 73 | matches &= t.getClassName().equals(methodInfo.getClassName()); 74 | if (methodInfo.getDescription() != null) { 75 | matches &= t.getDescription().equals(methodInfo.getDescription()); 76 | } 77 | return matches; 78 | } 79 | }; 80 | } 81 | 82 | public ClassHierarchy getClassHierarchy() { 83 | if (this.classHierarchy == null) { 84 | ClassHierarchy classHierarchy = new ClassHierarchy(); 85 | classHierarchy.build(getClassReaders()); 86 | this.classHierarchy = classHierarchy; 87 | } 88 | return this.classHierarchy; 89 | } 90 | 91 | public List getClassReaders() { 92 | if (this.cachedClassReaders == null) { 93 | try { 94 | List cachedClassReaders = new ArrayList<>(); 95 | for(BytecodeSource source : sources) { 96 | cachedClassReaders.addAll(source.getClassReaders()); 97 | } 98 | this.cachedClassReaders = cachedClassReaders; 99 | } catch (IOException e) { 100 | throw new RuntimeException(e); 101 | } 102 | } 103 | return this.cachedClassReaders; 104 | } 105 | 106 | public List> getTargetMethods() throws IOException { 107 | List> ret = new ArrayList<>(); 108 | for(Predicate targetMethod: targetMethods) { 109 | ret.add(targetMethod); 110 | } 111 | return ret; 112 | } 113 | 114 | public Predicate getStopCondition() { 115 | return stopCondition; 116 | } 117 | 118 | public boolean getPrune() { 119 | return prune; 120 | } 121 | 122 | public boolean isFollowInterfaces() { 123 | return followInterfaces; 124 | } 125 | 126 | public boolean isFollowSuper() { 127 | return followSuper; 128 | } 129 | 130 | 131 | 132 | public final static class ExplicitTargetMethod { 133 | MethodInfo methodInfo; 134 | boolean withSuper; 135 | boolean withInterfaces; 136 | public ExplicitTargetMethod(MethodInfo methodInfo, boolean withSuper, boolean withInterfaces) { 137 | super(); 138 | this.methodInfo = methodInfo; 139 | this.withSuper = withSuper; 140 | this.withInterfaces = withInterfaces; 141 | } 142 | } 143 | 144 | public final static class Builder { 145 | private List sources = new ArrayList<>(); 146 | private final List> targetMethods = new ArrayList<>(); 147 | private final List explicitTargetMethods = new ArrayList<>(); 148 | private Predicate stopCondition; 149 | private boolean prune; 150 | private boolean followSuper; 151 | private boolean followInterfaces; 152 | 153 | public Builder() { 154 | } 155 | 156 | public Builder addDirectorySource(String directory) { 157 | sources.add(new DirectoryBytecodeSource(directory)); 158 | return this; 159 | } 160 | 161 | public Builder addJarSource(String jarfile) { 162 | sources.add(new JarBytecodeSource(jarfile)); 163 | return this; 164 | } 165 | 166 | public Builder addClasspathSource(ClassLoader classLoader, Collection basePackages) { 167 | sources.add(new ClasspathBytecodeSource(classLoader, basePackages)); 168 | return this; 169 | } 170 | 171 | public Builder addClasspathSource(ClassLoader classLoader, String basePackage) { 172 | sources.add(new ClasspathBytecodeSource(classLoader, ImmutableList.of(basePackage))); 173 | return this; 174 | } 175 | 176 | public Builder addTargetMethod(Predicate predicate) { 177 | targetMethods.add(predicate); 178 | return this; 179 | } 180 | 181 | public Builder addExplicitTargetMethod(MethodInfo methodInfo, boolean withSuper, boolean withInterfaces) { 182 | if (methodInfo.getName() == null) 183 | throw new IllegalArgumentException("Method name is mandatory"); 184 | if (methodInfo.getClassName() == null) 185 | throw new IllegalArgumentException("Method class is mandatory"); 186 | explicitTargetMethods.add(new ExplicitTargetMethod(methodInfo, withSuper, withInterfaces)); 187 | return this; 188 | } 189 | 190 | public Builder stopCondition(Predicate stopCondition) { 191 | this.stopCondition = stopCondition; 192 | return this; 193 | } 194 | 195 | public Builder prune(boolean prune) { 196 | this.prune = prune; 197 | return this; 198 | } 199 | 200 | public Builder followInterfaces(boolean followInterfaces) { 201 | this.followInterfaces = followInterfaces; 202 | return this; 203 | } 204 | 205 | public Builder followSuper(boolean followSuper) { 206 | this.followSuper = followSuper; 207 | return this; 208 | } 209 | 210 | public CallGraphRequest build() { 211 | return new CallGraphRequest(this); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/ClassHierarchy.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import org.objectweb.asm.ClassReader; 10 | 11 | import com.google.common.collect.HashMultimap; 12 | import com.google.common.collect.Multimap; 13 | 14 | class ClassHierarchy { 15 | 16 | Map superNames = new HashMap<>(); 17 | Multimap interfaces = HashMultimap.create(); 18 | 19 | public ClassHierarchy() { 20 | } 21 | 22 | public void build(List classReaders) { 23 | for (ClassReader classReader : classReaders) { 24 | superNames.put(classReader.getClassName(), classReader.getSuperName()); 25 | interfaces.putAll(classReader.getClassName(), Arrays.asList(classReader.getInterfaces())); 26 | } 27 | } 28 | 29 | public String getSuper(String className) { 30 | return superNames.get(className); 31 | } 32 | 33 | public Collection getInterfaces(String className) { 34 | return interfaces.get(className); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/ClasspathBytecodeSource.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.Enumeration; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.StreamSupport; 12 | 13 | import org.apache.xbean.finder.archive.Archive; 14 | import org.apache.xbean.finder.archive.CompositeArchive; 15 | import org.apache.xbean.finder.archive.FileArchive; 16 | import org.apache.xbean.finder.archive.JarArchive; 17 | import org.objectweb.asm.ClassReader; 18 | 19 | public class ClasspathBytecodeSource implements BytecodeSource { 20 | 21 | private Collection basePackages; 22 | private ClassLoader classLoader; 23 | 24 | public ClasspathBytecodeSource(ClassLoader classLoader, Collection basePackages) { 25 | this.classLoader = classLoader; 26 | this.basePackages = basePackages; 27 | } 28 | 29 | @Override 30 | public List getClassReaders() throws IOException { 31 | 32 | 33 | List archives = new ArrayList<>(); 34 | for (String basePackage : basePackages) { 35 | Enumeration resources = ClasspathBytecodeSource.class.getClassLoader().getResources(basePackage.replace('.', '/')); 36 | while(resources.hasMoreElements()) { 37 | URL url = resources.nextElement(); 38 | if ("zip".equals(url.getProtocol()) || "jar".equals(url.getProtocol())) { 39 | archives.add(new JarArchive(classLoader, url)); 40 | } else { 41 | archives.add(new FileArchive(classLoader, url, basePackage)); 42 | } 43 | } 44 | } 45 | 46 | CompositeArchive compositeArchive = new CompositeArchive(archives); 47 | 48 | return StreamSupport.stream(compositeArchive.spliterator(), false) 49 | .map(new Function() { 50 | @Override 51 | public ClassReader apply(Archive.Entry t) { 52 | try { 53 | return new ClassReader(t.getBytecode()); 54 | } catch(IOException e) { 55 | //TODO 56 | System.out.println("EXC"); 57 | return null; 58 | } 59 | } 60 | }) 61 | .filter(x -> x != null) 62 | .collect(Collectors.toList()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/DirectoryBytecodeSource.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | import org.objectweb.asm.ClassReader; 13 | 14 | public class DirectoryBytecodeSource implements BytecodeSource { 15 | 16 | private String directory; 17 | 18 | public DirectoryBytecodeSource(String directory) { 19 | this.directory = directory; 20 | } 21 | 22 | @Override 23 | public List getClassReaders() throws IOException { 24 | return Files.walk(Paths.get(directory)) 25 | .sequential() 26 | .filter(x -> !x.toFile().isDirectory()) 27 | .filter(x -> x.toFile().getAbsolutePath().endsWith(".class")) 28 | .map(new Function() { 29 | @Override 30 | public ClassReader apply(Path t) { 31 | try { 32 | return new ClassReader(new FileInputStream(t.toFile())); 33 | } catch(IOException e) { 34 | //TODO 35 | System.out.println("EXC"); 36 | return null; 37 | } 38 | } 39 | }) 40 | .filter(x -> x != null) 41 | .collect(Collectors.toList()); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/JarBytecodeSource.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import java.util.Enumeration; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.jar.JarEntry; 11 | import java.util.jar.JarFile; 12 | import java.util.stream.Collectors; 13 | 14 | import org.objectweb.asm.ClassReader; 15 | 16 | public class JarBytecodeSource implements BytecodeSource { 17 | 18 | private String jarFileName; 19 | 20 | public JarBytecodeSource(String jarfile) { 21 | this.jarFileName = jarfile; 22 | } 23 | 24 | @Override 25 | public List getClassReaders() throws IOException { 26 | try(JarFile jarFile = new JarFile(new File(jarFileName))) { 27 | return Util.enumerationAsStream(jarFile.entries()) 28 | .filter(x -> !x.isDirectory()) 29 | .filter(x -> x.getName().endsWith(".class")) 30 | .map(new Function() { 31 | @Override 32 | public ClassReader apply(JarEntry t) { 33 | try { 34 | return new ClassReader(jarFile.getInputStream(t)); 35 | } catch(IOException e) { 36 | //TODO 37 | System.out.println("EXC"); 38 | return null; 39 | } 40 | } 41 | }) 42 | .filter(x -> x != null) 43 | .collect(Collectors.toList()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/Main.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.io.IOException; 4 | 5 | class Main { 6 | 7 | public static void main(String[] args) throws IOException { 8 | explicitTargetMethod(); 9 | callsToSystemExit(); 10 | } 11 | 12 | private static void callsToSystemExit() throws IOException { 13 | CallGraphRequest callGraphRequest = new CallGraphRequest.Builder() 14 | .addJarSource("/usr/lib/jvm/java-8-oracle/jre/lib/rt.jar") 15 | .addTargetMethod(x -> x.getClassName().equals("java/lang/System") && x.getName().equals("exit")) 16 | .stopCondition(x -> x.getClassName().startsWith("java/")) 17 | .prune(true) 18 | .build(); 19 | 20 | CallGraph callGraph = new CallGraph(callGraphRequest); 21 | callGraph.build(3); 22 | System.out.println(callGraph.dump()); 23 | } 24 | 25 | private static void explicitTargetMethod() throws IOException { 26 | CallGraphRequest callGraphRequest = new CallGraphRequest.Builder() 27 | .addClasspathSource(Main.class.getClassLoader(), "com.axt") 28 | .addExplicitTargetMethod(new MethodInfo("com/axt/jvmcallgraph/ClasspathBytecodeSource", "getClassReaders", null), true, true) 29 | .build(); 30 | 31 | CallGraph callGraph = new CallGraph(callGraphRequest); 32 | callGraph.build(5); 33 | 34 | System.out.println(callGraph.dump()); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/MethodCallCollector.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.objectweb.asm.ClassVisitor; 8 | import org.objectweb.asm.MethodVisitor; 9 | import org.objectweb.asm.Opcodes; 10 | 11 | import com.google.common.collect.HashMultimap; 12 | import com.google.common.collect.Multimap; 13 | 14 | class MethodCallCollector extends ClassVisitor { 15 | 16 | private String className; 17 | private Set calledMethods; 18 | private Multimap calleeMethods = HashMultimap.create(); 19 | 20 | public MethodCallCollector(Collection calledMethods) { 21 | super(Opcodes.ASM5); 22 | this.calledMethods = new HashSet(calledMethods); 23 | } 24 | 25 | @Override 26 | public MethodVisitor visitMethod(int access, final String outerName, final String outerDesc, String signature, String[] exceptions) { 27 | return new MethodVisitor(Opcodes.ASM5) { 28 | @Override 29 | public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { 30 | MethodInfo mi = new MethodInfo(owner, name, desc); 31 | if (calledMethods.contains(mi)) { 32 | MethodInfo outerMethodInfo = new MethodInfo(className, outerName, outerDesc); 33 | outerMethodInfo.setAccess(access); 34 | calleeMethods.put(mi, outerMethodInfo); 35 | // int INVOKEVIRTUAL = 182; 36 | // int INVOKESPECIAL = 183; 37 | // int INVOKESTATIC = 184; 38 | // int INVOKEINTERFACE = 185; 39 | //TODO 40 | // int INVOKEDYNAMIC = 186; // visitInvokeDynamicInsn 41 | // System.out.println("CALL " + outerMethodInfo + " -> " + mi + " " + opcode); 42 | } 43 | } 44 | }; 45 | } 46 | 47 | public void setClassName(String className) { 48 | this.className = className; 49 | } 50 | 51 | Collection getCallees(MethodInfo method) { 52 | return this.calleeMethods.get(method); 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/MethodCollector.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.function.Predicate; 7 | 8 | import org.objectweb.asm.ClassVisitor; 9 | import org.objectweb.asm.MethodVisitor; 10 | import org.objectweb.asm.Opcodes; 11 | 12 | class MethodCollector extends ClassVisitor { 13 | 14 | private final List> targetMethods; 15 | private final List collectedMethods = new ArrayList<>(); 16 | private String className; 17 | 18 | public MethodCollector(List> targetMethods) { 19 | super(Opcodes.ASM5); 20 | this.targetMethods = targetMethods; 21 | } 22 | 23 | @Override 24 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 25 | MethodInfo mi = new MethodInfo(className, name, desc); 26 | mi.setAccess(access); 27 | for (Predicate predicate : targetMethods) { 28 | if (predicate.test(mi)) { 29 | collectedMethods.add(mi); 30 | } 31 | } 32 | return super.visitMethod(access, name, desc, signature, exceptions); 33 | } 34 | 35 | public List getCollectedMethods() { 36 | return Collections.unmodifiableList(collectedMethods); 37 | } 38 | 39 | public boolean hasCollectedMethods() { 40 | return collectedMethods.size() > 0; 41 | } 42 | 43 | public void setClassName(String className) { 44 | this.className = className; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import org.objectweb.asm.Opcodes; 4 | 5 | public class MethodInfo { 6 | private String name; 7 | private String description; 8 | private String className; 9 | private Integer access; 10 | 11 | public MethodInfo(String className, String name, String description) { 12 | super(); 13 | this.className = className; 14 | this.name = name; 15 | this.description = description; 16 | } 17 | 18 | public void setAccess(int access) { 19 | this.access = access; 20 | } 21 | 22 | public Integer getAccess() { 23 | return access; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | public String getDescription() { 30 | return description; 31 | } 32 | public String getClassName() { 33 | return className; 34 | } 35 | 36 | public boolean isPublic() { 37 | return (access & Opcodes.ACC_PUBLIC) != 0; 38 | } 39 | public boolean isPrivate() { 40 | return (access & Opcodes.ACC_PRIVATE) != 0; 41 | } 42 | public boolean isProtected() { 43 | return (access & Opcodes.ACC_PROTECTED) != 0; 44 | } 45 | public boolean isNative() { 46 | return (access & Opcodes.ACC_NATIVE) != 0; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | String ret = ""; 52 | if (access != null) { 53 | if (isPublic()) 54 | ret += "public "; 55 | if (isProtected()) 56 | ret += "protected "; 57 | if (isPrivate()) 58 | ret += "private "; 59 | if (isNative()) 60 | ret += "native "; 61 | } 62 | 63 | ret += className + "::" + name + " " + description; 64 | return ret; 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | final int prime = 31; 70 | int result = 1; 71 | result = prime * result 72 | + ((className == null) ? 0 : className.hashCode()); 73 | result = prime * result 74 | + ((description == null) ? 0 : description.hashCode()); 75 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 76 | return result; 77 | } 78 | 79 | @Override 80 | public boolean equals(Object obj) { 81 | if (this == obj) 82 | return true; 83 | if (obj == null) 84 | return false; 85 | if (getClass() != obj.getClass()) 86 | return false; 87 | MethodInfo other = (MethodInfo) obj; 88 | if (className == null) { 89 | if (other.className != null) 90 | return false; 91 | } else if (!className.equals(other.className)) 92 | return false; 93 | if (description == null) { 94 | if (other.description != null) 95 | return false; 96 | } else if (!description.equals(other.description)) 97 | return false; 98 | if (name == null) { 99 | if (other.name != null) 100 | return false; 101 | } else if (!name.equals(other.name)) 102 | return false; 103 | return true; 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/axt/jvmcallgraph/Util.java: -------------------------------------------------------------------------------- 1 | package com.axt.jvmcallgraph; 2 | 3 | import java.util.Enumeration; 4 | import java.util.Iterator; 5 | import java.util.Spliterator; 6 | import java.util.Spliterators; 7 | import java.util.stream.Stream; 8 | import java.util.stream.StreamSupport; 9 | 10 | public class Util { 11 | 12 | public static Stream enumerationAsStream(Enumeration e) { 13 | return StreamSupport.stream( 14 | Spliterators.spliteratorUnknownSize( 15 | new Iterator() { 16 | public T next() { 17 | return e.nextElement(); 18 | } 19 | public boolean hasNext() { 20 | return e.hasMoreElements(); 21 | } 22 | }, 23 | Spliterator.ORDERED), false); 24 | } 25 | } 26 | --------------------------------------------------------------------------------