Skip to content

Instantly share code, notes, and snippets.

@RamonDevPrivate
Last active April 9, 2024 07:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RamonDevPrivate/3bb187ef89b2666b1b1d00232100f5ee to your computer and use it in GitHub Desktop.
Save RamonDevPrivate/3bb187ef89b2666b1b1d00232100f5ee to your computer and use it in GitHub Desktop.

ObjectInspector

Der ObjectInspector untersucht mittels Reflection ein gegebenes Objekt und erzeugt daraus ein Objektdiagramm mit Hilfe von Graphviz Dot.

Voraussetzung

Um den ObjectInspector zu nutzen zu können muss Graphviz auf dem System installiert sein:

Windows:

scoop install graphviz

Ubuntu:

apt install graphviz

Mac

brew install graphviz

Benutzung

Der ObjectInspector wird über die JShell verwendet.

MyObject myObject = new MyObject();
NodeGenerator g = NodeGenerator.inspect(myObject, "myObject").toGraph();

Bei der Methode inspect() handelt es sich um eine Factory-Methode, welche eine NodeGenerator Instance erzeugt und das übergebene Objekt analysiert. Bei den übergebenen Argumenten handelt es sich um das zu untersuchende Objekt und den Namen der Variable, welche das Objekt referenziert.

Optional können weiter Argumente übergeben werden. inspectSuperClasses ist ein Boolean, welcher angibt, ob bei der Analyse auch Superklassen berücksichtigt werden sollen. Standardmäßig. Ist dieser nicht angegeben wird er als true gewertet. Der Compiler und die JShell können unter manchen Umständen Variablen erzeugen, welche nicht Teil des eigentlichen Objektes sind. Diese werden normalerweise ausgeblendet, können aber durch den Boolean hideGeneratedVars auch angezeigt werden.

Die Methode toGraph erzeugt im Arbeitsverzeichnis eine PNG-Datei.

Aufbau

Bei Dot handelt es sich um eine einfache Sprache, zu erzeugen. Eine einfache Dot Datei sieht wie folgt aus:

digraph G {
    start [shape=circle,label="",height=.25];
    n0 [label="MyObject"];
    n1 [label="5",style=filled,fillcolor="white"];
    
    start -> n0 [label=" x"] ;
    n0 -> n1 [label=" i"] ;
}

Zunächst werden die einzelnen Nodes definiert. In den eckigen Klammern stehen zusätzliche Eigenschaften, wie Form, Größe, Frage und Label. Das Label ist dabei der Text, der in der Node angezeigt wird. Mit den Pfeilen werden dann Verbindungen zwischen den Nodes dargestellt. Die Label geben hier die Bezeichnung der Beziehung an.

Nodes

Um aus einem Objekt und seinen Unterobjekten ein Diagramm zu erzeugen, müssen diese zunächst Nodes zugeordnet werden. Dies geschieht mit Hilfe der Node Klassen. Die verschiedenen Node-Klassen erben dabei von der abstrakten Klasse Node. Deren Konstruktor benötigt folgende Argumente:

  • name: Jede Node braucht einen eindeutigen Namen, um sie von anderen Gleichnamigen zu unterscheiden. Hierzu wird ein Zähler in der Klasse NodeGenerator verwendet.
  • value: Entweder der Name der Klasse mit der das Objekt erzeugt wurde oder der konkrete Wert im Falle von primitiven Typen oder Strings. Da dieses Argument ein Optional ist, kann auch Optional.empty() übergeben werden. In diesem Fall wird die Node als Null-Node gewertet.
  • identifier: Der Variablen Name in dem das Objekt gespeichert ist. Dieser Wert wird auf den Pfeilen angezeigt.
  • color: Die Hintergrundfarbe der Node
  • children: Hier werden Nodes für Unterobjekte gespeichert.

Die toString() Methode dieser Klasse generiert die Beziehungen der Nodes, also den unteren Teil der Ausgabe.

RootNode

Diese Node ist der Einstiegspunkt des Diagramms und stellt das Objekt dar, das eigentlich untersucht wird. Sie werden als Kreise dargestellt und generieren in der toString() Methode ihre Node Definition. Das besondere an dieser Node ist, dass die Start-Node zu ihr zeigt.

ChildNode

Die meisten Nodes sind ChildNodes. Sie werden als Kreise dargestellt und generieren in der toString() Methode ihre Node Definition.

ArrayNode

