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.
<?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.
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.
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.
<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.
<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
List all Java tutorials.