Jex Jexler created GROOVY-12458:
-----------------------------------
Summary: @Grab in a class defined by a non-GroovyClassLoader fails
at runtime with groovy-grape-maven ("No suitable ClassLoader found for grab")
Key: GROOVY-12458
URL: https://issues.apache.org/jira/browse/GROOVY-12458
Project: Groovy
Issue Type: Bug
Components: Grape
Affects Versions: 6.0.0
Reporter: Jex Jexler
Attachments: GrabClassLoaderRepro.groovy
Since Groovy 6, the static initializer generated for @Grab passes the class's
own class loader to Grape.grab()
(GrabAnnotationTransformation.callGrabAsStaticInitIfNeeded: classLoader:
<Class>.getClassLoader()).
If that class was defined by a class loader that is not a GroovyClassLoader (or
RootLoader), but has one as an ancestor, both engines reject the passed loader
in chooseClassLoader() without considering its parents, and fall back to the
stack walk via calleeDepth, which (as the comment in
callGrabAsStaticInitIfNeeded notes) overshoots from a static initializer:
* GrapeMaven.chooseClassLoader() then throws "RuntimeException: No suitable
ClassLoader found for grab".
* GrapeIvy.chooseClassLoader() usually recovers via its fallbacks (scanning
frames at calleeDepth +/- 2, which finds a class whose class loader has a valid
ancestor), i.e. more by chance than by design.
Hosts that define compiled script classes with their own class loaders below a
GroovyClassLoader are affected. We hit this with *Grengine*
(https://grengine.ch/), where @Grab works with groovy-grape-ivy, but fails with
groovy-grape-maven. Grengine now works around it in its GrapeEngine wrapper by
replacing the passed class loader with its closest valid ancestor:
https://github.com/alainstalder/grengine/commit/bd0dd9845440f627e282c4b474f1ec111b6f9a5d
(code:
https://github.com/alainstalder/grengine/blob/bd0dd9845440f627e282c4b474f1ec111b6f9a5d/src/main/java/ch/grengine/code/groovy/DefaultGroovyCompiler.java#L673-L699)
*Standalone reproducer (attached, GrabClassLoaderRepro.groovy):* compiles a
script with @Grab using a GroovyClassLoader, defines the resulting class with a
plain ClassLoader subclass whose parent is that GroovyClassLoader, and
instantiates it, which runs the static initializer:
* with groovy-6.0.0 + groovy-grape-ivy (and its dependencies) on the classpath:
OK
* with groovy-6.0.0 + groovy-grape-maven (and its dependencies) on the
classpath:
ExceptionInInitializerError, caused by RuntimeException: No suitable
ClassLoader found for grab
at groovy.grape.maven.GrapeMaven.chooseClassLoader(GrapeMaven.groovy:785)
Run e.g. with: java -cp <classpath> groovy.ui.GroovyMain
GrabClassLoaderRepro.groovy
*Suggested fix:* in chooseClassLoader() (in both engines, or at least in
GrapeMaven), if args.classLoader is set but not a valid target, walk up its
parents to the first valid one before falling back to the stack walk, e.g.:
{code:java}
ClassLoader loader = (ClassLoader) args.classLoader
while (loader && !isValidTargetClassLoader(loader)) {
loader = loader.getParent()
}
if (!isValidTargetClassLoader(loader)) {
// existing stack walk (and fallbacks)
}{code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)