转储Java对象的属性

70

有没有一个库可以递归地转储/打印对象的属性?我正在寻找类似于Firebug中console.dir()函数的东西。

我知道commons-lang ReflectionToStringBuilder,但它不会递归到一个对象中。也就是说,如果我运行以下内容:

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";
    }
}

我收到:

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

我意识到在我的例子中,我可以覆盖Inner的toString()方法,但在现实世界中,我正在处理无法修改的外部对象。

10个回答

43

你可以尝试使用XStream

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

打印输出:

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

您也可以以JSON格式输出。

注意避免循环引用 ;)


很好。我现在感到有些傻,因为我经常使用XStream,却从未想过它。 - Kevin
这是否适用于枚举值?我相信使用XMLEncoder.writeObject和Hibernate进行(默认)XML序列化时,会存在一些序列化枚举值/类型的问题。 - extraneon
@extraneon: 抱歉,我对Java 1.5(包括枚举等)没有太多经验。 - cherouvim

42

我尝试使用最初建议的XStream,但是发现我想要转储的对象图包括对XStream marshaller本身的引用,它对此并不友好(为什么它必须抛出异常而不是忽略它或记录一个好的警告,我不确定)。

然后我尝试了上面用户user519500的代码,但发现我需要进行一些调整。这里有一个类可以添加到项目中,提供以下额外功能:

  • 可以控制最大递归深度
  • 可以限制数组元素的输出
  • 可以忽略任何类、字段或类+字段组合的列表 - 只需传递一个带有任意组合的类名数组、classname+fieldname对(用冒号分隔)或具有冒号前缀的字段名,例如:[<classname>][:<fieldname>]
  • 不会输出相同的对象两次(输出指示先前访问过对象,并提供哈希码以进行关联)- 这避免了循环引用导致的问题

您可以使用以下两种方法之一调用此函数:

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

如上所述,使用此功能时需要注意堆栈溢出的问题,因此请使用最大递归深度功能以最小化风险。

希望有人会觉得这个有用!

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

5
谢谢,运行得很好(点赞)!我对自己的代码进行了一些小改动以消除混乱,例如在dumpValue()中,我扩展了条件列表,即会立即将value转换为字符串(不再进一步查看)的情况。有用的附加功能包括:value.getClass() == java.util.Date.classvalue.getClass().isEnum()等。你通常对这些类型的对象的内部不感兴趣,只对字符串表示形式感兴趣。 - Cornel Masson
你可以特别将你的代码发布到公共领域吗?拜托了。 - stolsvik
谢谢,我正在寻找类似的东西。这节省了我的时间 :) - Raghu Kiran
根据@CornelMasson的建议,在dumpValue中添加了额外的条件。 - John Rix
是的,那就是我在描述中提到的排除项。格式为 [<class>][:<field>],也就是说,类和字段都是可选的(当然至少要传递一个)。字段必须以冒号为前缀。肯定有更简洁的方法来做到这一点,但我是根据需求随便拼凑出来的! - John Rix
显示剩余3条评论

21
你可以使用ReflectionToStringBuilder和自定义的ToStringStyle,例如:
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());
    }
}

然后你可以像这样调用它:

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

注意循环引用!


