«    »

Visualizing Java Package Dependencies

I have recently been examining the overall package structure of a Java enterprise application. I discovered an easy way to visualize the dependencies between packages using two open source tools JDepend and Graphviz and a little glue code. JDepend analyzes compiled Java bytecode and determines dependencies and metrics between Java packages. These analysis results can be used to generate a graph with packages as nodes and dependencies as edges. Graphviz can then be used to layout and draw the graph as an image. Sample code is provided at the end of this article to show how to do this.

Visualizing package dependencies can be useful for a number of reasons:

  • Reverse engineering the architecture of an application when documentation is limited, out-of-date, or non-existent.
  • Automatically generating diagrams, saving the time required to draw them manually.
  • To understand impacts when doing large-scale refactorings or changing implementations of components.
  • To determine how to extract functionality for reuse.
  • To support good architecture / design as espoused by Robert C. Martin in his book Agile Software Development, Principles, Patterns, and Practices, such as by eliminating cyclical dependencies or unwanted dependencies.

Here is an example package dependency diagram generated by Graphviz:
Example Java Package Dependencies Diagram

The source code provided below that was used to generate this example diagram is available from my Java Examples project on GitHub or as a packaged bundle on my Software page. The code can easily be reused: change the directory of compiled code scanned by JDepend ("dist/classes"), and change the root package used to limit the scope of the diagram ("com.basilv.examples.packagediagram").

package com.basilv.examples.packagediagram;
import java.io.*;
import java.util.*;
import jdepend.framework.*;

public class PackageDiagramCreatorApp {

  public static void main(String[] args) {
    createPackageDependencyDiagram();
    System.exit(0);
  }
  
  public static void createPackageDependencyDiagram() {
    Collection packages = analyzePackages();
    StringBuilder builder = generateGraph(packages);
    generateImage("packages", builder.toString());
  }

  @SuppressWarnings("unchecked")
  private static Collection analyzePackages() {
    JDepend jdepend = new JDepend();
    try {
      jdepend.addDirectory("dist/classes");
    } catch (IOException e) {
      throw new RuntimeException("Error adding directory for JDepend to analyze.", e);
    }
    Collection packages = jdepend.analyze();
    return packages;
  }

  private static StringBuilder generateGraph(
    Collection packages) {
    StringBuilder builder = new StringBuilder();
    builder.append("digraph packages {").append("\n");
    builder.append("node [shape=box];").append("\n");
    builder.append("rankdir=BT;").append("\n");
    Set drawnDependencies = new HashSet();
    for (JavaPackage javaPackage : packages) {
      String packageNodeName = getGraphVizNodeForPackage(javaPackage);
      if (packageNodeName == null) {
        continue;
      }
      builder.append(packageNodeName).append("\n");

      @SuppressWarnings("unchecked")
      Collection dependencies = javaPackage.getEfferents();
      
      for (JavaPackage dependency : dependencies) {
        String dependencyNodeName = getGraphVizNodeForPackage(dependency);
        if (dependencyNodeName == null
          || packageNodeName.equals(dependencyNodeName)) {
          continue;
        }
        String dependencyKey = packageNodeName + "->"
          + dependencyNodeName;
        if (drawnDependencies.contains(dependencyKey)) {
          continue;
        }
        builder.append(packageNodeName).append(" -> ").append(
          dependencyNodeName).append(" [weight=4]").append("\n");
        drawnDependencies.add(dependencyKey);
      }
    }
    builder.append("}\n");
    return builder;
  }

  private static String getGraphVizNodeForPackage(
    JavaPackage javaPackage) {

    String rootPackage = "com.basilv.examples.packagediagram";
    String packageName = javaPackage.getName();
    if (!packageName.startsWith(rootPackage)) {
      return null;
    }

    return packageName.replace(".", "_");
  }

  private static void generateImage(String fileName,
    String graphVizDotFormattedGraph) {
    try {
      File graphFile = createFileWithContents(fileName
        + ".txt", graphVizDotFormattedGraph);

      // This requires the Graphviz software to be installed -
      // see http://graphviz.org/
      String imageFileLocation = fileName + ".png";
      Runtime.getRuntime().exec(
        "dot -v -Tpng " + graphFile.getName() + " -o "
          + imageFileLocation);

      System.out.println("Image file available at "
        + new File(imageFileLocation).getAbsolutePath());
    } catch (IOException e) {
      throw new RuntimeException("Error generating image " + fileName, e);
    }
  }

  private static File createFileWithContents(
    String fileName, String graphVizDotFormattedGraph)
    throws IOException {
    File graphFile = new File(fileName);
    FileWriter writer = new FileWriter(graphFile, false);
    try {
      writer.append(graphVizDotFormattedGraph);
    } finally {
      writer.close();
    }
    return graphFile;
  }

}

If you find this article helpful, please make a donation.

3 Comments on “Visualizing Java Package Dependencies”

  1. Yegor says:

    A normal constructor call shows up in the bytecode with all the
    package information about both the caller and the callee. However,
    when you instantiate a class dynamically using something like
    Class.forName(className).newInstance() package information is not
    known statically in the bytecode, thus will not show up in the graph.
    Can JDepend instrument the bytecode to pick up dependencies at
    runtime?

  2. @Yegor, you make an excellent point that JDepend captures only compile-time dependencies and not run-time dependencies. I don’t think JDepend has any options for detecting runtime dependencies, but you could use another source for populating the graph rendered by Graphviz. One source that comes to mind is the Spring context (assuming you are using Spring), which you likely can use to get information on all registered beans and their runtime dependencies.

  3. Mr Ed. says:

    Dude. This is awesome. Thank you! :D

«    »