70

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.

Benj
  • 1,184
  • 7
  • 26
  • 57
Kevin
  • 30,111
  • 9
  • 76
  • 83

10 Answers10

43

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 ;)

Schism
  • 275
  • 7
  • 15
cherouvim
  • 31,725
  • 15
  • 104
  • 153
  • Nice. I feel silly now because I use XStream a lot and I didn't even think of it. – Kevin Mar 02 '09 at 16:49
  • Does that work with enum values? I believe (default) XML serialization using XMLEncoder.writeObject and hibernate have some problems with serializing enum values/types. – extraneon Mar 02 '09 at 21:13
  • @extraneon: sorry, I don't have much experience with Java 1.5 (enums etc) – cherouvim Mar 03 '09 at 06:08
42

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;
    }
}
John Rix
  • 6,271
  • 5
  • 40
  • 46
  • 5
    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. – Cornel Masson Apr 10 '13 at 14:41
  • Can you specifically release your code to Public Domain, pretty please? – stolsvik Oct 08 '13 at 10:05
  • 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). – John Rix Mar 27 '14 at 15:28
  • Thank you was looking for something similar to this. Saved my time :) – Raghu Kiran Apr 02 '14 at 13:18
  • Added extra conditions in dumpValue per @CornelMasson's suggestion – John Rix Apr 03 '14 at 09:54
  • @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? – Kshitiz Sharma Jul 22 '15 at 14:39
  • Yes, that's the exclusions that I mentioned in the description. Format is `[][:]`. 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! – John Rix Jul 22 '15 at 22:04
  • 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. – Pavel Vlasov Apr 05 '16 at 09:40
21

You can use ReflectionToStringBuilder with a custom ToStringStyle, for example:

class MyStyle extends ToStringStyle {
    private final static ToStringStyle instance = new MyStyle();

    public MyStyle() {
        setArrayContentDetail(true);
        setUseShortClassName(true);
        setUseClassName(false);
        setUseIdentityHashCode(false);
        setFieldSeparator(", " + SystemUtils.LINE_SEPARATOR + "  ");
    }

    public static ToStringStyle getInstance() {
        return instance;
    };

    @Override
    public void appendDetail(StringBuffer buffer, String fieldName, Object value) {
        if (!value.getClass().getName().startsWith("java")) {
            buffer.append(ReflectionToStringBuilder.toString(value, instance));
        } else {
            super.appendDetail(buffer, fieldName, value);
        }
    }

    @Override
    public void appendDetail(StringBuffer buffer, String fieldName, Collection value) {
        appendDetail(buffer, fieldName, value.toArray());
    }
}

And then you invoke it like:

ReflectionToStringBuilder.toString(value, MyStyle.getInstance());

Beware of circular references though!


You can also use json-lib (http://json-lib.sourceforge.net) and just do:

JSONObject.fromObject(value);
Peter Oehlert
  • 16,368
  • 6
  • 44
  • 48
  • 2
    here is another version of a custom ToStringStyle http://stackoverflow.com/questions/3149951/java-tostring-tostringbuilder-not-sufficient-wont-traverse/3514475#3514475 – remipod Jan 27 '11 at 12:30
  • @remipod's link says: "ToStringStyle will take care of values, already processed, and will not allow recursion" – Vadzim Dec 03 '13 at 10:31
  • It appears that `ToStringStyle` has built-in protection from circular references: http://grepcode.com/file/repo1.maven.org/maven2/commons-lang/commons-lang/2.6/org/apache/commons/lang/builder/ToStringStyle.java#135 – Vadzim Dec 05 '13 at 17:32
  • Here is a modified version featuring single buffer reuse, thread safety, multi-line indent and use of object's `toString` method if it has been overridden: http://stackoverflow.com/a/20407041/603516 – Vadzim Dec 05 '13 at 17:52
13

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();
}
Community
  • 1
  • 1
user519500
  • 131
  • 1
  • 3
  • Can you specifically release your code to Public Domain, pretty please? – stolsvik Oct 08 '13 at 10:06
  • 1
    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) – dano Apr 22 '14 at 17:30
7

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.

Bryan W. Wagner
  • 867
  • 1
  • 12
  • 13
  • good, but I had to impose a hard limit of dump recursion 6500 as was causing stackoverflow – Aquarius Power Feb 23 '15 at 20:33
  • 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. – Bryan W. Wagner Feb 24 '15 at 02:42
  • hehe I was about to ask that, but I didnt have time to guess if it would be possible, cool thanks :) – Aquarius Power Feb 24 '15 at 02:48
  • 1
    @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! – Bryan W. Wagner Mar 01 '15 at 02:39
  • 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](http://programmers.stackexchange.com/a/40571/112610) I guess, thx! – Aquarius Power Mar 01 '15 at 17:58
  • 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. – Aquarius Power Mar 01 '15 at 18:31
  • 1
    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. – Bryan W. Wagner Mar 01 '15 at 18:45
  • btw, the object I am trying to dump is a PhysicsRigidBody from JMonkeyEngine. – Aquarius Power Mar 01 '15 at 18:45
4

You should use RecursiveToStringStyle:

System.out.println(ReflectionToStringBuilder.toString(new Outer(), new RecursiveToStringStyle()));
Eduardo
  • 93
  • 1
  • 5
4

Maybe you could use an XML binding framework like XStream, Digester or JAXB for that.

smonff
  • 3,399
  • 3
  • 36
  • 46
Fabian Steeg
  • 44,988
  • 7
  • 85
  • 112
2

You could use Gson to represent your object in json format :

new GsonBuilder().setPrettyPrinting().create().toJson(yourObject);
Ofek Ron
  • 8,354
  • 13
  • 55
  • 103
1

I recommend you to use the GSON Lib fo Java.

if You use Maven you can use this.

Or you can download the Jar file from here.

Here example how to use it:

Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(obj);
System.out.println(json);
0
JSONObject.fromObject(value)

Does not work for Map objects with other keys than String. Maybe JsonConfig can handle this.