maven plugin.png

I wanted to write a Maven plugin with no experience on it. This is a short summary on my takeaways.

As first step I would recommend Maven Apache documentation short tutorial at https://maven.apache.org/guides/plugin/guide-java-plugin-development.html. And then…​

Archetype scaffolding

You can start with the Maven archetype generator

mvn archetype:generate -DgroupId=org.apache.maven -DartifactId=maven-plugin-test -DarchetypeArtifactId=maven-archetype-mojo -DinteractiveMode=false

Maven generates the scaffolder of the Maven plugin project. Check the pom.xml of the generated project and its dependencies. There is provided dependency org.apache.maven:maven-plugin-api which provides parent class AbstractMojo for starting to write a Maven plugin. The generator then introduces class or.apache.maven.MyMojo. Check there the javadoc annotations used to define properties of the plugin. The documentation for this approach could be found at https://books.sonatype.com/mvnref-book/reference/writing-plugins-sect-mojo-params.html. I haven’t written the plugin this way but I changed for using Maven annnotations.

Maven plugin creation

Plugin project dependencies

I took the project and add these dependencies to the pom.xml.

<!-- basic Maven plugin dependency containing `AbstractMojo` class as parent
     to all Maven plugin instances -->
<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-plugin-api</artifactId>
    <version>3.5.2</version>
    <scope>provided</scope>
</dependency>
<!-- annotation used for plugin properties definitions and injecting Maven objects -->
<dependency>
    <groupId>org.apache.maven.plugin-tools</groupId>
    <artifactId>maven-plugin-annotations</artifactId>
    <version>3.5.1</version>
    <scope>provided</scope>
</dependency>
<!-- Introducing Maven objects `MavenProject`, `PluginDescriptor` etc. -->
<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-core</artifactId>
    <version>3.5.2</version>
    <scope>provided</scope>
</dependency>
I’m not sure about the correct scope but the provided works for me.

Source code definition

The Maven plugin consists from one (or several) classes which inherit from class org.apache.maven.plugin.AbstractMojo. When inherited it forces you to implement method execute() which is called when the plugin is invoked. The plugin is introduced by annotation @Mojo (Maven plain Old Java Object ) marking the class as the Maven plugin, assigning the name of executable goal. You can create multiple Mojo classes in your project and cover more goals.

import org.apache.maven.plugin.AbstractMojo;

@Mojo(name = "go-test")
public class MavenPluginTest extends AbstractMojo {

  @Override
    public void execute() throws MojoExecutionException, MojoFailureException     {
    }
}

To get executed your plugin in the referenced project you will add this to the pom.xml

<build>
   <plugins>
     <plugin>
       <groupId>org.apache.maven</groupId>
       <artifactId>maven-test-plugin</artifactId>
       <version>...</version>
       <executions>
         <execution>
           <phase>process-classes</phase>
           <goals>
             <goal>go-test</goal>
           </goals>
         </execution>
       </executions>
     </plugin>
   </plugins>
 </build>
you can define multiple <execution> elements (e.g. with different configuration), you can use multiple <goal> elements to get them executed

Plugin parametrized

The plugin can be parametrized by using @Parameter annotation. This provides way of passing values from the pom.xml to the plugin code execution. Let’s say you want to provide name of the build directory for your plugin which is resolved for ./target/classes. The project executing the plugin defines in pom.xml

<plugin>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-test-plugin</artifactId>
  <version>...</version>
  <configuration>
    <buildDirectory>${project.build.directory}</buildDirectory>
  </configuration>
  <executions>
    <execution>
      <phase>process-classes</phase>
      <goals>
        <goal>go-test</goal>
      </goals>
    </execution>
  </executions>
</plugin>
the <configuration> element could be part of the <execution> element too
@Mojo(name = "go-test")
public class MavenPluginTest extends AbstractMojo {

  @Parameter
  private String buildDirectory;

  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {
  }
}

We’ve used the simple @Parameter annotation. This annotation offers several attributes. We can define the default value as the project build folder while using in form of Maven variable @Parameter(defaultValue = "${project.build.directory}").

A bit trouble is with multi value (array type) parameter. The defaultValue is not evaluated. This @Parameter(defaultValue = "${project.build.directory}") private String[] buildPaths does not work. We can define the default value by direct assignment @Parameter private String[] buildPaths = new String()["${project.build.directory}"] which works but it does not evaluate the stig form of the Maven configuration property.

Maven brings several classes that could be used for getting information from the Maven execution. One of them is org.apache.maven.project.MavenProject that could be injected to @Parameter annotation and then queried for things like build directory. One way of solving this is

@Parameter
private String[] buildPaths;

@Parameter(defaultValue = "${project}", readonly = true, required = true)
protected MavenProject project;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
  if(buildPaths == null) buildPaths
    = new String[] {project.getBuild().getOutputDirectory()};

  getLog().info("provided buildPath is '" + Arrays.asList(buildPaths) + "'");
}

Here I use the getLog() method declared in the AbstractMojo providing the Maven log - driven by Maven execution parameters (e.g. debug is switch on while run mvn install -X).

The nice brief summary of the annotations and the Maven classes to be used in the Maven plugin is provided at https://maven.apache.org/plugin-tools/maven-plugin-tools-annotations

Definition of default phase

The @Mojo annotation provides way of defininig default phase for the plugin being executed. Then this information is not needed to be part of the definition of the pom.xml (you can omit <phase> element under <execution>).

@Mojo(name = "go-test", defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class MavenPluginTest extends AbstractMojo {
<plugin>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-test-plugin</artifactId>
  <version>...</version>
  <executions>
    <execution>
      <goals>
        <goal>go-test</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Consult the documentation to check all the annotations offered by Maven annotations artifact and their parameters http://maven.apache.org/developers/mojo-api-specification.html

Class loading troubles

I needed to create a plugin which takes a list of paths which will be scanned for classes and then loaded. I found that the I need to get a bit into classloading scheme of the Maven plugin. You can check the explanation at http://takari.io/book/91-maven-classloading.html.

My trouble was that Class.forName("cz.chalda.MyClass") was not resolved with the restricted classpath enriched with the dependencies taken during project compilation. Normally (at least I understand this) the plugin can see dependencies defined in compile scope of the plugin project itself. Not the project it’s referenced in (the project the plugin is executed at).

This could be shown with use of PluginDescriptor Maven class. You can verify it with the following code snippet

@Parameter( defaultValue = "${plugin}", readonly = true )
private PluginDescriptor pluginDescriptor;

// -- or --

final PluginDescriptor pluginDescriptor = (PluginDescriptor) getPluginContext().get("pluginDescriptor");

// printing the ClassRealm content containing plugin classpath dependencies
final ClassRealm classRealm = pluginDescriptor.getClassRealm();
for(URL url: classRealm.getURLs()) getLog().info(" >>> " + url.toString());

The ClassRealm can be enriched by URL to broad class loading scope

final File classes = new File(getProject().getBuild().getOutputDirectory());
try {
  classRealm.addURL(classes.toURI().toURL());
} catch (MalformedURLException e) {
  getLog().error("Can't create URL from path to project output directory '"
    + getProject().getBuild().getOutputDirectory() + "'", e)
}

If you want to get the classpath depenedencies from the project the plugin is executed at, you can use the following code snippet

@Mojo(name = "go-test", requiresDependencyResolution = ResolutionScope.COMPILE)
public class MavenPluginTest extends AbstractMojo {

  @Parameter(defaultValue = "${project}", readonly = true, required = true)
  protected MavenProject project;

  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {

    // listing the Maven project class path of compile and runtime
    try {
        getLog().info(("compile cp: " +
          this.project.getCompileClasspathElements());
        getLog().info(("runtime cp: " +
          this.project.getRuntimeClasspathElements());
    } catch (DependencyResolutionRequiredException e) {
        new MojoExecutionException("Dependency resolution failed", e);
    }
  }
}

You can see I used the @Mojo annotation attribute requiresDependencyResolution defining that the depenedencies should be resolved for this plugin. If it’s not used (at least in my experience) the list of the compile class path elements will contains only path ./target/classes but not the list of the Maven project dependencies of compile scope, which was desired.

With these I could create my own class loader and particularly say what is the scope of the class loading (you can check other notes on this over here http://blog.semsur-it.com/2011/11/java-class-loader-and-maven-plugin.html)

List<URL> pathUrls = new ArrayList<>();
for(String mavenCompilePath: project.getCompileClasspathElements()) {
    currentPathProcessed = mavenCompilePath;
    pathUrls.add(new File(mavenCompilePath).toURI().toURL());
}

URL[] urlsForClassLoader = pathUrls.toArray(new URL[pathUrls.size()]);
getLog().debug("urls for URLClassLoader: " + Arrays.asList(urlsForClassLoader));

// need to define parent classloader which knows all dependencies of the plugin
classLoader = new URLClassLoader(urlsForClassLoader, MavenPluginTest.class.getClassLoader());

Note: class loader debugging

I found it’s handy to understand what are the resources the particular class loader works with. Thus I took inspiration from http://www.java2s.com/Tutorial/Java/0125__Reflection/AnalyzeClassLoaderhierarchyforanygivenobjectorclassloader.htm and adjusted for my purposes https://github.com/ochaloup/class-loader-debug/blob/master/src/main/java/cz/chalda/classloader/ClassLoaderUtils.java

With this you can get printed the tree of the classloaders (up to the parent one) and check what is the classpath the class loader works with.

bear in mind the normal java classloader first ask parent if the resource is known and then it tries to resolve it himself. This is reflected in the printing (ie. you can see the child classloader prints it can work with what the parent classloader is capable too).
See https://zeroturnaround.com/rebellabs/rebel-labs-tutorial-do-you-really-get-classloaders/2/.

The usage of the ClassLoaderUtils class could be in way

System.out.printf("%n--%ncontext class loader hierarchy: %s",
  ClassLoaderUtils.showClassLoaderHierarchy(
    Thread.currentThread().getContextClassLoader()));
System.out.printf("%n--%nplugin class loader hierarchy: %s",
  ClassLoaderUtils.showClassLoaderHierarchy(
    MavenPluginTest.class.getClassLoader()));
comments powered by Disqus