Uploaded image for project: 'Clojure'
  1. CLJ-2066

Reflection on internal classes fails under Java 9

    Details

    • Approval:
      Ok
    • Patch:
      Code and Test

      Description

      Due to changes in reflective access for the Jigsaw module system in Java 9, the Reflector will now fail on some cases that worked in previous Java versions.

      (def fac (javax.xml.stream.XMLInputFactory/newInstance))
      (.createXMLStreamReader fac (java.io.StringReader. ""))
      

      Here fac will be an instance of com.sun.xml.internal.stream.XMLInputFactoryImpl, which is an extension of javax.xml.stream.XMLInputFactory. In the new java.xml module, javax.xml.stream is an exported package, but the XMLInputFactoryImpl is an internal extension of the public abstract class in that package. The invocation of createXMLStreamReader will be reflective and the Reflector will attempt to invoke the method based on the implementation class, which is not accessible outside the module, yielding:

      WARNING: An illegal reflective access operation has occurred
      WARNING: Illegal reflective access by clojure.lang.Reflector (file:/Users/alex/.m2/repository/org/clojure/clojure/1.10.0-alpha8/clojure-1.10.0-alpha8.jar) to method com.sun.xml.internal.stream.XMLInputFactoryImpl.createXMLStreamReader(java.io.Reader)
      WARNING: Please consider reporting this to the maintainers of clojure.lang.Reflector
      WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
      WARNING: All illegal access operations will be denied in a future release
      

      A workaround here is to avoid the reflective call by type-hinting to the public exported interface but that defeats the point of reflection support:

      (.createXMLStreamReader ^javax.xml.stream.XMLInputFactory fac (java.io.StringReader. ""))
      

      Another (undesirable) workaround is to export the private package from java.xml to the unnamed module (which is the module used when code is loaded from the classpath rather than from a module) when invoking java/javac:

      java --add-exports=java.xml/com.sun.xml.internal.stream=ALL-UNNAMED --add-exports=java.xml/com.sun.xml.internal.stream.writers=ALL-UNNAMED --add-exports=java.xml/com.sun.org.apache.xerces.internal.impl=ALL-UNNAMED ...
      

      Alternatives:

      We have investigated a number of options to enhance the Reflector. When we are trying to reflect, the only class we know is the class of the target object (here a non-public class, but one we can look at due to the default settings of --illegal-access). Some options considered include:

      • checking canAccess() on the Method (here it returns true, so no help)
      • invoking and looking for IllegalAccessError (pretty gross)
      • traversing the super class and interface hierarchy to find "more public" instances (this really is replicating logic that already exists in the Java method lookup logic)

      None of these is "good".

      Proposed:

      This continues to be a warning on Java 9, 10, and (just released) 11. The warning is accurate - you are using reflection to a method on a non-exported class. The default setting in Java 9, 10, 11 is --illegal-access=permit which allows this but reports a warning on the first occurrence (other settings are warn, debug, and deny). In some future Java, the default may be deny.

      If at some point in the future the JVM changes the default to deny, we still want these reflective calls to work. In that case, we will know this situation has occurred because Method#canAccess will return false. When that happens, we need to look up through the superclasses of the target object to find one that has an accessible version of the method.

      Patch: clj-2066-6.patch - when invoking an instance method, map each method to an accessible super class method, then filter non-null methods found (presumably all of them). When on Java 8 or when --illegal-access=permit (the default on Java 9, 10, and 11), then the canAccess() check on each method will return true, allowing it to occur. If --illegal-access=deny, then canAccess() will be false and super class methods will be used instead.

      Cases to test for screening (all with example at top):

      • Java 8 - should work with no warnings
      • Java 9, 10, and 11 with default JVM options (should succeed, but warn per --illegal-access=permit)
      • Java 9, 10, and 11 with --illegal-access=deny (should succeed, with no warnings)
      • Java 9, 10, and 11 with "--add-opens java.xml/com.sun.xml.internal.stream=ALL-UNNAMED" - should succeed, with no warnings

      More info:

        Attachments

          Activity

            People

            • Assignee:
              Unassigned
              Reporter:
              tcrawley tcrawley
            • Votes:
              6 Vote for this issue
              Watchers:
              11 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved: