Writting maven plugin using Kotlin.
- 6 minutes to read
- 1103 words
- Comments
If you are a Java developer you must be familiar with maven - a widely used Java build system. Maven has simple and reliable execution lifecycle: it runs several consecutive phases (like compile, test, package or deploy), and during each phase a set of plugins can be run, for example javac via maven-compiler-plugin during phase ‘compile’.
For a particular plugin one declares a <plugin>
section in pom.xml
, and each plugin can have several executions. Each
execution is bound to a particular phase of build lifecycle, and it may include several goals. Goal is a particular action
the plugin can execute, for example compile source code or run tests or check code style.
We are developing diktat - an automatic kotlin code style checker and formatter, which is intended to be used as CI/CD tool that constantly checks quality of code that developers are adding to their projects. To provide a convenient way to run it for all developers we support run from CLI and are preparing to release a dedicated maven plugin to run diktat directly from maven.
Designing a plugin
MOJO class
A main structural entity in maven plugin is a MOJO. MOJO stands for ‘Maven POJO’ or ‘Maven plain old Java object’, and it is
a class that defines logic for a particular plugin goal. MOJO is a Java class annotated with @Mojo
annotation, but since we
are developing kotlin code style checker, we are going to implement our plugin in kotlin too. Kotlin code can be compiled in
Java bytecode, so it won’t be a problem.
Desired usage
When the plugin is ready, we want to invoke it like this:
<build>
<plugins>
<plugin>
<groupId>org.cqfn.diktat</groupId>
<artifactId>diktat-maven-plugin</artifactId>
<version>${diktat.version}</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
With this config in pom.xml
during mvn verify
the goal check
will be run.
Implementation
Implementation of the plugin seems straightforward:
@Mojo("check")
class DiktatCheckMojo: AbstractMojo() {
/**
* Paths that will be scanned for .kt(s) files
*/
@Parameter(property = "diktat.inputs")
var inputs = listOf("\${project.basedir}/src")
// other properties...
override fun execute() {
// all logic goes here
}
}
The main point is the execute
method which will be called during our MOJO invocation. Also, maven lets us capture parameters passed
via configuration in pom.xml by simply annotating properties with @Parameter
.
Sharing common logic among plugin goals
Note, that only classes annotated with @Mojo
will be considered as mojos, not the classes extending AbstractMojo
. It means
that we can create a base abstract class extending AbstractMojo
with common logic and configuration parameters and then
create implementations of that base class for every particular plugin goal that we want.
For example, in diktat-maven-plugin
we want to support two operation modes: checking code style and fixing it. The following code
will help us achieve this:
class DiktatBaseMojo : AbstractMojo() {
/**
* Path to diktat yml config file. Can be either absolute or relative to project's root directory.
*/
@Parameter(property = "diktat.config", defaultValue = "diktat-analysis.yml")
lateinit var diktatConfigFile: String
abstract fun runAction(params: KtLint.Params)
override fun execute() {
// common logic
runAction(params)
}
}
@Mojo("check")
class DiktatCheckMojo : DiktatBaseMojo() {
override fun runAction(params: KtLint.Params) {
// logic of check goal
}
}
@Mojo("fix")
class DiktatFixMojo : DiktatBaseMojo() {
override fun runAction(params: KtLint.Params) {
// logic of fix goal
}
}
Instrumentation
What is less straightforward is how to package our code as a maven plugin.
You should create a maven plugin as a separate maven module in (possibly) multi-module maven project. The first step is to change the module’s packaging to a special value:
<packaging>maven-plugin</packaging>
Then we need to add a maven-plugin-plugin
to pom.xml
of our plugin module. This plugin will create our plugin’s descriptor -
a file called plugin.xml
, which will be included in resulting jar and will store all metadata that maven needs to run our plugin.
plugin.xml
pretty much acts as a pom.xml
for a plugin: it contains groupId, artifactId and version of plugin as well as
some special fields, e.g. goalPrefix
is a string which will be used when calling plugin goals from CLI (i.e. in diktat:check
diktat
is a goalPrefix).
This file also contains a list of mojos - xml descriptors for all plugin goals that we created. Mojo has a list of parameters, so that
IDE can suggest you values when configuring your pom.xml
, as well as goal and parameter descriptions - pieces of documentation
that IDE can show you when selecting plugin parameter.
Here appears the only real caveat of developing maven plugin entirely in kotlin: maven-plugin-plugin
by default retrieves
descriptions from JavaDocs. Kotlin doesn’t have JavaDocs, it has KDocs which are handled differently with special dokka
tool.
Luckily, maven-plugin-plugin
uses a set of extractors
to retrieve descriptions, and extractors can be overridden. Luckily x2,
there already exists an open-source maven plugin called kotlin-maven-plugin-tools,
which has embedded dokka and provides kotlin
extractor. After adding this plugin to our build and adding to maven-plugin-plugin
configuration
<extractors>
<!-- Extractor from kotlin-maven-plugin-tools -->
<extractor>kotlin</extractor>
</extractors>
we get a nice plugin.xml
with goals and properties descriptions.
Last steps
Now you can install a plugin to a local or remote maven repository and use it in your build!