Loading ServiceLoader providers in Plugin<Project>#apply(Project)


(Ross Goldberg) #1

In a plugin that I’m writing, I tried to use ServiceLoader.load(Class) to load providers of an SPI (all defined in my plugin’s code base) in Plugin<Project>#apply(Project), but the classes weren’t found.

The classes were found, however, when I tried the same call in the constructor for my Plugin<Project>.

Thread.currentThread().getContextClassLoader() is a different instance in the two places (but both are of class VisitableURLClassLoader), so I assume that’s the cause, since ServiceLoader.load(Class) calls that.

This requires me to save the loaded providers in an instance field in my plugin, and then use them in apply. I have no need for the providers outside of apply, so I’d prefer to load them there, and to get rid of the instance field.

Is there any nice way to get a correct ClassLoader in apply, or to correctly load the providers in apply some other way?


(Stefan Oehme) #2

Don’t use ServiceLoader for an SPI in Gradle. It’s slow and also inflexible wrt to classloader structure. Instead, have a simple method on your plugin’s extension that integrators can call from their plugin. Users then just apply these additional plugins.

Or did I misunderstand and it’s all part of a single plugin? In that case I don’t understand the need for an SPI though.


(Ross Goldberg) #3

@st_oehme

Thanks for the info.

This is for my update to the experimental-jigsaw plugin.

For now, all the providers are in that plugin, but someone could implement their own provider in another plugin.

I thought I needed something like an SPI, because, e.g., I want to support tasks like KotlinCompile (which is in the Kotlin plugin, not Gradle core) in my plugin. I would prefer if users could apply my plugin, and if the Kotlin plugin is also applied, then my plugin will automatically support it. But, if the Kotlin plugin isn’t applied to the build, the build should complete without errors and without downloading otherwise-unnecessary jars.

There are a few ways to achieve this, but the cleanest I imagined was to use an SPI; ServiceLoader is the built-in way to do that in Java SE.

I prefer not having a jigsaw-java plugin, plus a jigsaw-kotlin plugin, plus a jigsaw-scala plugin, etc. It will be simpler for users if they can just apply a jigsaw plugin that handles many languages / plugin tasks.

Also, a third-party could implement jigsaw compatibility for another plugin by creating a provider for my SPI. They could either put it directly in the affected plugin, if they control its source (e.g., JetBrains & the Kotlin plugin), or they could make a third-party plugin that applies both the main jigsaw plugin and the plugin to be affected (e.g., some random dev wants to add jigsaw support for plugin X, but they are not in control of the source for X).

Potentially, they could submit a PR, and their provider could then be incorporated into the main jigsaw plugin (or it could be left out in a separate plugin if the maintainers of the jigsaw plugin don’t want to include it).

Is there any way other than an SPI to achieve this? Is there any mechanism for an SPI that is better than ServiceLoader?

If I just have a provider registration method, wouldn’t that prevent me from having optional Kotlin plugin support in the main jigsaw plugin without requiring the Kotlin plugin to always be downloaded (and potentially requiring that it be applied)? Wouldn’t I have to split it out into a separate plugin?

If I were to have such a provider registration method, would I call it from the apply(Project) method of the additional plugin?


(Stefan Oehme) #4

To react to other plugins from your code, use plugins.withId. You can use a compileOnly dependency for that so you don’t pull that plugin in unnecessarily.

Also, a third-party could implement jigsaw compatibility for another plugin by creating a provider for my SPI.

They should just call a method on your extension instead. No need for a ServiceLoader.

Trust me, ServiceLoaders in Gradle are a bad idea. The Kotlin team has done it and is suffering for it (or rather their users are suffering. Depending on the user’s build setup, not all extensions are loaded and things don’t work. Plus ServiceLoader is rather expensive, a simple method call on the other hand costs almost nothing.

If I were to have such a provider registration method, would I call it from the apply(Project) method of the additional plugin?

That’s exactly what you would do.


(Ross Goldberg) #5

@st_oehme

Thanks again for the help.

I was already using compileOnly for the Kotlin plugin code for the SPI. I’ll switch over to using plugins.withId instead of the SPI soon; I’m in the middle of some other improvements, and don’t want to context switch right now.