Accessing the runtime classloader from the gradle build script


(Milan) #1

I’m trying to use a code generation library from my Gradle build script, but i’m facing an error - I don’t seem to be able to access my own classes from the build file. The task I have is the following:

    def exporter = new GenericExporter();
    exporter.setTargetFolder(sourceSets.generated.java.srcDirs.first());
    exporter.export("com.company.project");

The default GenericExporter constructor is instantiated with a Thread.currentThread().getContextClassLoader() class loader. When I run the task, I get nothing, meaning no classes that are in the com.company.project are found.
When I have the same code in my Application class (the main class inside my project), the code generation works. I debugged and saw that when executing the task from the gradle file I don’t have access to my own com.company.project classes.

Moreover, the project has a multi-project setup. The code generation task I’ve added to subprojects. Moreover I researched and tried to also add the following inside subprojects, but it didn’t help:

    buildscript {
        dependencies {
            classpath files("${buildDir}/classes")
        }
    } 

Any suggestions would be greatly appreciated.


(Mark Vieira) #2

This is the problem in that the build script cannot depend on code that is compiled by the build script. There is a cyclic dependency here. You have a couple of options.

  1. Put the code needed by the build in the buildSrc project
  2. Publish the code needed by the build and depend on it as an external dependency

(Milan) #3

Hi, I read about that problem but seem to not be able to grasp it completely. Wouldn’t having a task that first compiles only the model classes and then one that generates other files from the already compiled models be sufficient?
I’m saying that because both of your solutions seem to suggest kind of major changes - either a buildSrc project that contains the current top level project with all subproject or 2) (again having a dependency on the whole project). I’m looking for a simple solution that would involve minimum changes to the current build setup.


(Stefan Oehme) #4

I think you and Mark misunderstood each other. You don’t want to use the compiled classes in the buildscript. You want to postprocess them in another task. That’s an important difference.

That sounds too inflexible, the exporter should take a ClassLoader argument.

Of course, the buildscript is compiled and instantiated before your build can start, so your production code cannot be in the same classloader.

If you want to load your production classes for further processing, you can use a URLClassLoader. You might want to change your exporter to use a more efficient library like ASM though. That’s much faster.


(Milan) #5

I assumed there might be some misunderstanding, but I couldn’t express myself more clearly.

Yes, it does in fact does take a class loader as argument.

The exporter is not mine to change, it’s part of the QueryDSL library.

I tried to do so, but again with no result. Maybe this is a good time to mention that the files I’m trying to process are Groovy ones. Nevertheless, I tried to instantiate the class loader by two different ways, as shown below, but printing the loaded classes inside of both reveals not a single “com.company.project” class (only the jars of some external libraries I depend on).

def classLoader = new URLClassLoader(sourceSets.main.compileClasspath.collect { it.toURI().toURL() } as URL[])
def classLoader = new URLClassLoader(configurations.runtime.collect { it.toURI().toURL() } as URL[])

(Stefan Oehme) #6

You want sourceSets.main.runtimeClasspath.

sourceSets.main.compileClasspath only contains what is necessary to compile the code (and of course the code can’t depend on itself to compile :slight_smile: )

configurations.runtime will only give you the runtime dependencies of your code.


(Milan) #7

Thank you, it’s currently working with the sourceSets.main.runtimeClasspath. The problem I had before when I tried to use the runtimeClasspath was that I received an error saying that I cannot change dependencies of configuration ':app:runtime' after it has been resolved.

My root project’s build.gradle looks like:

build script { }
subprojects {}
project('1') {}
project('2') {}
...
project('n') {
    apply plugin: 'war'

    dependencies {
       compile project('1')
       ...

       providedRuntime('xxx')
    }
}

I “fixed” this by adding my generation task in another subproject section below the last project definition, i.e. after project('n'). This works, but 1) looks a bit strange and it would be great to have the previous structure and 2) it seems to be executed on every other task that I execute, i.e. compileGroovy, clean, etc.
Again, thanks for the very useful responses :slight_smile:


(Stefan Oehme) #8

You should only be instantiating the URLClassLoader inside the task action, not in the configuration phase. Otherwise, you are resolving dependencies too early.

Simplified:

Don’t:

task postProcess() {
  def loader = newUrlClassLoader()
}

Do:

taks postProcess() {
  doLast {
    def loader = new UrlClassLoader()
  }
}