parboiled: Parboiled stopped working in Java 16 due to enforcement of encapsulation
Code that depends upon Parboiled2 no longer works with Java 16, because encapsulation is now enforced. The following exception is thrown:
Exception in thread "main" java.lang.RuntimeException: Error creating extended parser class: Could not determine whether class 'mypackage.MyParser$$parboiled' has already been loaded
at org.parboiled.Parboiled.createParser(Parboiled.java:58)
at javaparse.JavaParsers.benchmarkParboiled1_java(JavaParsers.java:25)
at squirrel.TestUtils.findMinTime(TestUtils.java:46)
at squirrel.TestJavaParsing.main(TestJavaParsing.java:17)
Caused by: java.lang.RuntimeException: Could not determine whether class 'mypackage.MyParser$$parboiled' has already been loaded
at org.parboiled.transform.AsmUtils.findLoadedClass(AsmUtils.java:217)
at org.parboiled.transform.ParserTransformer.transformParser(ParserTransformer.java:35)
at org.parboiled.Parboiled.createParser(Parboiled.java:54)
... 3 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.findLoadedClass(java.lang.String) accessible: module java.base does not "opens java.lang" to unnamed module @3c5a99da
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at org.parboiled.transform.AsmUtils.findLoadedClass(AsmUtils.java:210)
... 5 more
The problematic code in org.parboiled.transform.AsmUtils.findLoadedClass is the call to findLoadedClassMethod.setAccessible(true) here:
public static Class<?> findLoadedClass(String className, ClassLoader classLoader) {
checkArgNotNull(className, "className");
checkArgNotNull(classLoader, "classLoader");
try {
Class<?> classLoaderBaseClass = Class.forName("java.lang.ClassLoader");
Method findLoadedClassMethod = classLoaderBaseClass.getDeclaredMethod("findLoadedClass", String.class);
// protected method invocation
findLoadedClassMethod.setAccessible(true);
try {
return (Class<?>) findLoadedClassMethod.invoke(classLoader, className);
} finally {
findLoadedClassMethod.setAccessible(false);
}
} catch (Exception e) {
throw new RuntimeException("Could not determine whether class '" + className +
"' has already been loaded", e);
}
}
Note that to even get this far you have to override the version of ASM depended upon by Parboiled to one that works with JDK 16:
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-java</artifactId>
<version>1.3.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>asm</artifactId>
<groupId>org.ow2.asm</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
<scope>test</scope>
</dependency>
Java 15 is dramatically faster than previous JVMs, and Java 16 has record types, so Java 16 will fast become a new baseline for many Java projects. Parboiled is still a dependency for a lot of old code. It would be nice if Parboiled could be updated to not use introspection in this way so that it works with Java 16+.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 35 (20 by maintainers)
Commits related to this issue
- parboiled-java with fix for JDK16+ added, min jdk changed to 11 @see https://github.com/sirthias/parboiled/issues/175 — committed to allegro/opel by bgalek 3 years ago
- Merge pull request #184 from bgalek/jdk16+ JDK16/17 partial support #175 — committed to sirthias/parboiled by sirthias 3 years ago
- Revert "jdk16+ partial support #175" This reverts commit 036d4f1afca0b49e0d4704da1af96eaeb81e1336. — committed to sirthias/parboiled by sirthias 3 years ago
- parboiled-java with fix for JDK16+ added, min jdk changed to 11 @see https://github.com/sirthias/parboiled/issues/175 — committed to allegro/opel by bgalek 3 years ago
@david-shao I can create PR that would force using the
MethodHandles.lookup()instead.As I’m switching to Java 17 on my projects as well, I’ve tested whether
--add-opensstill works for Java internal classes and I can confirm using--add-opens java.base/java.lang=ALL-UNNAMEDstill works for Parboiled, as @SethTisue suggested. So when you specify it for the runtime, everything still works as it should.Just released parboiled 1.4.0 which should work fine on Java 17.
Adding runtime flags does create deployment & project sharing headaches, so a long-term solution hopefully won’t require them.
@bgalek I’d be happy to look at and merge a PR that helps with overcoming the long-standing issues around dynamic class loading. But I’m afraid I’m too far away from the technical details (> 10 years!) to be of any help myself, unfortunately…
(shameless plug) If anybody is going to use Java 17 (soon), take a look at Rekex (which doesn’t work in Java 16 or lower:)
I’ve had some time do look into this a bit deeper:
--illegal-access=permitwill work as workaround for Java 16, however it will be removed in Java 17 (which is the upcoming LTS version - https://openjdk.java.net/jeps/403)findLoadedClass/defineClass- which does class lookup / new class definition from bytecode produced by ASMWhen Parboiled gets rid of the usage of those two methods, it starts working without any issues. How to achieve this is however a little problematic:
findLoadedClassanddefineClassis withMethodHandles$Lookupclass ->MethodHandles.lookup().findClass(className)andMethodHandles.lookup().defineClass(code)MethodHandles.lookup()instantiates Lookup class withReflection.getCallerClass(), which restricts the usage to the package the caller is fromorg.parboiled.transform.AsmUtilsIllegalAccessException, whenAsmUtilstries to define class in different package thanorg.parboiled.transformSo far I could think of three possible scenarios:
Replace
AsmUtils.findLoadedClassandAsmUtils.loadClassimplementation directly withMethodHandles.lookup().*methods a. Pros: change in only few lines, easy fix b. Cons: all parsers need to be defined inorg.parboiled.transformpackage, otherwise it won’t workReplace
AsmUtils.findLoadedClassandAsmUtils.loadClassimplementation directly withMethodHandles.lookup().*and update ASM to generate new updated classes insideorg.parboiled.transform, not in the original parser package a. Pros: works without any necessary modification to existing parsers b. Cons: all classes that are defined dynamically need to be generated in different package and all related calls need to be updated as well (class can be “renamed” withClassVisitor, but all references to the original class need to be updated / copied, otherwise similar IllegalAccessException will be thrown) -> I suspect this would require major rewrite of the ASM handlingReplace
AsmUtils.findLoadedClassandAsmUtils.loadClassimplementation with calls to similar static methods defined in parser class (see below) a. Pros: change in only few lines, easy fix b. Cons: all parsers need to define their own staticfindLoadedClassandloadClassstatic methods (creating custom method in BaseParser doesn’t help as it gets detected as different package)All options mentioned above require at least Java 9, because that’s when
Lookup.defineClasswas added (https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/MethodHandles.Lookup.html#defineClass-byte:A- )The third option can be achieved with following patch:
And following methods need to be defined in all parsers:
While this is far from optimal, it works 🤷♂️