Arrays werden im Objektdiagramm als Rechtecke dargestellt. Des Weiteren erhalten sie eine Extra-Node, welche die Arraygröße angibt.

NodeGenerator

Der NodeGenerator untersucht mittels Reflection ein gegebenes Objekt und bildet es auf die oben genannte Nodestruktur ab. Die Klasse besitzt 4 Methoden:

  • Die toString() Methode gibt den Dot String der Nodestruktur aus.
  • Die Methode root() gibt eine eine Referenz auf die RootNode zurück.
  • Die Methode toGraph() erzeugt das Dot Diagramm. Um die Klasse selbst jedoch zu erzeugen, wird die Methode inspect() benötigt. Dieser übergibt man das Objekt, zu dem das Diagramm erzeugt werden soll, sowie den Namen der Variable, welche die Referenz des Objektes hält. Dies ist nötig, um diesen Sachverhalt ebenfalls darstellen zu können. Optional können noch weitere Argumente übergeben werden, welche das Verhalten der Methode verändern. Mit inspectSuperClasses kann gesteuert werden, ob Superklassen in dem Diagramm berücksichtigt werden oder nicht. Genauso, wie hideGeneratedVars entscheidet, ob Compiler-Generierte Variablen, welche mit "$" starten, angezeigt werden oder nicht.

Um sicherzustellen, dass Rekursive Beziehungen von Objekten korrekt dargestellt werden und nicht zu Problemen führen, als auch, dass identische Referenzen auf die selbe Node verweisen, wird jedes Objekt mit seiner generierten Node in einer HashMap gespeichert. So kann, sollte das Objekt bereits eine Node besitzen, diese wiederverwendet werden. So müssen auch die bereits existierenden ChildNodes nicht erneut generiert werden, womit auch Rekursionsschleifen vermieden werden.

Zu jedem Objekt, welches untersucht wird, wird mittels Reflection auf die zugehörige Klasse zu gegriffen und deren Felder. Bei einem Feld kann es sich um eine Klassen- oder Objektvariable handeln. Sollen neben der Klasse des Objektes auch Oberklassen untersucht werden, wird durch ein rekursiver Aufruf auf diese zugegriffen, sodass auch Superklassen von Superklassen untersucht werden.

private Field[] combineFields(Class classToBeInspected, Field[] fields) {
        Field[] classFields = classToBeInspected.getDeclaredFields();
        Field[] combinedFields = new Field[fields.length + classFields.length];
        for (int i = 0; i < combinedFields.length; i++) {
            combinedFields[i] = i < fields.length ? fields[i] : classFields[i - fields.length];
        }

        Class superclass = classToBeInspected.getSuperclass();
        if (superclass != null && inspectSuperClasses) return combineFields(superclass, combinedFields);
        return combinedFields;
    }

Jedes der gesammelten Felder wird anschließend auf Access überprüft, heißt, ob das Feld "public" ist oder nicht. Zugriffsversuche auf "private" Felder würden zu Fehlern führen.

Arrays, Listen und Co

fields[i].getType().isArray()
Collection.class.isAssignableFrom(fieldObj.getClass())
Map.class.isAssignableFrom(fieldObj.getClass())

Mittels dieser Ausdrücke wird überprüft, ob es sich bei dem Feld um ein Array, eine Collection oder eine Map handelt. Diese fälle werden speziell behandelt. Collections und Maps wie Arrays dargestellt werden können, werden diese auch in Arrays umgewandelt, wobei im Falle der Map die Werteliste als Array interpretiert wird und die Keys die Indizes des Arrays überschreiben.

Objekt Arten

Die Darstellung der Felder unterscheidet sich je nachdem, ob es sich um ein Array, einen primitiven Typen (oder String) oder ein weiteres Objekt handelt.

Da es sich bei einem Array um eine Datenstruktur handelt, wird nicht die Array Klasse, sondern die Array Elemente untersucht. Im Falle von primitiven Typen oder Strings, sollen die entsprechenden Werte direkt in der Node erscheinen. Da Objekte durch die Reflection nur als generisches Objekt vorliegt, müssen die Werte entsprechend gecastet werden. Der Typ kann durch Reflection aus den Feld Informationen ausgelesen werden (z.B. "int") oder im Falle von Arrays aus den Klasseninformationen der Elemente (z.B. "java.lang.Integer").

