JDK-8226383 : Unstable behavior of java.beans.XMLEncoder since Java 9
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.beans
  • Affected Version: 9,12,13,14
  • Priority: P3
  • Status: In Progress
  • Resolution: Unresolved
  • OS: generic
  • CPU: x86
  • Submitted: 2019-06-16
  • Updated: 2021-07-13
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
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



Comments
As per submission, XMLEncoder display unstable behavior from JDK 9 and onward. Verified this with respective JDK versions and confirmed the result as detailed. This seems a regression in JDK 9 and onward. Result: ======== 8u212: Ok 9: Fail 12.0.1: Fail 13 ea b25: Fail 14 ea b01: Fail To verify, run the attached test case with respective JDK versions.
19-06-2019