你还可以使用json-lib (http://json-lib.sourceforge.net), 然后执行以下操作:

JSONObject.fromObject(value);

2
这是另一个自定义ToStringStyle的版本:https://dev59.com/oHA75IYBdhLWcg3ws7Yh#3514475 - remipod
@remipod的链接说:“ToStringStyle会处理已经处理过的值,并且不会允许递归”。 - Vadzim
似乎 ToStringStyle 已经内置了对循环引用的保护:http://grepcode.com/file/repo1.maven.org/maven2/commons-lang/commons-lang/2.6/org/apache/commons/lang/builder/ToStringStyle.java#135 - Vadzim
这里是一个修改过的版本,具有单个缓冲区重用、线程安全、多行缩进以及如果已被覆盖则使用对象的 toString 方法。链接:https://dev59.com/oHA75IYBdhLWcg3ws7Yh#20407041 - Vadzim

13

这将打印出一个对象的所有字段(包括对象数组)。

这是从此线程中修复了Ben Williams帖子的版本。

注意:此方法使用递归,因此如果您有一个非常深的对象图,可能会出现堆栈溢出错误(无恶意笑话;) 如果出现此情况,则需要使用VM参数-Xss10m。如果您使用的是eclipse,请将其放入运行配置>增强(选项卡)VM增强框并按应用。

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

你可以具体地将你的代码放入公共领域吗?拜托了。 - stolsvik
1
你忘记了包含这段代码的一个非常重要的部分 import java.lang.reflect.Array; import java.lang.reflect.Field;(我刚刚想到了如何编辑您的帖子以添加它) - dano

7
我希望对这个问题有一个优雅的解决方案,它需要满足以下条件:
  • 不使用任何外部库
  • 使用反射来访问字段,包括超类字段
  • 使用递归来遍历对象图,每次调用只使用一个堆栈帧
  • 使用IdentityHashMap处理反向引用并避免无限递归
  • 适当处理原始类型、自动装箱、CharSequences、枚举和null值
  • 允许您选择是否解析静态字段
  • 足够简单,可以根据格式化偏好进行修改

我编写了以下实用程序类:

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

我在许多类上进行了测试,对我来说非常高效。例如,尝试使用它转储主线程:
public static void main(String[] args) throws Exception {
    System.out.println(dump(Thread.currentThread()));
}

编辑

自从写下这篇文章以来,我有理由创建这个算法的迭代版本。递归版本的深度受总堆栈框架的限制,但您可能需要转储一个非常大的对象图。为了处理我的情况,我修改了算法,使用堆栈数据结构代替运行时堆栈。这个版本是时间有效的,受堆大小而不是堆栈帧深度的限制。

您可以下载和使用迭代版本


1
@AquariusPower 我编辑了我的答案,提供了迭代版本的链接。我需要它来转储一个非常大的对象图。希望它有所帮助! - Bryan W. Wagner
是的,我正好有这个问题,一个非常庞大的对象使用了大量的CPU时间和内存,我也会克隆该对象以便从线程中卸载它...刚刚下载了带有许可证的文件制作成JAR(因为我没有找到现成的),所以可以像这里所说一样与jmonkeyengine一起作为库使用,谢谢! - Aquarius Power
哇..我成功地部分转储了对象,它创建了一个180mb的文本文件 :o .., 我不得不添加一个OutOfMemoryError catch来处理这个问题:builder.append(tabs).append(label);, 另外,我不确定我真的需要它上面的许多数组,所以我过滤掉了它们,但我仍然得到了一个180mb的文件,哈哈.. (为此,我创建了一个布尔值isIncludingArrays,来模拟零长度数组算法流),所以我的最后想法是,如果对象可以被分段转储,附加到文本文件(或控制台)中,我认为最后一个对象哈希转储(object-seek)可以被存储下来,以便在下一次调用时继续转储。 - Aquarius Power
1
很高兴它能有一点帮助!这是一个完整的对象图转储,因此它将探索所有可能的图边缘。尝试使用JVM参数-Xmx1024m或-Xmx2048m启动程序以增加堆大小(而不是捕获OutOfMemoryError)。如果您需要比那更大的转储,请修改StringBuilder以写入FileOutputStream。 - Bryan W. Wagner
顺便提一下,我想要转储的对象是来自JMonkeyEngine的PhysicsRigidBody。 - Aquarius Power
显示剩余3条评论

4
你应该使用RecursiveToStringStyle:
System.out.println(ReflectionToStringBuilder.toString(new Outer(), new RecursiveToStringStyle()));

需要Apache,但听起来非常简单。 - Aquarius Power

4
也许你可以使用像XStream, Digester 或者 JAXB 这样的XML绑定框架来完成这个任务。

2
你可以使用 Gson 将你的对象表示为 JSON 格式:
new GsonBuilder().setPrettyPrinting().create().toJson(yourObject);

1
我建议您使用GSON库来处理Java相关的内容。
如果您使用Maven,可以使用this
或者您可以从here下载Jar文件。
以下是如何使用它的示例:
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(obj);
System.out.println(json);

0
JSONObject.fromObject(value)

对于键不是字符串的Map对象无效。也许JsonConfig可以处理此问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接