Für Objekte soll der Name der Objektklasse in der Node erscheinen. Dieser kann durch Reflection ausgelesen werden. Desweiteren werden Objekte auf weitere Objektreferenzen in ihren Feldern untersucht. Ausnahme bilden hierbei Objekte aus Java-internen Klassen, diese werden nur oberflächlich betrachtet.

Diagramm Generation

Ist die Nodestruktur fertig aufgebaut, kann diese gerendert werden. Dazu wird der aus den Nodes resultierende Dot String in eine temporäre Dot Datei geschrieben. Über einen Commandline Befehl wird aus dieser Datei die Grafik generiert.

//Author: https://github.com/RamonDevPrivate, Version 1, CC BY-NC-SA
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
abstract class ObjectNode_425 {
String name;
Optional<String> value;
String identifier;
boolean isDotted;
ObjectNode_425[] children;
/**
* @param name - custom name to uniquely identify node in dot graph
* @param value - values of primitive types/strings or classname; displayed inside the dot node
* @param identifier - variable name; displayed on dot arrow
* @param isDotted - dot node with dotted lines
* @param children - child nodes
*/
ObjectNode_425(String name, Optional<String> value, String identifier, boolean isDotted, ObjectNode_425... children) {
this.name = name;
this.value = value;
this.identifier = identifier;
this.children = children;
this.isDotted = isDotted;
}
@Override
public String toString() {
String output = "";
if (children != null && children.length > 0) {
for (ObjectNode_425 child : children) {
if (child == null) continue;
output += child.toString();
output += this.name + " -> " + child.name + "[label=\" "+ child.identifier + "\",style=" + (child.isDotted ? "dashed" : "solid") +"] ;\n";
}
}
return output;
}
}
class RootNode_425 extends ObjectNode_425 {
/**
* root / start of the graph; start point is pointing on this node.
* @param name - custom name to uniquely identify node in dot graph
* @param value - values of primitive types/strings or classname; displayed inside the dot node
* @param identifier - variable name; displayed on dot arrow
* @param children - child nodes
*/
RootNode_425(String name, Optional<String> value, String identifier, ObjectNode_425... children) {
super(name, value, identifier, false, children);
}
@Override
public String toString() {
String output = "start[shape=circle,label=\"\",height=.25];\n";
output += this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\"];\n" : " [shape=point,height=.25];\n");
output += "start -> " + name + "[label=\" "+ identifier + "\"] ;\n";
return output + super.toString();
}
}
class ChildNode_425 extends ObjectNode_425 {
/**
* child nodes
* @param name - custom name to uniquely identify node in dot graph
* @param value - values of primitive types/strings or classname; displayed inside the dot node
* @param identifier - variable name; displayed on dot arrow
* @param isDotted - dot node with dotted lines
* @param children - child nodes
*/
ChildNode_425(String name, Optional<String> value, String identifier, boolean isDotted, ObjectNode_425... children) {
super(name, value, identifier, isDotted, children);
}
@Override
public String toString() {
String output = this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\",style=" + (isDotted ? "dashed" : "solid") +"];\n" : " [shape=point,height=.25];\n");
return output + super.toString();
}
}
class ArrayNode extends ObjectNode_425 {
int length;
/**
* array nodes are displayed as a box and have an additional value for the array length
* can aswell be used to display any collection or map
* @param name - custom name to uniquely identify node in dot graph
* @param value - values of primitive types/strings or classname; displayed inside the dot node
* @param identifier - variable name; displayed on dot arrow
* @param length - array length
* @param isDotted - dot node with dotted lines
* @param children - child nodes
*/
ArrayNode(String name, Optional<String> value, String identifier, int length, boolean isDotted, ObjectNode_425... children) {
super(name, value, identifier, isDotted, children);
this.length = length;
}
@Override
public String toString() {
String output = this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\",shape=box,style=" + (isDotted ? "dashed" : "solid") +"];\n" : " [shape=point,height=.25];\n");
output += this.name + "length[label=\""+ this.length + "\"];\n";
output += this.name + "->" + this.name + "length[label=\"length\"]\n";
return output + super.toString();
}
}
class NodeGenerator {
private int nodeCounter = 0; //used to generate an unique node name
// save inspected objects to prevent infinite loops in case of recursion and identify already used objects
private Map<Object, ObjectNode_425> inspectedObject = new HashMap<>();
private ObjectNode_425 root;
private boolean hideGeneratedVars, inspectSuperClasses;
private NodeGenerator(){}
/**
* Inspect the object using reflections and store it in a tree structure of Nodes
* @param objectToBeInspected - root object of the tree structure;
* @param identifier - variable name referencing the object
* @return instance of NodeGenerator
*/
public static NodeGenerator inspect(Object objectToBeInspected, String identifier) {
return inspect(objectToBeInspected, identifier, true, true);
}
/**
* Inspect the object using reflections and store it in a tree structure of Nodes
* @param objectToBeInspected - root object of the tree structure;
* @param identifier - variable name referencing the object
* @param inspectSuperClasses - true -> super class fields are inspected too
* @return instance of NodeGenerator
*/
public static NodeGenerator inspect(Object objectToBeInspected, String identifier, boolean inspectSuperClasses) {
return inspect(objectToBeInspected, identifier, inspectSuperClasses, true);
}
/**
* Inspect the object using reflections and store it in a tree structure of Nodes
* @param objectToBeInspected - root object of the tree structure;
* @param identifier - variable name referencing the object
* @param inspectSuperClasses - true -> super class fields are inspected too
* @param hideGeneratedVars - true -> compiler generated vars are hidden
* @return instance of NodeGenerator
*/
public static NodeGenerator inspect(Object objectToBeInspected, String identifier, boolean inspectSuperClasses, boolean hideGeneratedVars) {
assert !objectToBeInspected.getClass().getPackageName().startsWith("java") : "Can't inspect Java owned objects!";
NodeGenerator g = new NodeGenerator();
g.hideGeneratedVars = hideGeneratedVars;
g.inspectSuperClasses = inspectSuperClasses;
g.root = g.objectReferenceToNodeTree(objectToBeInspected, identifier, true, false);
return g;
}
/**
* Convert Node tree into a dot graph and save it in the working directory
* @param root - root node of the Node tree
*/
public void toGraph() {
String dotSource = "digraph G {\n" + root.toString() + "}";
File dot;
byte[] img_stream = null;
File img;
try {
dot = writeDotSourceToFile(dotSource);
if (dot != null) {
img = File.createTempFile("graph_", ".png", new File("./"));
Runtime rt = Runtime.getRuntime();
String[] cmd = {"dot", "-Tpng", dot.getAbsolutePath(), "-o", img.getAbsolutePath()};
Process p = rt.exec(cmd);
p.waitFor();
// dot.delete(); // Delete dot file - remove this line to view the dot file
}
} catch (IOException | InterruptedException e) {
System.err.println(e.getMessage());
}
}
public ObjectNode_425 root() {
return root;
}
@Override
public String toString() {
return root.toString();
}
private Field[] combineFields(Class classToBeInspected, Field[] fields) {
Field[] classFields = classToBeInspected.getDeclaredFields();
Field[] combinedFields = new Field[fields.length + classFields.length];
for (int i = 0; i < combinedFields.length; i++) {
combinedFields[i] = i < fields.length ? fields[i] : classFields[i - fields.length];
}
Class superclass = classToBeInspected.getSuperclass();
if (superclass != null && inspectSuperClasses) return combineFields(superclass, combinedFields);
return combinedFields;
}
private ObjectNode_425 objectReferenceToNodeTree(Object objectToBeInspected, String identifier, boolean isRoot, boolean isDotted) {
Class classToBeInspected = objectToBeInspected.getClass();
// reuse same node for identical objects
if (inspectedObject.keySet().stream().anyMatch(key -> key == objectToBeInspected)) {
return new ChildNode_425(inspectedObject.get(objectToBeInspected).name, inspectedObject.get(objectToBeInspected).value, identifier, inspectedObject.get(objectToBeInspected).isDotted);
}
ObjectNode_425 result = isRoot
? new RootNode_425("n"+nodeCounter++, Optional.of(classToBeInspected.getSimpleName()), identifier)
: new ChildNode_425("n"+nodeCounter++, Optional.of(classToBeInspected.getSimpleName()), identifier, isDotted);
// Identify when the same object is used
inspectedObject.put(objectToBeInspected, result);
Field[] fields = combineFields(classToBeInspected, new Field[0]);
ObjectNode_425[] childs = new ObjectNode_425[fields.length];
for(int i = 0; i < fields.length; i++) {
if (!fields[i].getName().startsWith("$") && !Modifier.isStatic(fields[i].getModifiers()) && !fields[i].canAccess(objectToBeInspected))
continue; //ignore inaccessible fields
if (fields[i].getName().startsWith("$") && hideGeneratedVars)
continue; //ignore intern vars
try {
Object fieldObj = fields[i].get(objectToBeInspected);
if (fieldObj != null) {
// reuse same node for identical fields
if (inspectedObject.keySet().stream().anyMatch(key -> key == fieldObj)) {
childs[i] = new ChildNode_425(inspectedObject.get(fieldObj).name, inspectedObject.get(fieldObj).value, fields[i].getName(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i]));
continue;
}
// special cases like array, collections and maps
if (fields[i].getType().isArray()) {
childs[i] = processArray(fieldObj, fields[i].getName(), Optional.empty(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i]));
continue;
}
if (Collection.class.isAssignableFrom(fieldObj.getClass())) {
childs[i] = processArray(((Collection)fieldObj).toArray(), fields[i].getName(), Optional.empty(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i]));
childs[i].value = Optional.of(fieldObj.getClass().getSimpleName());
continue;
}
if (Map.class.isAssignableFrom(fieldObj.getClass())) {
childs[i] = processArray(((Map)fieldObj).values().toArray(), fields[i].getName(),
Optional.of(((Map)fieldObj).keySet().toArray()), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i]));
childs[i].value = Optional.of(fieldObj.getClass().getSimpleName());
continue;
}
}
// regular values / objects
childs[i] = processTypes(fields[i].getType().getTypeName(), fieldObj, fields[i].getName(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i]));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
result.children = childs;
return result;
}
private ObjectNode_425 processTypes(String typename, Object obj, String identifier, boolean isDotted) {
// special cases for primitive types and strings
return switch (typename) {
case "int", "java.lang.Integer" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Integer)obj).toString()), identifier, isDotted);
case "boolean", "java.lang.Boolean" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Boolean)obj).toString()), identifier, isDotted);
case "float", "java.lang.Float" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Float)obj).toString()), identifier, isDotted);
case "double", "java.lang.Double" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Double)obj).toString()), identifier, isDotted);
case "char", "java.lang.Character" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Character)obj).toString()), identifier, isDotted);
case "String", "java.lang.String" -> new ChildNode_425("n" + nodeCounter++, Optional.of("\\\"" + ((String)obj) + "\\\""), identifier, isDotted);
default -> (obj != null) // if object is null display it as point
? (!obj.getClass().getPackageName().startsWith("java") // recursivly travel through objects that are not part of java
? objectReferenceToNodeTree(obj, identifier, false, isDotted)
: new ChildNode_425("n" + nodeCounter++, Optional.of(obj.getClass().getSimpleName()), identifier, isDotted))
: new ChildNode_425("n" + nodeCounter++, Optional.empty(), identifier, isDotted);
};
}
private ObjectNode_425 processArray(Object obj, String identifier, Optional<Object[]> index, boolean isDotted) {
int arrayLength = Array.getLength(obj);
ObjectNode_425[] arrayChilds = new ObjectNode_425[arrayLength];
for (int j = 0; j < arrayLength; j++) {
Object element = Array.get(obj, j);
ObjectNode_425 child = (element != null)
? (element.getClass().getPackageName().startsWith("java") // recursivly travel through objects that are not part of java
? processTypes(element.getClass().getTypeName(), element, index.isPresent() //display regular index or custom one for e.g. maps
? index.get()[j].toString()
: Integer.valueOf(j).toString(), isDotted)
: objectReferenceToNodeTree(element, index.isPresent() //display regular index or custom one for e.g. maps
? index.get()[j].toString()
: Integer.valueOf(j).toString(), false, isDotted))
: new ChildNode_425("n" + nodeCounter++, Optional.empty(), index.isPresent() //display regular index or custom one for e.g. maps
? index.get()[j].toString()
: Integer.valueOf(j).toString(), isDotted);
arrayChilds[j] = child;
}
return new ArrayNode("n" + nodeCounter++, Optional.of(obj.getClass().getSimpleName()), identifier, arrayLength, isDotted, arrayChilds);
}
private File writeDotSourceToFile(String str) throws IOException {
File temp = File.createTempFile("temp", ".dot", new File("./"));
FileWriter fw = new FileWriter(temp);
fw.write(str);
fw.close();
return temp;
}
}
// jshell -R-ea
// MyObject myObject = new MyObject();
// NodeGenerator g = NodeGenerator.inspect(myObject, "myObject");
// NodeGenerator g = NodeGenerator.inspect(myObject, "myObject", true, false);
// g.toGraph(); // generate dot image
// g.root(); // generated node structure
// g.toString(); // generated dot string
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment