We have a plugin that does a decent amount of service loading. We’ve been doing it using the traditional ServiceLoader pattern with a META-INF/services registration. We understand the proper way to do this is via application/projectServices in the config.xml files, so we started moving them over to that pattern. This has worked great except for one case where we want to load multiple implementations of an interface. We found the ApplicationManager.getApplication().getServices() method, but it appears to just load the first found registered implementation (and it’s anyone’s guess what gets loaded first
) and return that instead of loading all of them.
Do you have any recommendations on the proper way to do this? Our hack right now is to create those classes using Class.forName(className).getConstructor().newInstance(), which is obviously disgusting since we have to hard code the classes (and completely counter to how we try and organize our projects).
You definitely need extensions instead of services.
We tried to setup extension points but no matter what we do we end up with an error when trying to access the extensions. Missing extension point: <my extension> in container Application (internal) (RA allowed)
I created a very stripped down replication of our project’s setup, and it also generates this error. The repo is here: https://github.com/MrAaronOlsen/MultiProjectIntelliJPluginExample.
We’re nearly certain we’re going about this all wrong, so if you can find a little time to review this and tell us what that is, we’d be grateful.
Unfortunately Github link produces 404
I failed to make it public. I apologize for that. You should have access now.
First of all, declare EP_NAME with static modifier somewhere (do not create in-place inside processing):
static final ExtensionPointName<MyInterface> extensionPointName =
ExtensionPointName.create("net.meadow.projectA.myInterface");
For instance, in the extension interface:
public interface MyInterface {
ExtensionPointName<MyInterface> EP_NAME =
ExtensionPointName.create("net.meadow.projectA.myInterface");
String getName();
}
Then you should fix qualified name in declaration to:
<extensionPoints>
<extensionPoint qualifiedName="net.meadow.projectA.myInterface" interface="net.meadow.MyInterface" dynamic="true"/>
</extensionPoints>
Finally, dependency declaration for optional may not use project_a.xml, it needs a separate XML file to declare extension instances:
<depends optional="true" config-file="when_a_is_loaded.xml">net.meadow.ProjectA</depends>
The biggest problem is our different plugin declaration formats. For multi-module project setup you unfortunately need the newer format which we have not properly documented yet.
Here is the example with fixes GitHub - jreznot/intellij-multi-module-plugin-fixed
Let’s try to build a structure with plugin model v2 approach:
First, the main plugin.xml declares the set of modules, this is plugin itself:
<idea-plugin>
<id>net.meadow.ProjectA</id>
<name>ProjectA</name>
<vendor url="https://www.meadow.com">Meadow</vendor>
<description>An example project that has an extension point</description>
<dependencies>
<plugin id="com.intellij.modules.platform"/>
</dependencies>
<content>
<module name="MultiProjectExample.project_a"/>
<module name="MultiProjectExample.project_b"/>
</content>
</idea-plugin>
In the main build.gradle.kts we refer to modules included as pluginModule:
dependencies {
intellijPlatform {
var type = "IU"
var version = "2025.2.5"
create(type, version)
pluginModule(implementation(project(":project_a")))
pluginModule(implementation(project(":project_b")))
}
}
Then, project A inside /resources (not in META-INF!) declares MultiProjectExample.project_a.xml (name of the file is important, it must match the module ID):
<idea-plugin>
<extensionPoints>
<extensionPoint qualifiedName="net.meadow.projectA.myInterface"
interface="net.meadow.projectA.MyInterface" dynamic="true"/>
</extensionPoints>
<actions>
<action id="net.meadow.projectA.MyAction"
class="net.meadow.projectA.MyAction"
text="My Action"
description="Test an Action"
icon="AllIcons.Actions.Execute">
<add-to-group group-id="MainToolbarRight" anchor="last"/>
</action>
</actions>
</idea-plugin>
Then project B declares in /resources its descriptor with an explicit dependency on project_a (MultiProjectExample.project_b.xml):
<idea-plugin>
<dependencies>
<plugin id="com.intellij.modules.platform"/>
<module name="MultiProjectExample.project_a"/>
</dependencies>
<extensions defaultExtensionNs="net.meadow.projectA">
<myInterface implementation="net.meadow.projectB.MyInterfaceImpl"/>
</extensions>
</idea-plugin>
Here I must also say we must avoid split Java packages and do not try to include XML via x-include.
That’s amazing Yuriy! Thank you so much! We’re going to spend some time with this and see if we can get this implemented correctly. We really appreciate you!
I just wanted to follow up. Based on your examples and recommendations we got everything working as expected. So thank you Yuriy for your help and thorough examples! We love how clean and simple everything is now.
I understand this module functionality is a work in progress, so I thought I’d just share a bit of our experience.
- When you fail to set up a module correctly the only error you get is that the plugin has a misconfigured descriptor. We did a lot of trouble shooting to figure out what we did wrong in those cases.
- When a gradle.build file depends on a gradle project that is being handled as a plugin module, the .xml file for that project also has to include the module as a dependency. The only error you get here are class loader issues at runtime. By this point we sort of intuitively understood what needed to happen, but can see this being confusing.
Otherwise we’re full steam ahead now with this. Can’t thank you enough for your help.