Friday, March 29, 2024

Implement a Dynamic toString() using JavaBeans Introspection

Implement a Dynamic toString() using JavaBeans Introspection

In the Building a Better toString() Method tutorial, we learned about the importance of overriding the default toString() method in our Java classes. Using Java’s powerful Reflection API, we were able to create a generic toString() that displayed an object’s declared fields and their values. Another way to achieve the same result is to use the JavaBeans API. It supplies a set of classes and interfaces that provide introspection, much in the same way that Java Reflection does. Originally meant to be utilized by builder tools and other automated environments to provide information on a bean, we can make use of them to reveal details about any class. In today’s article, we’ll construct a dynamic toString() method using the BeanInfo class’s getPropertyDescriptors() method.

Obtaining Information about a Bean or Class

Information about a bean or class may be obtained by invoking the Introspector’s static getBeanInfo() method. It has a few different signatures, but the one that we are using accepts two arguments: the object Class and that of the stopClass. The method returns a BeanInfo object that holds details about the class’s properties and exposed methods, but only up to the supplied stopClass, which is the ancestor class at which to stop.

The getBeanInfo() may throw an IntrospectionException so you have to handle it by either throwing it back to the caller or surrounding it with a try/catch block. I chose the latter because I would hate to force every method that calls my function to have to handle the exception.

Here is a class that contains the static objToStringUsingBeanIntrospector() delegate method. When invoked, the method returns the class details produced by the Bean Introspector’s getBeanInfo() method. In the event of an IntrospectionException, our method returns the supplied class name along with the exception message:

package com.robgravelle.tostring;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;

public class ToStringTests {

  public static String objToStringUsingBeanIntrospector(Object obj) {
      BeanInfo info;
      try {
          info = Introspector.getBeanInfo(obj.getClass(), Object.class);
      } catch (IntrospectionException e) {
          return e.getClass().getSimpleName() + ": " + e.getMessage();
      }
      //...

Obtaining Field Names and Types

From a bean perspective, a PropertyDescriptor describes one property that the bean may expose via an accessor (getter) and/or mutator (setter) method. If that sounds like a class field, it’s because it is! Hence, calling the BeanInfo instance’s getPropertyDescriptors() retrieves all of the declared fields of a class. From there we can get individual fields’ names and types within a for loop:

PropertyDescriptor[] props = info.getPropertyDescriptors();
for (PropertyDescriptor pd : props) {
    String name = pd.getName();
    Class<?> type = pd.getPropertyType();
}

Obtaining a Field’s Value

The PropertyDescriptor has an instance method called getReadMethod() that returns a reference to the field’s getter(). We can invoke it to obtain the field’s value. Just make sure to check that there is a getter for that particular field and to surround the invoke() call with a try/catch block because it could throw a few different types of exceptions.

for (PropertyDescriptor pd : props) {
   String name = pd.getName();
   Class<?> type = pd.getPropertyType();
   Method getter = pd.getReadMethod();
   
   if (getter != null) {
      Object value;
      try {
          value = getter.invoke(obj);
      } catch (IllegalAccessException | IllegalArgumentException
               | InvocationTargetException e) {
          return e.getClass().getSimpleName() + ": " + e.getMessage();
      }
   }
}

The objToStringUsingBeanIntrospector() Method

Here is the full source for the objToStringUsingBeanIntrospector() method, including output formatting:

public static String objToStringUsingBeanIntrospector(Object obj) {
        StringBuilder result = new StringBuilder();
        String newLine = System.getProperty("line.separator");

        result.append(obj.getClass().getName());
        result.append(" Object {");
        result.append(newLine);
         
        BeanInfo info;
        try {
            info = Introspector.getBeanInfo(obj.getClass(), Object.class);
        } catch (IntrospectionException e) {
            return e.getClass().getSimpleName() + ": " + e.getMessage();
        }
  PropertyDescriptor[] props = info.getPropertyDescriptors();
  for (PropertyDescriptor pd : props) {
      String name = pd.getName();
      Method getter = pd.getReadMethod();
      Class<?> type = pd.getPropertyType();
    
      if (getter != null) {
        Object value;
        try {
            value = getter.invoke(obj);
        } catch (IllegalAccessException | IllegalArgumentException
              | InvocationTargetException e) {
            return e.getClass().getSimpleName() + ": " + e.getMessage();
        }

        result.append("    " + name);
        result.append(": ");
        result.append(value);
        result.append(", type: ");
        result.append(type);
        result.append(newLine);
     }
  }
  result.append("}");

  return result.toString();
}

Calling the objToStringUsingBeanIntrospector() Method

This example uses the same BankAccount class that we tested in the Building a Better toString() Method tutorial. Just pass the class instance to the function and it returns a nicely formatted string!

public static void main(String[] args) {
  Account account = new BankAccount(1234, "Money Baggs", 1000000);
  System.out.println( objToStringUsingBeanIntrospector( account ) );
}

And here is the output:

ca.gc.cbsa.banking.models.BankAccount Object {
    accountHolderName: Money Baggs, type: class java.lang.String
    accountNumber: 1234, type: int
    balance: 1000000.0, type: double
}

Conclusion

Now that we’ve covered a couple of ways of coding our own generic toString() method, we’ll explore some of the outstanding libraries that others have produced.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured