├── .gitignore ├── .img └── demo-customer-in-the-browser.PNG ├── README.md ├── callgraphplotter.py ├── javacg └── javacg-0.1-SNAPSHOT-static.jar ├── nodeselectors.py ├── requirements.txt └── target-jar └── demo-customer-0.0.1-SNAPSHOT.jar /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | .idea 4 | output/* 5 | __pycache__ 6 | call-graph.txt 7 | lib -------------------------------------------------------------------------------- /.img/demo-customer-in-the-browser.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcello-dev/java-call-graph-plotter/e315b0099827add58e545064aa4117c9145e5bb6/.img/demo-customer-in-the-browser.PNG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Call Graph Plotter 2 | 3 | This project allows generating and visualizing static call graphs for Spring/Java applications. 4 | 5 | # Installation 6 | 7 | After cloning the repository, create a Python 3.8 virtual environment in the project folder: 8 | ```bash 9 | python -m venv .venv 10 | ``` 11 | Activate the virtual environment and install the dependencies: 12 | ```bash 13 | source .venv/bin/activate 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | # Usage 18 | 19 | Create a call graph from your Jar: 20 | 21 | ```java 22 | java -jar javacg/javacg-0.1-SNAPSHOT-static.jar your-jar.jar > call-graph.txt 23 | ``` 24 | 25 | If you just want to test how it works, you can use the example jar in the folder *target-jar*: 26 | ``` 27 | java -jar javacg/javacg-0.1-SNAPSHOT-static.jar target-jar/demo-customer-0.0.1-SNAPSHOT.jar > call-graph.txt 28 | ``` 29 | Now create the graph by running: 30 | ``` 31 | python callgraphplotter.py call-graph.txt ApiDBSelector 32 | ``` 33 | A new graph will be created in the folder *output*. It's a html file so you can open it in the browser. 34 | If you used the example jar, this is what you should see in the browser: 35 | ![Graph in the browser](.img/demo-customer-in-the-browser.PNG) 36 | 37 | # More info 38 | 39 | See the full article [on Medium](https://epsilongem.medium.com/visualize-the-api-db-relations-in-a-java-spring-application-896f26096920). 40 | -------------------------------------------------------------------------------- /callgraphplotter.py: -------------------------------------------------------------------------------- 1 | from pyvis.network import Network 2 | import networkx as nx 3 | import sys 4 | import os 5 | import nodeselectors 6 | 7 | if len(sys.argv) != 3: 8 | print('Error: Missing file') 9 | print('Usage: callgraphplotter.py ') 10 | sys.exit() 11 | 12 | # Get file path from input 13 | call_graph_file_path = sys.argv[1] 14 | 15 | # Get node selector class name from input 16 | ns_class_name = sys.argv[2] 17 | 18 | # Open and read input file 19 | print('Reading file:', call_graph_file_path) 20 | call_graph_file = open(call_graph_file_path, 'r') 21 | # If the call graph file uses utf-16 then use the following 22 | #call_graph_file = open(call_graph_file_path, 'r', encoding='utf-16') 23 | lines = call_graph_file.readlines() 24 | 25 | # Get call graph filename 26 | call_graph_file_name = os.path.basename(os.path.splitext(call_graph_file_path)[0]) 27 | 28 | def extract_nx_source_name(line): 29 | tokens = line.strip().split(":") 30 | class1 = tokens[1] 31 | middle_tokens = tokens[2].split(" ") 32 | method1 = middle_tokens[0].split("(")[0] 33 | return class1 + ":" + method1 34 | 35 | def extract_nx_target_name(line): 36 | tokens = line.strip().split(":") 37 | middle_tokens = tokens[2].split(" ") 38 | class2 = middle_tokens[1].split(")")[1] 39 | method2 = tokens[3].split("(")[0] 40 | return class2 + ":" + method2 41 | 42 | # Create Networkx Directed Graph 43 | G = nx.DiGraph() 44 | for line in lines: 45 | # Consider only the method calls 46 | if line.startswith("M:"): 47 | # Add source node 48 | source = extract_nx_source_name(line) 49 | G.add_node(source) 50 | # Add target node 51 | target = extract_nx_target_name(line) 52 | G.add_node(target) 53 | # Add edge 54 | G.add_edge(source, target) 55 | 56 | 57 | # Instantiate the node selector 58 | klass = getattr(nodeselectors, ns_class_name) 59 | node_selector = klass(G) 60 | 61 | # Select the source and sink nodes 62 | source_nodes = node_selector.source_nodes() 63 | sink_nodes = node_selector.sink_nodes() 64 | 65 | # Create a new graph (Visjs) with only the nodes that we selected before 66 | # The selected nodes are in the lists source_nodes and sink_nodes 67 | net = Network(height='550px', width='700px', directed=True) 68 | 69 | # One source node can point to many sink nodes 70 | for source in source_nodes: 71 | for sink in sink_nodes: 72 | for path in nx.all_simple_paths(G, source=source.id, target=sink.id): 73 | # add source node 74 | #print("Visjs Source node: ", source_node_name) 75 | net.add_node(source.display_name, title=source.display_name, color=source.color) 76 | 77 | # Add sink node with grey color 78 | net.add_node(sink.display_name, title=sink.display_name, color=sink.color) 79 | #print("Visjs Sink node: ", sink_node_name) 80 | 81 | # Add the edge: green is a read operation, red is a write operation 82 | net.add_edge(source.display_name, sink.display_name, color=sink.edge_color(), title=sink.edge_label()) 83 | 84 | # Visualization 85 | net.show_buttons() 86 | output_file = 'output/'+call_graph_file_name+'.html' 87 | # create output directory if it doesn't exist 88 | if not os.path.exists("output"): 89 | os.makedirs("output") 90 | 91 | net.write_html(output_file) 92 | # If you want to open the file immediately use the following line instead 93 | # net.show(output_file) 94 | print('File generated:',output_file) -------------------------------------------------------------------------------- /javacg/javacg-0.1-SNAPSHOT-static.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcello-dev/java-call-graph-plotter/e315b0099827add58e545064aa4117c9145e5bb6/javacg/javacg-0.1-SNAPSHOT-static.jar -------------------------------------------------------------------------------- /nodeselectors.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class VisJsNode: 4 | def __init__(self, id, color): 5 | self.id = id 6 | self.display_name = id 7 | self.color = color 8 | 9 | class SourceNode(VisJsNode): 10 | def __init__(self, id, color): 11 | super().__init__(id, color) 12 | self.set_display_name() 13 | 14 | # build name of the source node 15 | # for example if id is "java.lang.Integer:valueOf" this function set display_name to "Integer:valueOf" 16 | def set_display_name(self): 17 | class_name = self.id.split(":")[0].split(".")[-1] 18 | method_name = self.id.split(":")[1] 19 | self.display_name = class_name + ":" + method_name 20 | 21 | class SinkNode(VisJsNode): 22 | 23 | def __init__(self, id, color): 24 | super().__init__(id, color) 25 | self.set_display_name() 26 | 27 | # build name of the sink node 28 | # for example if id is "java.lang.Integer:valueOf" this function set display_name to "Integer" 29 | def set_display_name(self): 30 | class_name = self.id.split(":")[0].split(".")[-1] 31 | self.display_name = class_name 32 | 33 | def parse_method_name(self): 34 | return self.id.split(":")[1] 35 | 36 | def edge_label(self): 37 | return self.parse_method_name() 38 | 39 | def edge_color(self): 40 | method_name = self.parse_method_name() 41 | is_write_op = "save" in method_name or "delete" in method_name 42 | return "red" if is_write_op else "green" 43 | 44 | class NodeSelector(ABC): 45 | def __init__(self, call_graph): 46 | self.call_graph = call_graph 47 | self.nodes = call_graph.nodes() 48 | 49 | def source_nodes(self): 50 | return self.select_source_nodes(list(self.nodes())) 51 | 52 | @abstractmethod 53 | def select_source_nodes(self, nodes): 54 | pass 55 | 56 | def sink_nodes(self): 57 | return self.select_sink_nodes(list(self.nodes())) 58 | 59 | @abstractmethod 60 | def select_sink_nodes(self, nodes): 61 | pass 62 | 63 | 64 | # This is an example for the demo-customer Spring Project 65 | class ApiDBSelector(NodeSelector): 66 | def select_source_nodes(self, nodes): 67 | source_nodes = [] 68 | for node_id in nodes: 69 | if "com.example.democustomer.controller.CustomerController" in node_id: 70 | source_nodes.append(SourceNode(node_id, 'orange')) 71 | elif "com.example.democustomer.controller.ReportController" in node_id: 72 | source_nodes.append(SourceNode(node_id, 'blue')) 73 | return source_nodes 74 | 75 | def select_sink_nodes(self, nodes): 76 | sink_nodes = [] 77 | for node_id in nodes: 78 | if "com.example.democustomer.repo" in node_id: 79 | sink_nodes.append(SinkNode(node_id, '#E0E0E0')) 80 | return sink_nodes 81 | 82 | 83 | # Implement your custom NodeSelector below 84 | class MyNodeSelector(NodeSelector): 85 | def select_source_nodes(self, nodes): 86 | source_nodes = [] 87 | for node_id in nodes: 88 | # add nodes to source_nodes list 89 | pass 90 | return source_nodes 91 | 92 | def select_sink_nodes(self, nodes): 93 | sink_nodes = [] 94 | for node_id in nodes: 95 | # add nodes to sink_nodes list 96 | pass 97 | return sink_nodes 98 | 99 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyvis==0.3.2 2 | -------------------------------------------------------------------------------- /target-jar/demo-customer-0.0.1-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcello-dev/java-call-graph-plotter/e315b0099827add58e545064aa4117c9145e5bb6/target-jar/demo-customer-0.0.1-SNAPSHOT.jar --------------------------------------------------------------------------------