Created
December 11, 2020 06:30
-
-
Save stanio/c7e810a8597287d515f89229924624af to your computer and use it in GitHub Desktop.
Dump object's internal state
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.example.util; | |
import java.io.IOException; | |
import java.lang.reflect.Array; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Modifier; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Set; | |
/** | |
* Dump of the object internal state. | |
* <pre> | |
* Object myObj; | |
* ... | |
* ObjectDump.dump(myObj, System.out);</pre> | |
*/ | |
public class ObjectDump { | |
private static final int MAX_NEST_LEVEL = 10; | |
private static final int MAX_COLLECTION_ITEMS = 10; | |
private static final int MAX_STRING_LENGTH = 80; | |
private static String lineSep = System.lineSeparator(); | |
private static String indentString = " "; | |
private Appendable out; | |
private StringBuilder currentIndent = new StringBuilder(); | |
private Set<Object> dumped = new HashSet<>(); | |
private int nestLevel = 0; | |
ObjectDump(Appendable out) { | |
this.out = out; | |
} | |
private void increaseIndent() { | |
currentIndent.append(indentString); | |
nestLevel += 1; | |
} | |
private void decreaseIndent() { | |
currentIndent.setLength(currentIndent.length() - indentString.length()); | |
nestLevel -= 1; | |
} | |
private Appendable indent() throws IOException { | |
return out.append(currentIndent); | |
} | |
public static String toString(Object obj) { | |
StringBuilder buf = new StringBuilder(); | |
dump(obj, buf); | |
return buf.toString(); | |
} | |
public static void dump(Object obj, Appendable out) { | |
new ObjectDump(out).dump(obj); | |
} | |
void dump(Object obj) { | |
nestLevel = 0; | |
currentIndent.setLength(0); | |
try { | |
dumpValue(obj); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} finally { | |
dumped.clear(); | |
} | |
} | |
private void dumpValue(Object val) throws IOException { | |
if (val == null || val instanceof Boolean | |
|| val instanceof Number) { | |
out.append(String.valueOf(val)); | |
} else if (val instanceof Character) { | |
dumpChar((char) val); | |
} else if (val instanceof String) { | |
dumpString((String) val); | |
} else if (nestLevel > MAX_NEST_LEVEL) { | |
dumpObjectID(val, '@'); | |
} else if (dumped.contains(val)) { // prevent cycle and repeat | |
dumpObjectID(val, '#'); | |
} else if (val.getClass().isArray()) { | |
dumped.add(val); | |
dumpArray(val); | |
} else if (val instanceof Map) { | |
dumped.add(val); | |
dumpMap((Map<?,?>) val); | |
} else if (val instanceof Collection) { | |
dumped.add(val); | |
dumpCollection((Collection<?>) val); | |
} else { | |
dumped.add(val); | |
dumpObject(val); | |
} | |
} | |
private void dumpSimpleValue(Object val) throws IOException { | |
if (val == null || val instanceof Boolean | |
|| val instanceof Number) { | |
out.append(String.valueOf(val)); | |
} else if (val instanceof Character) { | |
dumpChar((char) val); | |
} else if (val instanceof String) { | |
dumpString((String) val); | |
} else { | |
dumpObjectID(val, '@'); | |
} | |
} | |
private void dumpChar(char ch) throws IOException { | |
out.append('\''); | |
dumpChar0(ch); | |
out.append('\''); | |
} | |
private static final char[] digits = { | |
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' | |
}; | |
private void dumpChar0(char ch) throws IOException { | |
if (ch < 32 || ch > 126) { | |
char[] hexDigit = digits; | |
out.append("\\u").append(hexDigit[(ch >> 12) & 0xf]) | |
.append(hexDigit[(ch >> 8) & 0xf]) | |
.append(hexDigit[(ch >> 4) & 0xf]) | |
.append(hexDigit[ ch & 0xf]); | |
} else { | |
out.append(ch); | |
} | |
} | |
private void dumpString(String val) throws IOException { | |
int length = val.length(); | |
out.append('"'); | |
int max = Math.min(length, MAX_STRING_LENGTH); | |
for (int i = 0; i < max; i++) { | |
dumpChar0(val.charAt(i)); | |
} | |
if (max < length) { | |
out.append("..."); | |
} | |
out.append('"'); | |
} | |
private void dumpMap(Map<?,?> map) throws IOException { | |
int size = map.size(); | |
dumpObjectID(map, '@').append('(').append(String.valueOf(size)).append(") {").append(lineSep); | |
increaseIndent(); | |
int max = Math.min(size, MAX_COLLECTION_ITEMS); | |
for (Map.Entry<?,?> entry : map.entrySet()) { | |
indent(); | |
dumpSimpleValue(entry.getKey()); | |
out.append(": "); | |
dumpValue(entry.getValue()); | |
out.append(',').append(lineSep); | |
if (--max == 0) break; | |
} | |
if (size > MAX_COLLECTION_ITEMS) { | |
indent().append("...").append(lineSep); | |
} | |
decreaseIndent(); | |
indent().append("}"); | |
} | |
private void dumpCollection(Collection<?> col) throws IOException { | |
int size = col.size(); | |
dumpObjectID(col, '@').append('(').append(String.valueOf(size)).append(") [").append(lineSep); | |
increaseIndent(); | |
int max = Math.min(size, MAX_COLLECTION_ITEMS); | |
for (Object item : col) { | |
indent(); | |
dumpValue(item); | |
out.append(',').append(lineSep); | |
if (--max == 0) break; | |
} | |
if (size > MAX_COLLECTION_ITEMS) { | |
indent().append("...").append(lineSep); | |
} | |
decreaseIndent(); | |
indent().append("]"); | |
} | |
private Appendable dumpObjectID(Object obj, char sep) throws IOException { | |
if (obj.getClass().isArray()) { | |
out.append(obj.getClass().getComponentType().getName()).append("[]"); | |
} else { | |
out.append(obj.getClass().getName()); | |
} | |
return out.append(sep).append(String.valueOf(System.identityHashCode(obj))); | |
} | |
private void dumpArray(Object arr) throws IOException { | |
int length = Array.getLength(arr); | |
dumpObjectID(arr, '@').append('(').append(String.valueOf(length)).append(") [").append(lineSep); | |
Class<?> componentType = arr.getClass().getComponentType(); | |
boolean compact = componentType.isPrimitive() | |
|| componentType == Character.class | |
|| Number.class.isAssignableFrom(componentType); | |
increaseIndent(); | |
if (compact) indent(); | |
int max = Math.min(length, MAX_COLLECTION_ITEMS); | |
for (int i = 0; i < max; i++) { | |
if (!compact) indent(); | |
dumpValue(Array.get(arr, i)); | |
if (compact) out.append(", "); | |
else out.append(',').append(lineSep); | |
} | |
if (max < length) { | |
if (compact) out.append("..."); | |
else indent().append("...").append(lineSep); | |
} | |
if (compact) out.append(lineSep); | |
decreaseIndent(); | |
indent().append("]"); | |
} | |
private void dumpObject(Object obj) throws IOException { | |
dumpObjectID(obj, '@').append(" {").append(lineSep); | |
increaseIndent(); | |
Map<String, Collection<Field>> fields = mapFields(obj); | |
dumpFields(obj, fields.get("public"), "public "); | |
dumpFields(obj, fields.get("protected"), "protected "); | |
dumpFields(obj, fields.get(""), ""); | |
dumpFields(obj, fields.get("private"), ""); | |
decreaseIndent(); | |
indent().append("}"); | |
} | |
private void dumpFields(Object obj, Collection<Field> fields, String accessModifier) throws IOException { | |
if (fields == null) return; | |
for (Field field : fields) { | |
Object value; | |
try { | |
field.setAccessible(true); | |
value = field.get(obj); | |
} catch (Exception e) { | |
value = "<error>"; | |
} | |
indent().append(accessModifier).append(field.getName()).append(": "); | |
if (field.getClass().isPrimitive()) { | |
out.append(String.valueOf(value)); | |
} else { | |
dumpValue(value); | |
} | |
out.append(",").append(lineSep); | |
} | |
} | |
private static Map<String, Collection<Field>> mapFields(Object obj) { | |
Map<String, Collection<Field>> fieldMap = new HashMap<>(); | |
Class<?> currentClass = obj.getClass(); | |
while (currentClass != null && currentClass != Object.class) { | |
Field[] declaredFields = currentClass.getDeclaredFields(); | |
for (int i = 0, len = declaredFields.length; i < len; i++) { | |
Field field = declaredFields[i]; | |
if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) | |
continue; | |
fieldMap.computeIfAbsent(getAccessModifier(field.getModifiers()), | |
k -> new ArrayList<>()) | |
.add(field); | |
} | |
currentClass = currentClass.getSuperclass(); | |
} | |
return fieldMap; | |
} | |
private static String getAccessModifier(int modifiers) { | |
if (Modifier.isPublic(modifiers)) { | |
return "public"; | |
} else if (Modifier.isPrivate(modifiers)) { | |
return "private"; | |
} else if (Modifier.isProtected(modifiers)) { | |
return "protected"; | |
} | |
return ""; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment