Stefan Kendall
Stefan Kendall

Reputation: 67832

No suitable classloader found for grab

I have this at the beginning of a class:

@Grab(group = 'org.ccil.cowan.tagsoup', module = 'tagsoup', version = '1.2')
class MyClass{...

I'm trying to unit test this class, but whenever I try to run JUnit 4 tests, I get this error:

Caused by: java.lang.RuntimeException: No suitable ClassLoader found for grab
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:77)
    at org.codehaus.groovy.runtime.callsite.ConstructorSite$ConstructorSiteNoUnwrapNoCoerce.callConstructor(ConstructorSite.java:102)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:52)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:190)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:198)
    at groovy.grape.GrapeIvy.chooseClassLoader(GrapeIvy.groovy:163)
    at groovy.grape.GrapeIvy$chooseClassLoader.callCurrent(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:44)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:141)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:149)
    at groovy.grape.GrapeIvy.grab(GrapeIvy.groovy:227)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSite.invoke(PogoMetaMethodSite.java:225)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.callCurrent(PogoMetaMethodSite.java:51)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:44)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:141)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:153)
    at groovy.grape.GrapeIvy.grab(GrapeIvy.groovy:216)
    at groovy.grape.Grape.grab(Grape.java:131)
    at groovy.grape.Grape$grab.callStatic(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:165)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:173)
    at ammoscanner.AmmoScanner.<clinit>(AmmoScanner.groovy)
    ... 30 more

Any ideas? I'm using groovy 1.7.5

Upvotes: 21

Views: 20474

Answers (8)

Alexey Subach
Alexey Subach

Reputation: 12312

Solution that worked for me (both for running tests for scripts using @Grab in IntelliJ and via Maven):

  1. Reference the dependencies used via @Grab in your Maven pom.xml file (this is useful anyway for better coding experience):

E.g. I have the following @Grab in my Groovy script:

@Grab(group='info.picocli', module='picocli', version='4.6.1')

So I add the following Maven dependency:

    <dependency>
      <groupId>info.picocli</groupId>
      <artifactId>picocli</artifactId>
      <version>4.6.1</version>
    </dependency>
  1. Add an optional dependency on ivy in your Maven pom.xml file (needed to handle @Grab properly in your IDE):
    <dependency>
      <groupId>org.apache.ivy</groupId>
      <artifactId>ivy</artifactId>
      <version>${ivy.version}</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
  1. In your test code, set groovy.grape.enable system property to false. This is the main and crucial part of the solution - it disables @Grab annotation processing for the script, but remember we already have those dependencies referenced in our Maven pom.xml file:
    static {
        System.setProperty("groovy.grape.enable", "false")
    }

    @Test
    void test() {
        MainScript.call()
    }

The downside of the solution is that you have to duplicate your dependencies in @Grab and Maven pom.xml file but again, if you develop a Groovy script you oftentimes already do so to improve your coding experience (get better code highlights etc).

Upvotes: 4

wheeler
wheeler

Reputation: 3251

There is a solution to this!

You can use Groovy's metaprogramming to override the methods responsible for determining if the class loader is an instance of groovy.lang.GroovyClassLoader or org.codehaus.groovy.tools.RootLoader.

Because of this Groovy bug, you cannot override the private methods using metaprogramming, otherwise you could go ahead and change the isValidTargetClassLoaderClass method by doing this:

GrapeIvy.metaClass.isValidTargetClassLoaderClass = { Class loaderClass ->
    return (loaderClass != null)
}

However, isValidTargetClassLoaderClass is called by isValidTargetClassLoader (another private method), which is called by chooseClassLoader, which is a public method, which can be overridden using metaprogramming:

GrapeIvy.metaClass.chooseClassLoader = { Map args ->
    def loader = args.classLoader
    if (loader?.class == null) {
        loader = (args.refObject?.class
                ?: ReflectionUtils.getCallingClass(args.calleeDepth?:1)
        )?.classLoader
        while (loader && loader?.class == null) {
            loader = loader.parent
        }

        if (loader?.class == null) {
            throw new RuntimeException("No suitable ClassLoader found for grab")
        }
    }
    return loader
}

All I did was replace any calls to !isValidTargetClassLoader with loader?.class == null.

Now, I am using Spock, so I put this code in my setupSpec method in my test class. However if you are using JUnit, I would imagine it would want to go in the method annotated with @BeforeClass.

Here is an example of it working with Spock (notice IntelliJ showing it about to return a class loader that would normally throw an exception:

enter image description here

Upvotes: 0

rmaruszewski
rmaruszewski

Reputation: 2417

There's one more solution for testing a class with @Grab annotation:

  1. Extract an interface from this class.
  2. Create another class which implements its interface. Move the @Grab annotation to this class. Then make this class a simple wrapper, which just passes all the messages to the original class.
  3. Run the tests against your original class.
  4. Whenever you need to have a version @Grab, use the wrapper.

Upvotes: 0

tim_yates
tim_yates

Reputation: 171114

I assume you've tried adding

@GrabConfig(systemClassLoader=true)

like so:

@Grapes([
    @Grab(group = 'org.ccil.cowan.tagsoup', module = 'tagsoup', version = '1.2'),
    @GrabConfig( systemClassLoader=true )
])
class MyClass{...

Upvotes: 2

Nick Grealy
Nick Grealy

Reputation: 25882

The Problem

Looking at the source code, this exception is thrown whenever the supplied ClassLoader's name (or it's superclasses) is not groovy.lang.GroovyClassLoader or org.codehaus.groovy.tools.RootLoader. i.e. The target classloader must be an instance of the aforementioned classes (a bit restrictive IMHO).

A Solution

Currently I don't know how to configure a specific classloader using @Grape/@Grab/@GrabConfig annotations. The closest would be to use @GrabConfig(systemClassLoader=true), and ensure the System classloader is an instance of one of the above ClassLoader classes.

If anyone does know, please let me know (and I'll update this answer).

A Workaround

The following code will programmatically download your Grapes, and load them into the supplied GroovyClassLoader (admittedly, not quite what you want).

def loadGrapes(){
    ClassLoader classLoader = new groovy.lang.GroovyClassLoader()
    Map[] grapez = [[group : 'org.ccil.cowan.tagsoup', module : 'tagsoup', version : '1.2']]
    Grape.grab(classLoader: classLoader, grapez)
    println "Class: " + classLoader.loadClass('org.ccil.cowan.tagsoup.jaxp.SAXParserImpl')
}

Upvotes: 7

chris topinka
chris topinka

Reputation: 37

Add the plugin snapshot update site for Kepler.

This seems to solve the "..no suitable classloader problem". Unfortunately, I still had to add the grape repo to the classpath for the project after this.

Upvotes: 0

akostadinov
akostadinov

Reputation: 18614

If you are not using systemClassLoader=true then it seems your IDE is not rrunning the code with a groovy compiler, you can check that with a simple groovy class that outputs the class name of its classloader. I would guess it tries to compile the groovy classes and run them with a non-groovy classloader.

See also this answer to General error during conversion: No suitable ClassLoader found for grab. Also this blog post explains more about running pre-compiled groovy classes with the stock classloader.

Upvotes: 1

Stefan Kendall
Stefan Kendall

Reputation: 67832

Using @Grab makes code untestable, at least as of 01/26/2011.

Upvotes: 5

Related Questions