The Source for Java Technology Collaboration
User: Password:



Kirill Grouchnikov

Kirill Grouchnikov's Blog

Reflection and dynamically changing classes

Posted by kirillcool on August 10, 2005 at 08:29 AM | Comments (0)

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.

Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds