Dumping a java object's properties

76,808

Solution 1

You could try XStream.

XStream xstream = new XStream(new Sun14ReflectionProvider(
  new FieldDictionary(new ImmutableFieldKeySorter())),
  new DomDriver("utf-8"));
System.out.println(xstream.toXML(new Outer()));

prints out:

<foo.ToString_-Outer>
  <intValue>5</intValue>
  <innerValue>
    <stringValue>foo</stringValue>
  </innerValue>
</foo.ToString_-Outer>

You could also output in JSON

And be careful of circular references ;)

Solution 2

I tried using XStream as originally suggested, but it turns out the object graph I wanted to dump included a reference back to the XStream marshaller itself, which it didn't take too kindly to (why it must throw an exception rather than ignoring it or logging a nice warning, I'm not sure.)

I then tried out the code from user519500 above but found I needed a few tweaks. Here's a class you can roll into a project that offers the following extra features:

  • Can control max recursion depth
  • Can limit array elements output
  • Can ignore any list of classes, fields, or class+field combinations - just pass an array with any combination of class names, classname+fieldname pairs separated with a colon, or fieldnames with a colon prefix ie: [<classname>][:<fieldname>]
  • Will not output the same object twice (the output indicates when an object was previously visited and provides the hashcode for correlation) - this avoids circular references causing problems

You can call this using one of the two methods below:

    String dump = Dumper.dump(myObject);
    String dump = Dumper.dump(myObject, maxDepth, maxArrayElements, ignoreList);

As mentioned above, you need to be careful of stack-overflows with this, so use the max recursion depth facility to minimise the risk.

Hopefully somebody will find this useful!

