Skip to main content

Reflection and dynamically changing classes

Posted by kirillcool on August 10, 2005 at 8:29 AM PDT

Half the dynamic XML binding libraries use reflection to marshal Java object trees to XML and unmarshal XML back to objects. These libraries expect to work on JavaBean-compliant classes (or accompanying configuration files), and use reflection heavily both during marshaling (retrieving attributes and elements) and unmarshaling (creating elements and populating attributes). Since reflection is an expensive operation, most of the results are cached for future reuse.

There are three main types of reflection operations:

  • Getting the Class object, using either Class.forName or some ClassLoader
  • Getting the Constructor, Method and Field objects from Class
  • Invoking newInstance on Constructor, invoke on Method or set* on Field

In most cases, the first two operation types return results that can be cached and reused, while the third type must be invoked for each specific operation (since the parameter values change). However, improper caching of the first two operation type results can lead to erroneous and unpredictable results when the underlying classes are changed during runtime.

One of the features of last-generation application servers is hot deploy, that allows swapping the class files without rebooting the whole application. In case of XML binders, this may mean either changes to existing get/set pairs implementation, or additional get/set pairs. Here is an example of such change, and the way it is handled in XStream:

First, we generate very simple test class (during run-time) with a single integer value attribute:

     
      Logger _logger = Logger.getLogger(Main.class.getName());
      // create first version of class
      String className = "TestClass";
      String dir = "C:\\temp";
      BufferedWriter bw1 = new BufferedWriter(new FileWriter(dir
            + File.separator + className + ".java"));
      bw1.write("public class " + className + " {");
      bw1.newLine();
      bw1.write("  private int value;");
      bw1.newLine();
      bw1.write("  public int getValue() {");
      bw1.newLine();
      bw1.write("    return this.value;");
      bw1.newLine();
      bw1.write("  }");
      bw1.newLine();
      bw1.write("  public void setValue(int value) {");
      bw1.newLine();
      bw1.write("    this.value = value;");
      bw1.newLine();
      bw1.write("  }");
      bw1.newLine();
      bw1.write("}");
      bw1.newLine();
      bw1.close();

Here is how the generated class looks like:

     
public class TestClass {
  private int value;
  public int getValue() {
    return this.value;
  }
  public void setValue(int value) {
    this.value = value;
  }
}

Now, we compile it using tools.jar:

     
      // compile
      compStatus = com.sun.tools.javac.Main.compile(new String[] { dir
            + File.separator + className + ".java" });
      _logger.info("Compilation status is " + compStatus);

And load it using URLClassLoader:

     
      // load
      ClassLoader classLoader1 = new URLClassLoader(new URL[] { new File(dir)
            .toURL() });
      Class clazz1 = classLoader1.loadClass(className);
      _logger.info("Loaded " + clazz1.getName() + " [" + clazz1.hashCode()
            + "]");

The resulting class is loaded, and its hash code is

     
INFO [18:04:27.480] [XMain.main] Loaded TestClass [13577344]

Now, we fetch its constructor and get/set pair:

     
      Constructor ctr1 = clazz1.getConstructor(new Class[0]);
      Method getter1 = clazz1.getMethod("getValue", new Class[0]);
      Method setter1 = clazz1
            .getMethod("setValue", new Class[] { int.class });

Create new object, set value to 10 and check that the set succeeded:

     
      Object obj1 = ctr1.newInstance(new Object[0]);
      setter1.invoke(obj1, new Object[] { new Integer(10) });
      Object val1 = getter1.invoke(obj1, new Object[0]);
      _logger.info("Fetched value " + ((Integer) val1).intValue());

The result of get is, as expected, 10:

     
INFO [18:04:27.480] [XMain.main] Fetched value 10

Now, we create a new XStream object, configure it to handle our loaded class and marshal our object to XML:

     
      XStream xstream = new XStream();
      xstream.alias("myobj", clazz1);
      String xml1 = xstream.toXML(obj1);
      _logger.info("First XML : " + xml1);

The result is, as expected:

     
INFO [18:04:27.652] [XMain.main] First XML : <myobj>
  <value>10</value>
</myobj>

Now, we unload the TestClass class (WeakReference is used to make sure that the class is unloaded):

     
      ReferenceQueue weakQueueCl = new ReferenceQueue();
      WeakReference weakRefCl = new WeakReference(clazz1,
            weakQueueCl);
      weakRefCl.enqueue();

      // Clear strong references
      clazz1 = null;

      // Invoke garbage collector - the reference will be queued
      System.gc();

      try {
         Reference ref = weakQueueCl.remove();
         ref.clear();
      } catch (InterruptedException e) {
         e.printStackTrace();
         return;
      }
      _logger.info("Reference to the class cleaned");

A few centiseconds later, the class is unloaded:

     
INFO [18:04:27.699] [XMain.main] Reference to the class cleaned

Now, we generate a new version of the same class, this time changing the getter implementation:

     
      BufferedWriter bw2 = new BufferedWriter(new FileWriter(dir
            + File.separator + className + ".java"));
      bw2.write("public class " + className + " {");
      bw2.newLine();
      bw2.write("  private int value;");
      bw1.newLine();
      bw2.write("  public int getValue() {");
      bw2.newLine();
      bw2.write("    return (this.value + 10);");
      bw2.newLine();
      bw2.write("  }");
      bw2.newLine();
      bw2.write("  public void setValue(int value) {");
      bw2.newLine();
      bw2.write("    this.value = value;");
      bw2.newLine();
      bw2.write("  }");
      bw2.newLine();
      bw2.write("}");
      bw2.newLine();
      bw2.close();

New version of our data class is

     
public class TestClass {
  private int value; 
  public int getValue() {
    return (this.value + 10);
  }
  public void setValue(int value) {
    this.value = value;
  }
}

Compile and load it, using a new URLClassLoader:

     
      // compile
      compStatus = com.sun.tools.javac.Main.compile(new String[] { dir
            + File.separator + className + ".java" });
      _logger.info("Compilation status is " + compStatus);

      // load
      ClassLoader classLoader2 = new URLClassLoader(new URL[] { new File(dir)
            .toURL() });
      // MyClassLoader classLoader2 = new MyClassLoader(Main.class
      // .getClassLoader(), dir);
      Class clazz2 = classLoader2.loadClass(className);
      _logger.info("Loaded " + clazz2.getName() + " [" + clazz2.hashCode()
            + "]");

The new class is loaded, and its hash code is different (as expected):

     
INFO [18:04:28.011] [XMain.main] Loaded TestClass [26281671]

Once again, we fetch the constructor and get/set pair:

     
      Constructor ctr2 = clazz2.getConstructor(new Class[0]);
      Method getter2 = clazz2.getMethod("getValue", new Class[0]);
      Method setter2 = clazz2
            .getMethod("setValue", new Class[] { int.class });

And test that the changes to the getter function were loaded:

     
      Object obj2 = ctr2.newInstance(new Object[0]);
      setter2.invoke(obj2, new Object[] { new Integer(10) });
      Object val2 = getter2.invoke(obj2, new Object[0]);
      _logger.info("Fetched value " + ((Integer) val2).intValue());

The result is, as expected, 20:

     
INFO [18:04:28.011] [XMain.main] Fetched value 20

Now, we create a new XStream object and ask it to marshal the new object:

     
      XStream xstream2 = new XStream();
      xstream2.alias("myobj", clazz2);
      String xml2 = xstream2.toXML(obj2);
      _logger.info("Second XML : " + xml2);

The result is erroneous:

     
INFO [18:04:28.011] [XMain.main] Second XML : <myobj>
  <value>10</value>
</myobj>

In case we reuse the same XStream object, it throws exception:

     
//    xstream.alias("myobj", clazz2);
    String xml2 = xstream.toXML(obj2);
    _logger.info("Second XML : " + xml2);

The exception is:

   
Exception in thread "main"
com.thoughtworks.xstream.converters.reflection.ObjectAccessException:
Could not get field class java.lang.reflect.Field.value : null
  at com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider.
visitSerializableFields(PureJavaReflectionProvider.java:110)
  at com.thoughtworks.xstream.converters.reflection.ReflectionConverter.
marshal(ReflectionConverter.java:44)
  at com.thoughtworks.xstream.core.ReferenceByXPathMarshaller.
convertAnother(ReferenceByXPathMarshaller.java:36)
  at com.thoughtworks.xstream.core.TreeMarshaller.start(TreeMarshaller.java:46)
  at com.thoughtworks.xstream.core.ReferenceByXPathMarshallingStrategy.
marshal(ReferenceByXPathMarshallingStrategy.java:17)
  at com.thoughtworks.xstream.XStream.marshal(XStream.java:461)
  at com.thoughtworks.xstream.XStream.marshal(XStream.java:451)
  at com.thoughtworks.xstream.XStream.toXML(XStream.java:432)
  at wr.XMain.main(XMain.java:134)
Caused by: java.lang.IllegalArgumentException
  at sun.reflect.UnsafeFieldAccessorImpl.ensureObj(Unknown Source)
  at sun.reflect.UnsafeIntegerFieldAccessorImpl.getInt(Unknown Source)
  at sun.reflect.UnsafeIntegerFieldAccessorImpl.get(Unknown Source)
  at java.lang.reflect.Field.get(Unknown Source)
  at com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider.
visitSerializableFields(PureJavaReflectionProvider.java:108)
  ... 8 more

This exception happens also when the alias call is uncommented.

Few techniques can be employed here, one of them being WeakHashMap, and another not using class names as keys in reflection caches. In any case, when you use reflection, you can not assume that the cached results will always be valid. In addition, libraries such as JiBX that inject marshaling and unmarshaling code to the data classes must be used not only during the build, but each time the classes are changed and compiled at runtime.

I beg to differ: The value is still 10. Only the getter ...

kirillcool wrote:
     
INFO [18:04:28.011] [XMain.main] Fetched value <b>20</b>

Now, we create a new XStream object and ask it to marshal the new object:

     
      XStream xstream2 = new XStream();
      xstream2.alias("myobj", clazz2);
      String xml2 = xstream2.toXML(obj2);
      _logger.info("Second XML : " + xml2);

The result is erroneous:

     
INFO [18:04:28.011] [XMain.main] Second XML : <b>&lt;myobj&gt;
  &lt;value&gt;<font color='red'>10</font>&lt;/value&gt;
&lt;/myobj&gt;</b>

I beg to differ: The value is still 10. Only the getter returns 20, but marshalling the instance of the class shall not store, what any function with more or less side-effects would return but instead the internal properties of the instance, so it could be reconstructed.

As you probably know, the whole point is to first marshall the instance of the old clas, then unload the class, load the new one and then unmarshall all the previously existing instances. Of course, for this to work, the real internals have to be stored (and some other conditions have to be met).

kirillcool wrote:
In case we reuse the same XStream object, it throws exception:

     
//    xstream.alias("myobj", clazz2);
    String xml2 = xstream.toXML(obj2);
    _logger.info("Second XML : " + xml2);

As expected. The old class no longer exists. (The new class even with same name doesn't matter, it's still a different class.)