Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8226383

Unstable behavior of java.beans.XMLEncoder since Java 9

    Details

    • Subcomponent:
    • Understanding:
      Cause Known
    • Introduced In Version:
      9
    • CPU:
      x86
    • OS:
      generic

      Description

      ADDITIONAL SYSTEM INFORMATION :
      This bug is not platform-dependent.

      A DESCRIPTION OF THE PROBLEM :
      In https://hg.openjdk.java.net/jdk/jdk/rev/f13fa56f89ba the generated method accessors for reflection were changed to use auto-boxing for primitive objects instead of explicitly producing new wrapper objects. With auto-boxing, instances of Boolean, Byte, Character, Short, Integer and Long are returned from internal caches so different methods with primitive return values can return the same wrapper object when accessed through reflection.

      However, java.beans.XMLEncoder operates on the assumption that wrapper objects for primitive return values always have a different identity. The new behavior of reflection leads to an unstable behavior of XMLEncoder. At first, the correct output is observed. After a while, the "inflation" mechanism in jdk.internal.reflect.ReflectionFactory produces bytecode for a method accessor that exhibits the auto-boxing behavior. Subsequently, the encoded output changes and "pulls" primitive values from other properties that are listed earlier in the property descriptors of the BeanInfo class. While technically correct, this unstable behavior lead to problems and is generally undesirable.

      REGRESSION : Last worked in version 8u202

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Execute the test class below on Java 1.8, then on any Java release >= 9. With Java 1.8, the expected behavior is observed, higher versions show the regression.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Correct XML encoding:
      <?xml version="1.0" encoding="UTF-8"?>
      <java version="1.8.0_202" class="java.beans.XMLDecoder">
       <object class="com.ejt.framework.xmlencoding.XmlEncoderBugTest">
        <void property="booleanTwo">
         <boolean>true</boolean>
        </void>
       </object>
      </java>

      No error detected after 100 iterations.
      ACTUAL -
      The output of the test class as observed with Java 13-ea (but also with 9,10,11 and 12) is:

      Correct XML encoding:
      <?xml version="1.0" encoding="UTF-8"?>
      <java version="13-ea" class="java.beans.XMLDecoder">
       <object class="com.ejt.framework.xmlencoding.XmlEncoderBugTest">
        <void property="booleanTwo">
         <boolean>true</boolean>
        </void>
       </object>
      </java>

      Wrong XML encoding in iteration 6:
      <?xml version="1.0" encoding="UTF-8"?>
      <java version="13-ea" class="java.beans.XMLDecoder">
       <object class="com.ejt.framework.xmlencoding.XmlEncoderBugTest" id="XmlEncoderBugTest0">
        <void id="Boolean0" method="isBooleanOne"/>
        <void property="booleanTwo">
         <object idref="Boolean0"/>
        </void>
       </object>
      </java>


      ---------- BEGIN SOURCE ----------
      import java.beans.IntrospectionException;
      import java.beans.PropertyDescriptor;
      import java.beans.SimpleBeanInfo;
      import java.beans.XMLEncoder;
      import java.io.ByteArrayOutputStream;

      public class XmlEncoderBugTest extends SimpleBeanInfo {

          private static final int MAX_ITERATIONS = 100;
          private static PropertyDescriptor[] propertyDescriptors;

          static {
              try {
                  propertyDescriptors = new PropertyDescriptor[]{
                          new PropertyDescriptor("booleanOne", XmlEncoderBugTest.class),
                          new PropertyDescriptor("booleanTwo", XmlEncoderBugTest.class)
                  };
              } catch (IntrospectionException e) {
                  throw new RuntimeException(e);
              }
          }

          private boolean booleanOne = true;
          private boolean booleanTwo = false;

          public boolean isBooleanOne() {
              return booleanOne;
          }

          public void setBooleanOne(boolean booleanOne) {
              this.booleanOne = booleanOne;
          }

          public boolean isBooleanTwo() {
              return booleanTwo;
          }

          public void setBooleanTwo(boolean booleanTwo) {
              this.booleanTwo = booleanTwo;
          }

          @Override
          public PropertyDescriptor[] getPropertyDescriptors() {
              return propertyDescriptors;
          }

          public static void main(String[] args) {
              for (int i = 0; i < MAX_ITERATIONS; i++) {
                  ByteArrayOutputStream out = new ByteArrayOutputStream();
                  XMLEncoder xmlEncoder = new XMLEncoder(out);

                  XmlEncoderBugTest object = new XmlEncoderBugTest();
                  object.setBooleanTwo(true);

                  xmlEncoder.writeObject(object);
                  xmlEncoder.close();

                  String encoded = new String(out.toByteArray());
                  if (i == 0) {
                      System.out.println("Correct XML encoding:");
                      System.out.println(encoded);
                  } else if (encoded.contains("Boolean0")) {
                      System.out.println("Wrong XML encoding in iteration " + i + ":");
                      System.out.println(encoded);
                      return;
                  }
              }
              System.out.println("No error detected after " + MAX_ITERATIONS + " iterations.");
          }
      }


      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Temporary workaround: Setting -Dsun.reflect.inflationThreshold=100000 prevents the "inflation" mechanism in jdk.internal.reflect.ReflectionFactory from generating the method accessor and so the correct output will be observed with Java 9+.

      Permanent fix: The following patch to java.beans.Statement against the jdk13 repository fixes the problem by duplicating wrapper objects for primitive types with internal caches:

      diff --git a/src/java.desktop/share/classes/java/beans/Statement.java b/src/java.desktop/share/classes/java/beans/Statement.java
      index 3a47f84787..aaaec6e7d7 100644
      --- a/src/java.desktop/share/classes/java/beans/Statement.java
      +++ b/src/java.desktop/share/classes/java/beans/Statement.java
      @@ -301,7 +301,29 @@ public class Statement {
               if (m != null) {
                   try {
                       if (m instanceof Method) {
      - return MethodUtil.invoke((Method)m, target, arguments);
      + Object returnValue = MethodUtil.invoke((Method)m, target, arguments);
      + Class<?> returnType = ((Method)m).getReturnType();
      + if (returnType.isPrimitive()) {
      + // In https://hg.openjdk.java.net/jdk/jdk/rev/f13fa56f89ba method accessors for reflection were
      + // changed not to return new primitive objects but to use boxing which can return the same
      + // objects for different methods with primitive return types. XMLEncoder operates with the
      + // assumption that these objects are always different.
      + // Float and Double do not have to be duplicated because they do not have internal caches.
      + if (returnType == Boolean.TYPE) {
      + return new Boolean((Boolean)returnValue);
      + } else if (returnType == Character.TYPE) {
      + return new Character((Character)returnValue);
      + } else if (returnType == Byte.TYPE) {
      + return new Byte((Byte)returnValue);
      + } else if (returnType == Short.TYPE) {
      + return new Short((Short)returnValue);
      + } else if (returnType == Integer.TYPE) {
      + return new Integer((Integer)returnValue);
      + } else if (returnType == Long.TYPE) {
      + return new Long((Long)returnValue);
      + }
      + }
      + return returnValue;
                       }
                       else {
                           return ((Constructor)m).newInstance(arguments);



      FREQUENCY : always


        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                serb Sergey Bylokhov
                Reporter:
                webbuggrp Webbug Group
              • Votes:
                0 Vote for this issue
                Watchers:
                3 Start watching this issue

                Dates

                • Created:
                  Updated: