Class loader issue between settings.gradle and sub-projects using a plugin

I have a somewhat intricate setup which ends up with a class cast exception between instances of the same class. Essentially I have custom build logic which is externalized into a plugin and which is used from:

  • settings.gradle to construct a multi-project structure * all projects in the structure which do an ‘apply plugin’ on the plugin

I have a github repo for replicating the issue at:

https://github.com/mbjarland/gradle-settings-classloader-issue

and the repo readme contains instructions (two steps) for reproducing the issue.

Here is essentially what happens:

  • I include the plugin in the buildscript.dependencies.classpath in settings.gradle so that I can use a MultiProjectConfigurator class from the plugin. I then instantiate the MultiProjectConfigurator in the settings.gradle file and call a method on it. The MultiProjectConfigurator looks as follows:
class MultiProjectConfigurator {
   def setup(settings) {
    SomeClass instance = new SomeClass()
          settings.gradle.allprojects {
      project.ext.someClass = instance
    }
  }
}

where SomeClass also lives in the plugin jar. This code is naturally a bare bones, gutted out example and my actual plugin has some complex logic in it for setting up the multi-project structure. The essential piece for this issue is the instantiation of SomeClass and the setting of a project property on all projects.

  • I then do a allprojects { apply plugin: ‘greeting’ } in build.gradle. The plugin in question looks as follows:
class GreetingPlugin implements Plugin<Project> {
  void apply(Project project) {
    // The below line breaks with ClassCastException...even though
     // we are casting between two instances of the same class. I.e.
     // we have class loader issues of some sort...
    SomeClass instance = project.ext.someClass as SomeClass
    println "Successfully cast ${project.ext.someClass} to SomeClass"
  }
}

executing anything on the root project will apply the plugin and cause a class cast exception :


org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'SomeClass@6c69df61' with class 'SomeClass' to class 'SomeClass'  

Note that if I uncomment the following line in settings.gradle, the class cast exception goes away and everything works as expected:

buildscript {
   ...
  //configurations.classpath.each { file -> settings.classLoader.addURL(file.toURI().toURL()) }
}

The driver for this use case is that my actual plugin code does an extensive and time consuming (sub-second…but this is for every single build…so we would like to not repeat that work) file system scan in the MultiProjectConfigurator class. This scan in turn results in a complex data structure which I need to make available for all projects in the multi-project structure.

So what is going on here? And perhaps more importantly, how would I go about transporting an instance between these two code snippets and execution phases?

Thank you in advance and cudos for making such a stellar build system.

Gradle gurus? Any ideas most welcome as I am quite stumped on this.

The CCE is expected here, because you are really loading two copies of the class in two different loaders (settings and project). There’s no safe way to load this class in a common loader at this time.

The simplest solution would be to simply not type the object in your plugin and use it untyped. How well this is going to work will depend on exactly what you are doing with it. But I’d start there and see how far you get. This also implies that you don’t need to add the jar to the buildscript classpath.