ZetCode

Maven custom plugin

last modified November 13, 2026

In this article we write our own Maven plugin. We create a goal that counts the lines of Java source files, give it parameters, install the plugin, and use it in another project, from the command line and bound to a build phase. The examples use Maven 3.10, Maven Plugin Tools 3.16.0, and Java 25.

Everything Maven does is done by plugins, and every goal of a plugin is implemented by a Java class called a Mojo (Maven plain Old Java Object). When no existing plugin does what a build needs, we can write one. Smaller tasks can often be solved without a plugin, for instance with the Exec or Antrun plugins; a custom plugin is worth it when the logic is shared by many projects.

The plugin project

A plugin is a regular Maven project with the maven-plugin packaging.

linecount-maven-plugin/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>linecount-maven-plugin</artifactId>
    <version>1.0.0</version>
    <packaging>maven-plugin</packaging>

    <name>Line Count Maven Plugin</name>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.9.16</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.16.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.16.0</version>
            </plugin>
        </plugins>
    </build>
</project>

By convention, the artifact ID of a third-party plugin follows the xxx-maven-plugin pattern; the maven-xxx-plugin pattern is reserved for plugins of the Apache Maven project. From the linecount-maven-plugin name, Maven derives the goal prefix linecount.

<packaging>maven-plugin</packaging>

The maven-plugin packaging adds the descriptor goal of the Maven Plugin Plugin to the build. It generates the plugin descriptor, a file that tells Maven which goals the plugin has and which parameters they accept.

<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-plugin-api</artifactId>
    <version>3.9.16</version>
    <scope>provided</scope>
</dependency>

The maven-plugin-api artifact contains the AbstractMojo class and the exceptions. Its scope is provided, because Maven itself supplies these classes when it runs the plugin. The same applies to the annotations.

<maven.compiler.release>21</maven.compiler.release>

The plugin runs in the JVM of Maven, so its bytecode level decides which Java versions can run builds that use it. We compile it for Java 21.

The Mojo

Our goal counts the lines of the Java source files and can fail the build when a file is too long.

linecount-maven-plugin/src/main/java/com/example/linecount/CountMojo.java
package com.example.linecount;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

/**
 * Counts the lines of the Java source files of a project.
 */
@Mojo(name = "count", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true)
public class CountMojo extends AbstractMojo {

    /**
     * The directory with the Java sources.
     */
    @Parameter(defaultValue = "${project.build.sourceDirectory}", readonly = true)
    private File sourceDirectory;

    /**
     * The maximum number of lines of a single file; 0 means no limit.
     */
    @Parameter(property = "linecount.maxLines", defaultValue = "0")
    private int maxLines;

    /**
     * Skips the goal.
     */
    @Parameter(property = "linecount.skip", defaultValue = "false")
    private boolean skip;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Line count skipped");
            return;
        }
        if (!sourceDirectory.isDirectory()) {
            getLog().info("No sources in " + sourceDirectory);
            return;
        }

        Path root = sourceDirectory.toPath();
        List<Path> files;
        try (Stream<Path> paths = Files.walk(root)) {
            files = paths.filter(p -> p.toString().endsWith(".java")).sorted().toList();
        } catch (IOException e) {
            throw new MojoExecutionException("Cannot read " + root, e);
        }

        long total = 0;
        int tooLong = 0;
        for (Path file : files) {
            long lines = countLines(file);
            total += lines;
            getLog().info(String.format("%5d %s", lines, root.relativize(file)));
            if (maxLines > 0 && lines > maxLines) {
                getLog().error(root.relativize(file) + " has more than " + maxLines + " lines");
                tooLong++;
            }
        }
        getLog().info(String.format("%5d lines in %d files", total, files.size()));

        if (tooLong > 0) {
            throw new MojoFailureException(tooLong + " file(s) longer than " + maxLines + " lines");
        }
    }

    private static long countLines(Path file) {
        try (Stream<String> lines = Files.lines(file)) {
            return lines.count();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

The Mojo extends AbstractMojo and implements the execute method.

@Mojo(name = "count", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true)

The @Mojo annotation names the goal. The defaultPhase is the phase the goal binds to when it is used in an execution without a phase. The threadSafe attribute tells Maven that the goal can run in parallel builds started with the -T option.

@Parameter(defaultValue = "${project.build.sourceDirectory}", readonly = true)
private File sourceDirectory;

Parameters are fields annotated with @Parameter. Maven sets them before it calls execute. The default value can be an expression; here it is the source directory of the project. A readonly parameter is not meant to be set in the POM; Maven prints a warning when it is.

@Parameter(property = "linecount.maxLines", defaultValue = "0")
private int maxLines;

The property attribute names a user property that sets the parameter from the command line with -D. Maven converts the text value to the type of the field.

getLog().info(String.format("%5d %s", lines, root.relativize(file)));

The getLog method returns the Maven log. Messages written with it get the [INFO], [WARNING], or [ERROR] prefix and respect the -q and -X options.

throw new MojoFailureException(tooLong + " file(s) longer than " + maxLines + " lines");

Both exceptions fail the build. A MojoFailureException means that the goal found a problem in the project, such as a failed check. A MojoExecutionException means that the goal itself could not run, such as when a file cannot be read.

Installing the plugin

We install the plugin into the local repository, so that other projects on our machine can use it.

The descriptor goal reads the annotations and writes the META-INF/maven/plugin.xml descriptor into the JAR. It records the goal prefix and the required Java and Maven versions. The Java version comes from the release option, the Maven version from the version of the maven-plugin-api dependency. To support older Maven versions, we would build against an older API.

$ mvn install
--- plugin:3.16.0:descriptor (default-descriptor) @ linecount-maven-plugin ---
java-annotations mojo extractor found 1 mojo descriptor.
--- jar:3.5.1:jar (default-jar) @ linecount-maven-plugin ---
--- install:3.2.0:install (default-install) @ linecount-maven-plugin ---
Installing /home/user/project/linecount-maven-plugin/pom.xml to /home/user/.m2/repository/com/example/linecount-maven-plugin/1.0.0/linecount-maven-plugin-1.0.0.pom
Installing /home/user/project/linecount-maven-plugin/target/linecount-maven-plugin-1.0.0.jar to /home/user/.m2/repository/com/example/linecount-maven-plugin/1.0.0/linecount-maven-plugin-1.0.0.jar
BUILD SUCCESS
$ sed -n '5,15p' target/classes/META-INF/maven/plugin.xml
<plugin>
  <name>Line Count Maven Plugin</name>
  <description></description>
  <groupId>com.example</groupId>
  <artifactId>linecount-maven-plugin</artifactId>
  <version>1.0.0</version>
  <goalPrefix>linecount</goalPrefix>
  <isolatedRealm>false</isolatedRealm>
  <inheritedByDefault>true</inheritedByDefault>
  <requiredJavaVersion>21</requiredJavaVersion>
  <requiredMavenVersion>3.9.16</requiredMavenVersion>

Running the goal

Our application has two classes.

app/src/main/java/com/example/app/Greeter.java
package com.example.app;

import java.util.Arrays;
import java.util.stream.Collectors;

public class Greeter {

    private final String greeting;

    public Greeter(String greeting) {
        this.greeting = greeting;
    }

    public String greet(String name) {
        return greeting + ", " + name + "!";
    }

    public String greetAll(String... names) {
        return Arrays.stream(names)
                .map(this::greet)
                .collect(Collectors.joining(" "));
    }
}

The Greeter class creates greetings. Together with the Main class, it is the source code our plugin counts.

A goal of any plugin can be run with its full coordinates, groupId:artifactId:version:goal. The short form with the prefix fails: Maven searches prefixes only in the plugins of the current project and in the plugin groups org.apache.maven.plugins and org.codehaus.mojo. More groups can be added in the pluginGroups element of settings.xml.

The Help plugin describes our plugin from its descriptor. The Javadoc comments of the class and the fields became the descriptions of the goal and its parameters. The read-only sourceDirectory parameter is not listed.

$ mvn com.example:linecount-maven-plugin:1.0.0:count
--- linecount:1.0.0:count (default-cli) @ app ---
   23 com/example/app/Greeter.java
   10 com/example/app/Main.java
   33 lines in 2 files
BUILD SUCCESS
$ mvn linecount:count
BUILD FAILURE
No plugin found for prefix 'linecount' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (/home/user/.m2/repository), central (https://repo.maven.apache.org/maven2)]
$ mvn help:describe -Dplugin=com.example:linecount-maven-plugin -Ddetail
com.example:linecount-maven-plugin:1.0.0

Name: Line Count Maven Plugin
Description: (no description available)
Group Id: com.example
Artifact Id: linecount-maven-plugin
Version: 1.0.0
Goal Prefix: linecount

This plugin has 1 goal:

linecount:count
  Description: Counts the lines of the Java source files of a project.
  Implementation: com.example.linecount.CountMojo
  Language: java
  Bound to phase: verify

  Available parameters:

    maxLines (Default: 0)
      User property: linecount.maxLines
      The maximum number of lines of a single file; 0 means no limit.

    skip (Default: false)
      User property: linecount.skip
      Skips the goal.

Binding the goal to a phase

We add the plugin to the POM of the application.

pom.xml (build)
<build>
    <plugins>
        <plugin>
            <groupId>com.example</groupId>
            <artifactId>linecount-maven-plugin</artifactId>
            <version>1.0.0</version>
            <configuration>
                <maxLines>20</maxLines>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>count</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

The execution has no phase element, so the goal binds to its default phase, verify. The configuration sets the maxLines parameter.

The build fails, because Greeter.java has 23 lines. Trying to raise the limit from the command line does not help: a value in the configuration element always wins over a user property. Since the plugin is now declared in the POM, the linecount prefix works; the linecount.skip property skips the goal.

$ mvn verify
--- linecount:1.0.0:count (default) @ app ---
   23 com/example/app/Greeter.java
com/example/app/Greeter.java has more than 20 lines
   10 com/example/app/Main.java
   33 lines in 2 files
BUILD FAILURE
Failed to execute goal com.example:linecount-maven-plugin:1.0.0:count: 1 file(s) longer than 20 lines
$ mvn verify -Dlinecount.maxLines=50
BUILD FAILURE
$ mvn linecount:count -Dlinecount.skip
--- linecount:1.0.0:count (default-cli) @ app ---
Line count skipped
BUILD SUCCESS

User properties as defaults

To keep a value in the POM and still allow overriding it from the command line, we set the user property instead of the parameter.

pom.xml (properties)
<properties>
    <maven.compiler.release>25</maven.compiler.release>
    <linecount.maxLines>20</linecount.maxLines>
</properties>

The configuration element is removed from the plugin. The linecount.maxLines property sets the parameter, and a -D option replaces the property.

The build fails with the limit from the POM and passes with the limit from the command line.

$ mvn verify
BUILD FAILURE
$ mvn verify -Dlinecount.maxLines=50
--- linecount:1.0.0:count (default) @ app ---
   23 com/example/app/Greeter.java
   10 com/example/app/Main.java
   33 lines in 2 files
BUILD SUCCESS

Source

Guide to Developing Java Plugins, Maven Plugin Tools

In this article we have written, installed, and used a custom Maven plugin with a Mojo class and parameters.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.