package com.mycompany.myproject;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Dumper {
    private static Dumper instance = new Dumper();

    protected static Dumper getInstance() {
        return instance;
    }

    class DumpContext {
        int maxDepth = 0;
        int maxArrayElements = 0;
        int callCount = 0;
        HashMap<String, String> ignoreList = new HashMap<String, String>();
        HashMap<Object, Integer> visited = new HashMap<Object, Integer>();
    }

    public static String dump(Object o) {
        return dump(o, 0, 0, null);
    }

    public static String dump(Object o, int maxDepth, int maxArrayElements, String[] ignoreList) {
        DumpContext ctx = Dumper.getInstance().new DumpContext();
        ctx.maxDepth = maxDepth;
        ctx.maxArrayElements = maxArrayElements;

        if (ignoreList != null) {
            for (int i = 0; i < Array.getLength(ignoreList); i++) {
                int colonIdx = ignoreList[i].indexOf(':');
                if (colonIdx == -1)
                    ignoreList[i] = ignoreList[i] + ":";
                ctx.ignoreList.put(ignoreList[i], ignoreList[i]);
            }
        }

        return dump(o, ctx);
    }

    protected static String dump(Object o, DumpContext ctx) {
        if (o == null) {
            return "<null>";
        }

        ctx.callCount++;
        StringBuffer tabs = new StringBuffer();
        for (int k = 0; k < ctx.callCount; k++) {
            tabs.append("\t");
        }
        StringBuffer buffer = new StringBuffer();
        Class oClass = o.getClass();

        String oSimpleName = getSimpleNameWithoutArrayQualifier(oClass);

        if (ctx.ignoreList.get(oSimpleName + ":") != null)
            return "<Ignored>";

        if (oClass.isArray()) {
            buffer.append("\n");
            buffer.append(tabs.toString().substring(1));
            buffer.append("[\n");
            int rowCount = ctx.maxArrayElements == 0 ? Array.getLength(o) : Math.min(ctx.maxArrayElements, Array.getLength(o));
            for (int i = 0; i < rowCount; i++) {
                buffer.append(tabs.toString());
                try {
                    Object value = Array.get(o, i);
                    buffer.append(dumpValue(value, ctx));
                } catch (Exception e) {
                    buffer.append(e.getMessage());
                }
                if (i < Array.getLength(o) - 1)
                    buffer.append(",");
                buffer.append("\n");
            }
            if (rowCount < Array.getLength(o)) {
                buffer.append(tabs.toString());
                buffer.append(Array.getLength(o) - rowCount + " more array elements...");
                buffer.append("\n");
            }
            buffer.append(tabs.toString().substring(1));
            buffer.append("]");
        } else {
            buffer.append("\n");
            buffer.append(tabs.toString().substring(1));
            buffer.append("{\n");
            buffer.append(tabs.toString());
            buffer.append("hashCode: " + o.hashCode());
            buffer.append("\n");
            while (oClass != null && oClass != Object.class) {
                Field[] fields = oClass.getDeclaredFields();

                if (ctx.ignoreList.get(oClass.getSimpleName()) == null) {
                    if (oClass != o.getClass()) {
                        buffer.append(tabs.toString().substring(1));
                        buffer.append("  Inherited from superclass " + oSimpleName + ":\n");
                    }

                    for (int i = 0; i < fields.length; i++) {

                        String fSimpleName = getSimpleNameWithoutArrayQualifier(fields[i].getType());
                        String fName = fields[i].getName();

                        fields[i].setAccessible(true);
                        buffer.append(tabs.toString());
                        buffer.append(fName + "(" + fSimpleName + ")");
                        buffer.append("=");

                        if (ctx.ignoreList.get(":" + fName) == null &&
                            ctx.ignoreList.get(fSimpleName + ":" + fName) == null &&
                            ctx.ignoreList.get(fSimpleName + ":") == null) {

                            try {
                                Object value = fields[i].get(o);
                                buffer.append(dumpValue(value, ctx));
                            } catch (Exception e) {
                                buffer.append(e.getMessage());
                            }
                            buffer.append("\n");
                        }
                        else {
                            buffer.append("<Ignored>");
                            buffer.append("\n");
                        }
                    }
                    oClass = oClass.getSuperclass();
                    oSimpleName = oClass.getSimpleName();
                }
                else {
                    oClass = null;
                    oSimpleName = "";
                }
            }
            buffer.append(tabs.toString().substring(1));
            buffer.append("}");
        }
        ctx.callCount--;
        return buffer.toString();
    }

    protected static String dumpValue(Object value, DumpContext ctx) {
        if (value == null) {
            return "<null>";
        }
        if (value.getClass().isPrimitive() ||
            value.getClass() == java.lang.Short.class ||
            value.getClass() == java.lang.Long.class ||
            value.getClass() == java.lang.String.class ||
            value.getClass() == java.lang.Integer.class ||
            value.getClass() == java.lang.Float.class ||
            value.getClass() == java.lang.Byte.class ||
            value.getClass() == java.lang.Character.class ||
            value.getClass() == java.lang.Double.class ||
            value.getClass() == java.lang.Boolean.class ||
            value.getClass() == java.util.Date.class ||
            value.getClass().isEnum()) {

            return value.toString();

        } else {

            Integer visitedIndex = ctx.visited.get(value);
            if (visitedIndex == null) {
                ctx.visited.put(value, ctx.callCount);
                if (ctx.maxDepth == 0 || ctx.callCount < ctx.maxDepth) {
                    return dump(value, ctx);
                }
                else {
                    return "<Reached max recursion depth>";
                }
            }
            else {
                return "<Previously visited - see hashCode " + value.hashCode() + ">";
            }
        }
    }


    private static String getSimpleNameWithoutArrayQualifier(Class clazz) {
        String simpleName = clazz.getSimpleName();
        int indexOfBracket = simpleName.indexOf('['); 
        if (indexOfBracket != -1)
            return simpleName.substring(0, indexOfBracket);
        return simpleName;
    }
}

Solution 3

this will print out all fields (including arrays of objects) of an object.

Fixed version of Ben Williams post from this thread

Note: this method uses recursion so If you have a very deep object graph you may get a stack-overflow (no pun intended ;) IF so you need to use the VM parameter -Xss10m. If your using eclipse put it in run>runconfiguration>augments (tab) VM augment box and press apply

import java.lang.reflect.Array;
import java.lang.reflect.Field;

public static String dump(Object o) {
    StringBuffer buffer = new StringBuffer();
    Class oClass = o.getClass();
     if (oClass.isArray()) {
         buffer.append("Array: ");
        buffer.append("[");
        for (int i = 0; i < Array.getLength(o); i++) {
            Object value = Array.get(o, i);
            if (value.getClass().isPrimitive() ||
                    value.getClass() == java.lang.Long.class ||
                    value.getClass() == java.lang.Integer.class ||
                    value.getClass() == java.lang.Boolean.class ||
                    value.getClass() == java.lang.String.class ||
                    value.getClass() == java.lang.Double.class ||
                    value.getClass() == java.lang.Short.class ||
                    value.getClass() == java.lang.Byte.class
                    ) {
                buffer.append(value);
                if(i != (Array.getLength(o)-1)) buffer.append(",");
            } else {
                buffer.append(dump(value));
             }
        }
        buffer.append("]\n");
    } else {
         buffer.append("Class: " + oClass.getName());
         buffer.append("{\n");
        while (oClass != null) {
            Field[] fields = oClass.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                fields[i].setAccessible(true);
                buffer.append(fields[i].getName());
                buffer.append("=");
                try {
                    Object value = fields[i].get(o);
                    if (value != null) {
                        if (value.getClass().isPrimitive() ||
                                value.getClass() == java.lang.Long.class ||
                                value.getClass() == java.lang.String.class ||
                                value.getClass() == java.lang.Integer.class ||
                                value.getClass() == java.lang.Boolean.class ||
                                    value.getClass() == java.lang.Double.class ||
                                value.getClass() == java.lang.Short.class ||
                                value.getClass() == java.lang.Byte.class
                                ) {
                            buffer.append(value);
                        } else {
                            buffer.append(dump(value));
                        }
                    }
                } catch (IllegalAccessException e) {
                    buffer.append(e.getMessage());
                }
                buffer.append("\n");
            }
            oClass = oClass.getSuperclass();
        }
        buffer.append("}\n");
    }
    return buffer.toString();
}

Solution 4

I wanted an elegant solution to this problem that:

  • Does not use any external library
  • Uses Reflection to access fields, including superclass fields
  • Uses recursion to traverse the Object-graph with only one stack frame per call
  • Uses an IdentityHashMap to handle backwards references and avoid infinite recursion
  • Handles primitives, auto-boxing, CharSequences, enums, and nulls appropriately
  • Allows you to choose whether or not to parse static fields
  • Is simple enough to modify according to formatting preferences

I wrote the following utility class:

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * Utility class to dump {@code Object}s to string using reflection and recursion.
 */
public class StringDump {

    /**
     * Uses reflection and recursion to dump the contents of the given object using a custom, JSON-like notation (but not JSON). Does not format static fields.<p>
     * @see #dump(Object, boolean, IdentityHashMap, int)
     * @param object the {@code Object} to dump using reflection and recursion
     * @return a custom-formatted string representing the internal values of the parsed object
     */
    public static String dump(Object object) {
        return dump(object, false, new IdentityHashMap<Object, Object>(), 0);
    }

    /**
     * Uses reflection and recursion to dump the contents of the given object using a custom, JSON-like notation (but not JSON).<p>
     * Parses all fields of the runtime class including super class fields, which are successively prefixed with "{@code super.}" at each level.<p>
     * {@code Number}s, {@code enum}s, and {@code null} references are formatted using the standard {@link String#valueOf()} method.
     * {@code CharSequences}s are wrapped with quotes.<p>
     * The recursive call invokes only one method on each recursive call, so limit of the object-graph depth is one-to-one with the stack overflow limit.<p>
     * Backwards references are tracked using a "visitor map" which is an instance of {@link IdentityHashMap}.
     * When an existing object reference is encountered the {@code "sysId"} is printed and the recursion ends.<p>
     * 
     * @param object             the {@code Object} to dump using reflection and recursion
     * @param isIncludingStatics {@code true} if {@code static} fields should be dumped, {@code false} to skip them
     * @return a custom-formatted string representing the internal values of the parsed object
     */
    public static String dump(Object object, boolean isIncludingStatics) {
        return dump(object, isIncludingStatics, new IdentityHashMap<Object, Object>(), 0);
    }

    private static String dump(Object object, boolean isIncludingStatics, IdentityHashMap<Object, Object> visitorMap, int tabCount) {
        if (object == null ||
                object instanceof Number || object instanceof Character || object instanceof Boolean ||
                object.getClass().isPrimitive() || object.getClass().isEnum()) {
            return String.valueOf(object);
        }

        StringBuilder builder = new StringBuilder();
        int           sysId   = System.identityHashCode(object);
        if (object instanceof CharSequence) {
            builder.append("\"").append(object).append("\"");
        }
        else if (visitorMap.containsKey(object)) {
            builder.append("(sysId#").append(sysId).append(")");
        }
        else {
            visitorMap.put(object, object);

            StringBuilder tabs = new StringBuilder();
            for (int t = 0; t < tabCount; t++) {
                tabs.append("\t");
            }
            if (object.getClass().isArray()) {
                builder.append("[").append(object.getClass().getName()).append(":sysId#").append(sysId);
                int length = Array.getLength(object);
                for (int i = 0; i < length; i++) {
                    Object arrayObject = Array.get(object, i);
                    String dump        = dump(arrayObject, isIncludingStatics, visitorMap, tabCount + 1);
                    builder.append("\n\t").append(tabs).append("\"").append(i).append("\":").append(dump);
                }
                builder.append(length == 0 ? "" : "\n").append(length == 0 ? "" : tabs).append("]");
            }
            else {
                // enumerate the desired fields of the object before accessing
                TreeMap<String, Field> fieldMap    = new TreeMap<String, Field>();  // can modify this to change or omit the sort order
                StringBuilder          superPrefix = new StringBuilder();
                for (Class<?> clazz = object.getClass(); clazz != null && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) {
                    Field[] fields = clazz.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++) {
                        Field field = fields[i];
                        if (isIncludingStatics || !Modifier.isStatic(field.getModifiers())) {
                            fieldMap.put(superPrefix + field.getName(), field);
                        }
                    }
                    superPrefix.append("super.");
                }

                builder.append("{").append(object.getClass().getName()).append(":sysId#").append(sysId);
                for (Entry<String, Field> entry : fieldMap.entrySet()) {
                    String name  = entry.getKey();
                    Field  field = entry.getValue();
                    String dump;
                    try {
                        boolean wasAccessible = field.isAccessible();
                        field.setAccessible(true);
                        Object  fieldObject   = field.get(object);
                        field.setAccessible(wasAccessible);  // the accessibility flag should be restored to its prior ClassLoader state
                        dump                  = dump(fieldObject, isIncludingStatics, visitorMap, tabCount + 1);
                    }
                    catch (Throwable e) {
                        dump = "!" + e.getClass().getName() + ":" + e.getMessage();
                    }
                    builder.append("\n\t").append(tabs).append("\"").append(name).append("\":").append(dump);
                }
                builder.append(fieldMap.isEmpty() ? "" : "\n").append(fieldMap.isEmpty() ? "" : tabs).append("}");
            }
        }
        return builder.toString();
    }
}

I tested it on a number of classes and for me it's extremely efficient. For example, try using it to dump the main thread:

public static void main(String[] args) throws Exception {
    System.out.println(dump(Thread.currentThread()));
}

Edit

Since writing this post I had reason to create an iterative version of this algorithm. The recursive version is limited in depth by total stack frames, but you might have reason to dump an extremely large object graph. To handle my situation, I revised the algorithm to use a stack data structure in place of the runtime stack. This version is time-efficient and is limited by heap size instead of stack frame depth.

You can download and use the iterative version here.

Solution 5

You should use RecursiveToStringStyle:

System.out.println(ReflectionToStringBuilder.toString(new Outer(), new RecursiveToStringStyle()));
Share:
76,808
Kevin
Author by

Kevin

Updated on July 05, 2022

Comments

  • Kevin
    Kevin almost 2 years

    Is there a library that will recursively dump/print an objects properties? I'm looking for something similar to the console.dir() function in Firebug.

    I'm aware of the commons-lang ReflectionToStringBuilder but it does not recurse into an object. I.e., if I run the following:

    public class ToString {
    
        public static void main(String [] args) {
            System.out.println(ReflectionToStringBuilder.toString(new Outer(), ToStringStyle.MULTI_LINE_STYLE));
        }
    
        private static class Outer {
            private int intValue = 5;
            private Inner innerValue = new Inner();
        }
    
        private static class Inner {
            private String stringValue = "foo";
        }
    }
    

    I receive:

    ToString$Outer@1b67f74[ intValue=5
    innerValue=ToString$Inner@530daa ]

    I realize that in my example, I could have overriden the toString() method for Inner but in the real world, I'm dealing with external objects that I can't modify.

  • cherouvim
    cherouvim about 15 years
    @extraneon: sorry, I don't have much experience with Java 1.5 (enums etc)
  • Cornel Masson
    Cornel Masson about 11 years
    Thanks, works well (thumbs up)! I made small changes to mine to eliminate clutter, i.e. in dumpValue() I expanded the list of conditions that will result in the value being converted to a String immediately (no further drill-down). Useful additions are: value.getClass() == java.util.Date.class, value.getClass().isEnum(), etc. You're not normally interested in the internals of those kind of objects, only in the String representation.
  • stolsvik
    stolsvik over 10 years
    Can you specifically release your code to Public Domain, pretty please?
  • stolsvik
    stolsvik over 10 years
    Can you specifically release your code to Public Domain, pretty please?
  • John Rix
    John Rix about 10 years
    If it's just a case of me saying so, then I formally release the above code into the public domain (with no implied warranties and all that sort of stuff).
  • Raghu Kiran
    Raghu Kiran about 10 years
    Thank you was looking for something similar to this. Saved my time :)
  • John Rix
    John Rix about 10 years
    Added extra conditions in dumpValue per @CornelMasson's suggestion
  • dano
    dano about 10 years
    You forgot to include a REALLY important part of this code import java.lang.reflect.Array; import java.lang.reflect.Field; (I just worked out how to edit your post to add it)
  • Aquarius Power
    Aquarius Power about 9 years
    requires apache but sounds interestingly simple
  • Aquarius Power
    Aquarius Power about 9 years
    good, but I had to impose a hard limit of dump recursion 6500 as was causing stackoverflow
  • Bryan W. Wagner
    Bryan W. Wagner about 9 years
    Since writing this post, I've subsequently written a (non-recursive) iterative version to get around the same problem! I'll look through my files for the code and link via Github so you can compare.
  • Aquarius Power
    Aquarius Power about 9 years
    hehe I was about to ask that, but I didnt have time to guess if it would be possible, cool thanks :)
  • Bryan W. Wagner
    Bryan W. Wagner about 9 years
    @AquariusPower I've edited my answer to provide a link to the iterative version. I needed it to dump an extremely large object graph. Hopefully it's helpful!
  • Aquarius Power
    Aquarius Power about 9 years
    yes, I have exactly that problem, an incredibly huge object that uses loads of cpu time and memory, I will I will also clone the object to dump it from a thread... just downloaded with its license to make a jar (as I didnt find a ready one), so can be used together with jmonkeyengine as a lib as said here I guess, thx!
  • Aquarius Power
    Aquarius Power about 9 years
    wow.. I was able to partially dump the object, it created a 180mb text file :o .., I had to add a OutOfMemoryError catch for this: builder.append(tabs).append(label);, also, I am not sure I really need the many arrays on it, so I filtered them out and I still got a 180mb file hehe.. (for that I created a boolean isIncludingArrays, to mimic a zero-length array algorithm flux), so my last thought is if the object could be dumped by parts, appending to the text file (or console), I think the last object hash dumped (object-seek) could be stored to continue the dumping from it on the next call.
  • Bryan W. Wagner
    Bryan W. Wagner about 9 years
    Glad it's a little helpful! It's a complete object graph dump, so it'll explore all possible graph edges. Try launching your program with JVM argument -Xmx1024m or -Xmx2048m to increase your heap size (instead of catching OutOfMemoryError). If you need much larger dumps than that, instead of appending to StringBuilder you can modify it to write to a FileOutputStream.
  • Aquarius Power
    Aquarius Power about 9 years
    btw, the object I am trying to dump is a PhysicsRigidBody from JMonkeyEngine.
  • Kshitiz Sharma
    Kshitiz Sharma almost 9 years
    @JohnRix Adding some comments would make it even more useful. What is ignoreList supposed to do here? Ignore classes that match a given pattern? What is the format of valid input to this argument?
  • John Rix
    John Rix almost 9 years
    Yes, that's the exclusions that I mentioned in the description. Format is [<class>][:<field>]. That is, both class and field are optional (pass at least one or the other of course). Fields must be colon-prefixed. There's no doubt a tidier way of doing this, but I threw it together on a needs basis!
  • Pavel Vlasov
    Pavel Vlasov about 8 years
    Thanks! Some points... 1) there is no option to skip static fields 2) it prints 10 elements of a list while there are only 5.