A new look at the Maven-plugin for IDEA – GMaven
Hello, my name is Hryhoriy Myasoedov, I previously had experience working at JetBrains in the build tools team, namely, I was working on the Maven-plugin. In this article, I want to talk about how the plugin works under the hood, its strengths and weaknesses, and what I ended up doing with it all.
One of the most frequent problems I dealt with in JetBrains sounded like this – through the Maven command line, the project compiles, but in IDEA it is not imported (it is imported with errors). As will be shown below, most of these problems are related to the architecture. JB Maven plugin.
Contents
Overview of Maven plugin IDEA
The main task of the plugin for the IDE is to receive a project model from the build system in order to configure the project structure in the IDE itself based on this data (modules, their directories – java/test/resources, dependencies, etc.).
Maven internally uses Google Guice as dependency injection framework. For each launch, it creates a new process and raises with help Guice its programming context. The main components of which are:
-
ProjectBuilder – is responsible for building a project model in memory based on build scripts;
-
ModelInterpolator – replaces type expressions
${value}
their current values; -
ProjectDependenciesResolver – resolve dependencies, including transitive ones;
-
MavenSession – session context containing all process parameters;
-
MavenProject – the main class of the internal data model.
The current architecture of the JetBrains Maven plugin looks something like this: From the low-level Maven components above, a custom lightweight process is built that reads the build files, enables all project dependencies, and returns the project model. (The Eclipse plugin, by the way, uses the same approach). This process is run as a “daemon” to raise the program context once when it is first run, and then it is reused.
Advantages of this approach:
-
it is more light-hearted and uses only what is necessary for the final result (getting a project model with all dependencies);
-
reuses the program context;
-
as a result works faster;
-
due to the complete customization of the process, it is easier to add various features for the IDE;
Cons:
-
due to the fact that it is difficult to reproduce the original Maven process one by one, bugs constantly appear – something was not taken into account/missed;
-
Maven changes frequently at this low level. You constantly have to play catch-up with him, adding new Maven features to the JB process;
-
this also results in frequent headaches with the release of new versions of Maven and compatibility support (for example, the release of versions – 3.8.5, 4.0);
-
The IDEA Maven daemon saves state as the current Maven settings. This adds additional complexity;
-
difficult to maintain.
As a summary: the root cause of many problems was that the original Maven process is different from the JB process
You can see what this process looks like for Maven 3.x here. The code is full of Java Reflection and Maven version checks. A recent example is that Maven 4 support did not work. there were many changes in Maven 4, so it required a new process to be created for this version. We receive a large amount of code and its duplication. What affects the complexity of the project and its support and the stability of the plugin.
There is also an Open Source implementation of the “daemon” process for Maven – the Mvnd project (article on Habr). If you look at its original sources, you can see similar problems there. The daemon-m40 module also appeared there, as well as daemon-m39. This shows how non-trivial a task it is to create and maintain your “daemon” process for Maven and how easily mistakes can be made there. And you constantly need to “catch up” with Maven.
GMaven plugin overview
Even in JBI proposed a completely different approach – to resolve project dependencies through a custom maven plugin (
But due to well-known events, he did not have time to start implementing it JB. And so that it was not just a matter of words, but also showed in practice that this approach works, I decided to write my own Maven plugin for IDEA, which I called GMaven.
The main module of my plugin for IDEA is a plugin directly for Maven itself. The essence of which will allow all dependencies of the project. It contains almost no logic. There are three classes in total – one of which is a DTO, the other is a utility and the main Mojo class.
Consider an example of the simplest Maven plugin:
@Mojo(name = "my_task_name", defaultPhase = NONE, aggregator = true, requiresDependencyResolution = TEST)
public class ResolveProjectMojo extends AbstractMojo {
}
-
name – The name of the “pull” plugin;
-
defaultPhase – the phase of the life cycle to which the plug-in is bound by default;
-
aggregator – the true value means that the plugin is executed once for the entire aggregator, and not for each subproject separately;
-
requiresDependencyResolution – scope is required to resolve dependencies.
To run the plugin from the command line, execute: mvn
My Maven plugin code isn’t much more complicated than this example. A class of only 200 lines and the essence of this logic is data mapping for obtaining configurations of a number of plug-ins (configured through the extension point of the main GMaven plug-in), necessary for the correct import of the project model into IDEA. (As an example: maven-compiler-plugin, where we get the compiler parameters to pass to IDEA and the project could be assembled through the development environment). Next, the finished project model, with all dependencies allowed, is returned as the output of the process via the Maven build event listener. The Maven plugin is added to the user’s local m2 repository while the main IDE plugin is running.
Here we should dwell in a little more detail on how I get the project model from Maven, because its process does not expect to return any result other than the process code. IN JB the plugin does not have such a problem, because they have their own custom process where they directly operate on internal Maven objects and can do whatever they want with it. Therefore, they “wrapped” their “daemon” process in RMI and immediately receive a ready-made project model as a result of calling the method responsible for obtaining it.
I had two ways:
-
return the result through Maven output in the form of strings and serialize/deserialize it in some format (for example, JSON);
-
or also wrap the RMI process and return Java objects.
I chose the second way, such a mechanism is used in JB plugin and I was well acquainted with it. And from the point of view of saving time, it was better for me to reuse already ready code. Although the first option is more ideologically correct. So I also wrapped my process in RMI. And as I wrote above, through the event listener of the Maven assembly, I save the project model in a static variable, the result of which I take at the end of the RMI method call, which starts the usual Maven process.
Then, we import the received Maven project model into IDEA through ExternalSystem API. As a result, almost everything worked “out of the box” and the GMaven plugin is also basically just a mapping from the Maven project model to the ExternalSystem structure, which then itself fits into the IDEA project structure (Project Structure … ctrl + alt + shift + s). I plan to talk about more detailed work with the ExternalSystem API and other IDEA extension points necessary for writing such plugins in the next article, if this topic is of interest to anyone.
As a result, we get:
-
a very simple process of interaction with Maven, which consists in launching the plugin;
-
a complete Maven life cycle with all current features, which makes it impossible to have bugs of the kind that we did not take into account when obtaining the project model.
The results
GMaven |
IDEA Maven |
||
Quarks (~1100 modules) |
import errors |
– |
+ |
assembly errors |
+/- |
+ |
|
import time (sec) |
110 |
60 |
|
Dbeaver (~150 modules) |
import errors |
– |
+ |
assembly errors |
– |
+ |
|
import time (sec) |
60 |
||
Spring-Boot-2.1.x (~100 modules) |
import errors |
– |
– |
assembly errors |
+/- |
+/- |
|
import time (sec) |
20 |
12 |
|
Maven 3.8.x (15 modules) |
import errors |
– |
– |
assembly errors |
– |
– |
|
import time (sec) |
2 |
2 |
-
all dependencies at the time of measurements were already in the local repository;
-
In the Spring-Boot project, assembly errors in both plugins are caused by the gradle plugin module, if it is disabled, assembly is successful;
-
Dbeaver IDEA Maven plugin failed to import at all;
-
comparisons were made on IDEA version 2023.2-Xmx4g, i7-10875H, 32gb.
In general, we can say that the project import time is the same as in the original one JB plug-in and in mine, on small and medium projects up to ~50 modules, roughly comparable. On projects with a large number of modules, due to a completely custom process of obtaining a project model and a number of optimizations, the original plugin works faster.
Current status of the project
At this stage, this is an MVP with basic capabilities:
-
full import of the project model from Maven to IDEA;
-
execution of Maven pulls;
-
work with dependencies + Dependency Analyzer;
-
creation of Run Configurations for launch;
-
opening an existing project; creating a new project/module.
-
support for Maven 3.3.1+ (JDK 7+)
-
version IDEA 2022.2+
The basis is simplicity of development and stability. When receiving a project model, the build window of the IDE displays the standard Maven output, which helps in localization and solving most problems.
Of course, I might have backwards compatibility issues too. But Maven, at the level of the command line, project model and plugin api, changes much less often and is more stable. And at the moment I don’t have separate logic for Maven 3 and Maven 4. There is one simple general process – run a Maven task.
Yes, now, my plugin has fewer options than the original one, but on the other hand, because of this, it works faster in some aspects and consumes less OP, because stores less state. I use my plugin at my current place of work and this functionality is sufficient for my needs.
The main disadvantages of my plugin include:
-
for each project model import run, it creates a new process and raises the Maven program context. On average, it takes 0.5 seconds. I consider it a moderate price to pay for simplicity. There are ideas how it can be improved – integration with mvnd and delegating the execution of my maven plugin to it, so as not to write my own “daemon” process and not deal with its support;
-
incremental update of build scripts is not implemented, but it is noticeable only on projects with a large number of Maven modules – Quarkus/Spring;
-
the project is covered by tests, because The main task at the moment was to quickly finish the development and convey my opinion without spilling it, and to roll out the prototype.
The immediate goal is to collect feedback and understand whether it will be useful to someone. And fixing bugs that are in the process of the plug-in.
Result
The plugin is published in the alpha channel of the main marketplace. In order to download it through the IDE, you need to add a repository in the alpha settings – https://plugins.jetbrains.com/plugins/alpha/list.
Next can be used to open existing Java Maven projects. And for creating new ones through the standard wizard. I will be glad if it is useful to someone and helps to solve the problems of importing the project into the IDE. If not, then my contacts for communication are on the home page of the plugin and you can start an issue. You can also feel free to write to me by private message on Habr.