A DataType Printer in Java
For better or for worse, modern "enterprise" Java applications are full of deeply-nested hierarchies of objects where one instance might
contain a list of other objects which may themselves contain other objects, ad nauseam... debugging an unfamiliar application written in this
(not very object oriented) style means familiarizing yourself with the details of this hierarchy. Yet the tools we have at our disposal
for debugging aren't very robust at introspecting deeply nested objects like this. You can load the code in a debugger (assuming it's
running and deployable somewhere debugger friendly) and poke around the structure, but while the debugger tools are good for picking
out specific values, they don't really do a good job of giving you a holistic view of the object's contents. Of course, there's always
good old System.out.println
, but you can end up writing an awful lot of code to format an object in a way that you can
really see it — JSON.stringify (or the Jackson equivalent, ObjectMapper.writeValueAsString
) can get you part of the way
there if you already have a JSON dependency, but even then the output is missing some useful detail.
I finally decided to bite the bullet and write a generic, Java-only data formatting routine specifically for this purpose. Of course, if you're going to do this in pure Java, you're going to have to lean heavily on introspection, so you want to be careful not to introduce this into production code, but it's relatively easy to add and remove quickly since it's just a static function. I'll start out using the JavaBeans introspector — as it turns out, it's simple to use, but limited (I'll address that below). The simplest case is a single object with nothing but what the JavaBeans specification calls "properties", shown in listing 1.
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.IntrospectionException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
public class DataTypePrinter {
public static String printDataType(Object o) throws IntrospectionException,
IllegalAccessException,
InvocationTargetException {
StringBuffer stringRep = new StringBuffer();
if (o != null) {
BeanInfo objectDesc = Introspector.getBeanInfo(o.getClass());
PropertyDescriptor[] properties = objectDesc.getPropertyDescriptors();
for (PropertyDescriptor property : properties) {
Class propertyType = property.getPropertyType();
if (!propertyType.equals(Class.class)) {
Method getter = property.getReadMethod();
Object value = getter.invoke(o, new Object[] {});
stringRep.append(property.getName() + "(" + propertyType.getSimpleName() + ") = " +
value.toString() + "\n");
}
}
} else {
stringRep.append("null");
}
return stringRep.toString();
}
}
Listing 1: Data Type Printer for simple types
This handles the simple base case when the "data type" under consideration just has getters for scalar properties. I'm not trying to
introspect private fields; with a bit of fiddling you could do that, but that's not helpful for what I'm trying to do here. Even in this simple code listing, there are a couple
of interesting things worth pointing out: first, I have to check the type of the property and make sure it isn't Class
, since
the JavaBeans introspector will return a getter for the class, which I don't want to print out (in fact, when I make this recursive below,
including it would cause an infinite loop). Also, I invoke propertyType.getSimpleName()
to print out the unqualified class name of each
property after it's property name — package names are usually clutter and I don't want them here.
Listing 1 only works when the class includes simple properties, not properties that have properties themselves. It's not hard to extend this to introspect properties recursively as shown in listing 2:
public static String dataType(String prefix, Object o)
...
if (!propertyType.equals(Class.class)) {
Method getter = property.getReadMethod();
Object value = getter.invoke(o, new Object[] {});
if (value != null) {
if (propertyType.isPrimitive() || propertyType.equals(String.class)) {
stringRep.append(prefix + "." + property.getName() + "(" +
propertyType.getSimpleName() + ") = " + value.toString() + "\n");
} else {
stringRep.append(printDataType(prefix + "." + property.getName(), value));
}
} else {
stringRep.append(prefix + "." + property.getName() + "(" +
propertyType.getSimpleName() + ") = null\n");
}
}
Listing 2: Recursive Data Type Printer
Here I've added a null check along with a prefix on each call, so that nested structures can print out their containing element. The effect is already pretty useful, as shown in listing 3:
class Z {
private int s = 12;
public int getS() { return s; }
}
class Y {
private int q = 9;
private int r = 10;
private Z z = new Z();
public int getQ() { return q; }
public int getR() { return r; }
public Z getZ() { return z; }
}
class X {
private int a = 1;
private int b = 3;
private String c = "abc";
private Y y = new Y();
public int getA() { return a; }
public int getB() { return b; }
public String getC() { return c; }
public Y getY() { return y; }
}
...
X x = new X();
System.out.println(printDataType("x", x);
/*
x.a(int) = 1
x.b(int) = 3
x.c(String) = abc
x.y.q(int) = 9
x.y.r(int) = 10
x.y.z.s(int) = 12
*/
Listing 3: Recursive output sample
This is why I omitted the Class
getter above — first because it's not helpful in the output and second because it
causes listing 2 to enter an infinite loop since java.lang.Class
has its own getClass
getter...
So far, so good - but what about collection types? With a little bit of refactoring, I can include a nice output for Iterable
components. Before printing a property, I check to see if it's an implementation of Iterable: if so, I iterate over the contents and invoke
printDataType
on each. This is fully recursive, so if the iterated contents contain objects (or other iterables!), they'll
output correctly.
for (PropertyDescriptor property : properties) {
Class propertyType = property.getPropertyType();
if (!propertyType.equals(Class.class)) {
Method getter = property.getReadMethod();
if (o instanceof Iterable) {
Iterator<Object> it = ((Iterable) o).iterator();
int i = 0;
while (it.hasNext()) {
stringRep.append(printDataType(prefix + "[" + i++ + "]",
it.next()));
}
} else {
Object value = getter.invoke(o, new Object[] {});
if (value != null) {
Listing 4: iterate over collections.
Note that I check for iterable on the parent object - this works for nested objects because the reader will have been invoked recursively,
and has the nice side effect that I can pass a List
object directly into the printDataType and it will output correctly.
Listing 4 as presented only works for collections if the listed type is a class with properties, though; it fails if the iterable is a native type or, as is probably most
common, a String. The problem here is that my top-level handler assumes that what it's passed is either an iterable or an introspectable
Java object. To handle native collections and strings (and, while we're at it, Dates) correctly, I can introduce a third case at the
top:
StringBuffer stringRep = new StringBuffer();
if (o != null) {
Class objectType = o.getClass();
if (objectType.isPrimitive() ||
objectType.equals(String.class) ||
objectType.equals(Date.class)) {
stringRep.append(prefix + " (" + objectType.getSimpleName() +
") = " + o.toString() + "\n");
} else if (o instanceof Iterable) {
Iterator it = ((Iterable) o).iterator();
int i = 0;
while (it.hasNext()) {
stringRep.append(printDataType(prefix + "[" + i++ + "]", it.next()));
}
} else {
BeanInfo objectDesc = Introspector.getBeanInfo(o.getClass());
PropertyDescriptor[] properties = objectDesc.getPropertyDescriptors();
Listing 5: Support for native collection types
This works for most java collections, but not for native arrays. As you probably know, Java handles arrays somewhat differently than
most everything else for efficiency reasons, but Java reflection includes an Array
that allows you to access members of
arrays by index. So I can add a fourth "base case" here and include native arrays:
if (objectType.isPrimitive() ||
objectType.equals(String.class) ||
objectType.equals(Date.class)) {
stringRep.append(prefix + " (" + objectType.getSimpleName() + ") = " + o.toString() + "\n");
} else if (objectType.isArray()) {
if (Array.getLength(o) == 0) {
stringRep.append(prefix + " = empty array\n");
}
if (objectType.getComponentType().isPrimitive() ||
objectType.getComponentType().equals(String.class) ||
objectType.getComponentType().equals(Date.class)) {
for (int i = 0; i < Array.getLength(o); i++) {
stringRep.append(prefix + "[" + i + "] = " +
Array.get(o, i).toString() + "\n");
}
} else {
for (int i = 0; i < Array.getLength(o); i++) {
stringRep.append(printDataType(prefix + "[" + i + "]",
Array.get(o, i)));
}
}
} else if (o instanceof Iterable) {
int i = 0;
Listing 6: Support for native array types
There's nothing really tricky here; I just have to use java.lang.reflect.Array
to get the length of and access each member of
the array instance.
While we're in here, we might as well go ahead and support Map
types — they're not Iterable
s, but by
iterating over their Key
values we can make it look like they are:
}
} else if (o instanceof Map) {
Set<Object> keys = ((Map) o).keySet();
if (keys.isEmpty()) {
stringRep.append(prefix + " = empty map\n");
}
for (Object key : keys) {
stringRep.append(printDataType(prefix + "[" + key.toString() + "]",
((Map) o).get(key)));
}
} else
Listing 7: Support for Map types
This is a complete, working, useful data type printer. However, one thing I found when I started actually trying to use this is that there are quite a few getters that don't turn up as JavaBean properties,
though — especially Collection
types. I suppose this makes sense, since collection types aren't really Java Beans,
but that means that I have to "roll my own" introspector. As it turns out, that's not too difficult, although it's a bit more code:
else {
Method[] methods = objectType.getMethods();
for (Method method : methods) {
if (method.getName().startsWith("get") &&
method.getParameterCount() == 0) {
String propertyName = method.getName().substring(3,4).toLowerCase() +
method.getName().substring(4);
Object value = method.invoke(o, new Object[] {} );
// value.getClass().isPrimitive returns false, even if
// method.getReturnType().isPrimitive returns true
if (method.getReturnType().isPrimitive()) {
stringRep.append(prefix + "." + propertyName + " (" +
method.getReturnType().getSimpleName() + ") = " + value.toString() + "\n");
} else {
stringRep.append(printDataType(prefix + "." + propertyName, value));
}
} else if (method.getName().startsWith("is") &&
method.getParameterCount() == 0 &&
method.getReturnType().equals(boolean.class)) {
String propertyName = method.getName().substring(2,3).toLowerCase() +
method.getName().substring(3);
stringRep.append(prefix + "." + propertyName + " (boolean) = " +
(method.invoke(o, new Object[] {}).equals(true) ? "true" : "false") + "\n");
}
}
}
Listing 8: Introspector without JavaBeans
Here I do pretty much what (I presume) Introspector
does and make some inferences about what each method does: if it starts
with get or is and doesn't take any parameters, it's a property that I want to introspect. Listing 9 is the final data
type printer which I've actually found to be particularly helpful in making sense of new, large codebases.
public static String printDataType(String prefix, Object o) {
StringBuilder stringRep = new StringBuilder();
try {
if (o != null) {
Class objectType = o.getClass();
if (objectType.isPrimitive() ||
objectType.equals(String.class) ||
objectType.equals(Date.class)) {
stringRep.append(prefix + " (" + objectType.getSimpleName() +
") = " + o.toString() + "\n");
} else if (objectType.isArray()) {
if (Array.getLength(o) == 0) {
stringRep.append(prefix + " = empty array\n");
}
if (objectType.getComponentType().isPrimitive() ||
objectType.getComponentType().equals(String.class) ||
objectType.getComponentType().equals(Date.class)) {
for (int i = 0; i < Array.getLength(o); i++) {
stringRep.append(prefix + "[" + i + "] = " +
Array.get(o, i).toString() + "\n");
}
} else {
for (int i = 0; i < Array.getLength(o); i++) {
stringRep.append(printDataType(prefix + "[" + i + "]",
Array.get(o, i)));
}
}
} else if (o instanceof Iterable) {
int i = 0;
Iterator it = ((Iterable) o).iterator();
if (!it.hasNext()) {
stringRep.append(prefix + " = empty set\n");
}
while (it.hasNext()) {
stringRep.append(printDataType(prefix + "[" + i++ + "]",
it.next()));
}
} else if (o instanceof Map) {
Set<Object> keys = ((Map) o).keySet();
if (keys.isEmpty()) {
stringRep.append(prefix + " = empty map\n");
}
for (Object key : keys) {
stringRep.append(printDataType(prefix + "[" + key.toString() + "]",
((Map) o).get(key)));
}
} else if (o instanceof Class) {
// Do nothing otherwise the stack blows up
} else {
Method[] methods = objectType.getMethods();
for (Method method : methods) {
if (method.getName().startsWith("get") &&
method.getParameterCount() == 0) {
String propertyName = method.getName().substring(3,4).toLowerCase() +
method.getName().substring(4);
Object value = method.invoke(o, new Object[] {} );
// value.getClass().isPrimitive returns false, even if
// method.getReturnType().isPrimitive returns true
if (method.getReturnType().isPrimitive()) {
stringRep.append(prefix + "." + propertyName + " (" +
method.getReturnType().getSimpleName() + ") = " +
value.toString() + "\n");
} else {
stringRep.append(printDataType(prefix + "." + propertyName, value));
}
} else if (method.getName().startsWith("is") &&
method.getParameterCount() == 0 &&
method.getReturnType().equals(boolean.class)) {
String propertyName = method.getName().substring(2,3).toLowerCase() +
method.getName().substring(3);
stringRep.append(prefix + "." + propertyName + " (boolean) = " +
(method.invoke(o, new Object[] {}).equals(true) ? "true" : "false") + "\n");
}
}
}
} else {
return prefix + " = null\n";
}
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
return stringRep.toString();
}
Listing 9: Full data type printer
I mentioned JSON at the beginning of this post - it's worth noting that it would be relatively simple to modify this to output syntactically correct JSON, or even allow the caller to select a representation.