A comprehensive guide to create your own maven plugin
Learn how to create your own Maven plugin from scratch in this step-by-step guide.
A comprehensive guide to create your own maven plugin
The term maven is very common among Java developers. Let’s understand what is maven and maven plugins in a nutshell. Maven is a powerful build automation and project management tool developed by Apache which is widely used in the Java ecosystem. It helps in compiling source code, managing dependencies, running tests, packaging applications, and deploying artifacts. Maven plugins are integral to the Maven build system, providing extensibility and customization options for developers. Maven plugins are designed to execute specific tasks or goals during the build lifecycle of a Maven project. These tasks can range from simple actions like copying files or executing shell commands to more complex processes like generating reports, creating documentation, or deploying applications. Plugins are packaged as JAR files and can be easily integrated into Maven projects. At their core, Maven plugins are based on the concept of "Mojo"(Maven plain Old Java Object) which we will discuss in detail in the coming sections. In simple terms a Mojo represents a single unit of work within a plugin. As a part of this, we will develop a java-code-fixer-plugin which will help us in maintaining the consistency in the structure of code across the java project. We will not only create the plugin but will also create a bash script that will execute the plugin everytime we commit a code.
Syntax of Maven Plugin Execution
mvn [plugin-name]:[goal-name] A plugin generally provides set of goals which can be executed using the above syntax. Example: mvn compiler:compile The above command compiles the java project. As we have seen to compile a simple java project, we need maven-compiler-plugin with compile-goal which tells us how important plugins are in maven.
Types of Plugin
Maven provides 2 types of plugins
Build Plugins: They execute during the build process and should be configured in the <build/> element of pom.xml.
Reporting Plugins: They execute during the site generation process and they should be configured in the <reporting/> element of the pom.xml.
Creating a new Maven project for your plugin
Open your favorite IDE. In our case, we will be using Intellij IDEA. Now create a new maven project
The naming convention for our custom maven plugin should be <yourplugin>-maven-plugin. Note: We cannot use the naming convention as maven-<yourplugin>-plugin because it is a reserved naming pattern for official Apache Maven plugins maintained by the Apache Maven team. In our case, we are building a plugin that automatically fixes the formatting of our java code that’s why it is named as javacodefixer-maven-plugin. After creating the project, the pom.xml file will look somewhat similar to this
Add the <packaging> tag with type as maven-plugin <packaging>maven-plugin</packaging>
This tag in the pom.xml file specifies the type of artifact that your maven project produces when it is built. It indicates how your project should be packaged and what type of artifact should be generated. Here are some commonly used values for the <packaging> tag
jar
war
pom
ear
rar
maven-plugin
Here we are using maven-plugin because this packaging type is used for creating Maven plugins. It packages the plugin’s code and resources into the JAR file, which can be used by other projects as a build or execution plugin.
Add <name> and <description> tags <name>Java Code Fixer</name> <description>A plugin that fixes your java code according to the Google standing standards</description>
The <name> tag is used to specify the name of your project and the <description> tag allows you to provide a brief description of your project. It serves as a summary that describes what the project is about. Now you must be thinking where these information will be visible. The information provided in the <name> and <description> tags are visible in:
Maven-generated Documentation
Maven Central Respository
Project Reports
README Files and Project Documentation
Add the required Maven Dependencies
We will add 3 main dependencies: maven-core, maven-plugin-api and maven-plugin-annotations
The maven-plugin-api dependencyprovides the plugin interfaces and base classes required for developing Maven plugins. The maven-core dependency is needed when you want to access project related information in your plugin. It helps to access Maven Project object, which represents the current project being built. This dependency provides the necessary classes and functionality for working with Maven Project. Using the maven-plugin-annotations dependency, you can use the annotations provided by the maven-plugin-annotations plugin in your plugin code. For example, @Mojo, @Parameter which we will discuss in the coming sections.
Add the required Maven Plugins
We also need to add a few plugins in order to build our custom plugin.
org.apache.maven.plugins:maven-plugin-plugin:3.6.0 is commonly referred as a “Plugin Plugin” and is used to generate the necessary files and resources required to create a maven plugin. Org.apache.maven.plugins:maven-site-plugin:3.8.2 also known as the “Site Plugin” which is used to generate documentation or a website for your maven plugin. So our current pom.xml file looks something like this:
Mojo is nothing but a class that is also referred to as Maven plain Old Java Object. Before fully understanding Mojo, let's know what is goal first.
It’s a unit of work. In most cases, goals are linked to phases. As a java developer, you must know some of the important phases of Default Lifecycle which are:
compile
test compile
test
package
install
deploy
Now the above phases can also be considered goals that’s why we write the command mvn compiler:compile to execute the compile goal where compiler happens to be the name of the plugin. Lets understand this with one more example. For that let's keep the concept of phases aside as that can be a little bit confusing. Now for example you want to build a custom plugin say “example-plugin” that provides two goals: “goalA” and “goalB” For “goalA”, you will create a Mojo class that extends the AbstractMojo class provided by the Maven Plugin API. This base class provides essential methods and utilities to handle goal execution, parameter configuration, and interaction with the Maven build system and the same process goes for the “goalB” Now to execute these goals, you would run the following command in your command line interface: mvn ${artifactId}:${goal} mvn example-plugin:goalA -> for executing goalA mvn example-plugin:goalB -> for executing goalB If your plugin is not deployed to the central maven repository then this command will fail. In that case, we have to run the command specifying the groupId as well i.e. mvn ${groupId}:${artifactId}:${goal}
This command should definitely work!!!
Implementing the Mojo classes
We will create a new class: src/main/java/com/coditation/TestMojo.java That will extend from AbstractMojo class which provides essential methods to handle goal execution. Whenever we are creating a new Mojo class then it should extend from AbstractMojo class.
package com.coditation;
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;
@Mojo(name = "fix", defaultPhase = LifecyclePhase.INITIALIZE)
public class TestMojo extends AbstractMojo {
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Java code fixer is running");
}
}
@Mojo annotation is used to define and identify a class as a Mojo within a Maven plugin. It accepts several attributes but the most important ones are name and defaultPhase
name: It specifies the name of the Mojo or in simple terms it is the goal name. If we consider the example that we have discussed in the Understanding the Mojo Concept where we want to build a custom plugin with name example-plugin and goals goalA and goalB.So for this @Mojo annotation will be @Mojo(name = “goalA”) for goalA and @Mojo(name = “goalB”) for goalB
defaultPhase: This attribute is used to specify the default phase of the Maven build lifecycle at which the associated Mojo will be executed. So in our code above the defaultPhase is Lifecycle.INITIALIZE which means if you don’t specify the phase explicitly during the execution of Mojo then it will be executed during the initialize phase. However if you want to execute it during some other phase then you can specify that in the maven command. For example, if you want to execute the Mojo during test-comile phase instead of the default initialize phase then the maven command for that will be:
There are various default phases that you can use according to your requirements. Some of them are:
LifecyclePhase.VALIDATE
LifecyclePhase.COMPILE
LifecyclePhase.TEST_COMPILE
LifecyclePhase.TEST
The getLog().info()logs an informational message. In our case it will print “Java code fixer is running” in the terminal.
Building the plugin using Maven
To build the plugin using maven, we need to run only 2 commands If you are building for the first time:
mvn install :- It will ensure that the project is built, packaged, and installed into the local repository. This allows other projects on your local machine to refer to the installed articfact as a dependency, simplifying development and avoiding version conflicts.
After running this command you will get a message displaying BUILD SUCCESSFUL and also target folder will be created.
mvn com.coditation:javacodefixer-maven-plugin:fix :- It will execute the plugin. The syntax is
mvn ${groupId}:${artifactId}:${goal}
Create an another maven project to test if your plugin is working locally or not and add the plugin properties in pom.xml and then run
mvn com.coditation:javacodefixer-maven-plugin:fix
Once you have added the plugin topom.xml, then you can use shorthand notation to execute the plugin i.e. mvn <prefix>:<goal> In our case, it will be mvn javacodefixer:fix Currently our plugin properties that we have to to add in pom.xml file of another project in order to run our plugin looks something like this:
And the Build is successful as you see the message in the terminal. So our plugin is working as expected. This was the simple example of a plugin that shows only a single message on executing but does nothing. In the coming sections we will see how to build a more useful plugin.
The @Parameter annotation
The @Parameter annotation is used in Maven plugin development to define configurable parameters for a Mojo.
The property attributespecifies the name that will be used to set the value of the greeting parameter
The defaultValue attribute specifies the default value for the parameter in case the user has not explicitly set the value. In our case the default value is “World”.
The execute() method will simply print the greeting message as per the value of the parameter.
Now to execute the plugin we need to run the following command:
mvn ${groupId}:${artifactId}:${goal} -Dname=Ankit
This will print the message: “Hello Ankit”
If we run the command without specifying the value of the property
mvn ${groupId}:${artifactId}:${goal}
This will print the message: “Hello World”
Creating a more useful plugin
Lets create a more useful plugin that actually does some work. In our case, we will be building a code fixer plugin that formats the style of the java code written according to the Google standards. This plugin will help us maintain a consistent style of the code when working on a large project or when multiple developers are contributing to the same project.
Install google-java-format dependency
We will be using third party dependency in our plugin so that we don’t have to write the entire logic from scratch and our code will be error free.
We are using version 1.7 because it has the most usage count in maven repository. Our pom.xml file after adding the dependency looks something like this
ording to the Google standing standards</description>
We have already created a Mojo in “Implementing the Mojo classes” section with the name as TestMojo.java Lets give a useful name like JavaCodeFixerMojo.java Now create a variable with an abstract data type File and data structure as List in which we will store all the java files present in the src directory. private List<File> javaFilesPresentInTheProject = new ArrayList<>(); Now we will create 2 functions - one for returning all the java files and other for checking if the file extension is java or not
//Checks if the file extension is java or not
private boolean isJavaFile(String fileName) {
boolean isJavaFile = false;
int index = fileName.lastIndexOf('.');
if (index > 0) {
if (fileName.substring(index + 1).equals("java")) {
isJavaFile = true;
}
}
return isJavaFile;
}
// Function to return the list of java files.
private List<File> findJavaFiles(File directory) {
for (File file : directory.listFiles()) {
if (file.isDirectory()) {
findJavaFiles(file);
}
// Checking if the file is a java file and adding only that file to the list
String fileName = file.getName();
if (isJavaFile(fileName)) {
javaFilesPresentInTheProject.add(file);
}
}
return javaFilesPresentInTheProject;
}
Now we will create the main function where we will write the logic to reformat the a single java file
private void formatFile(File javaFile) throws MojoExecutionException {
try {
String content;
try {
content = new String(Files.readAllBytes(javaFile.toPath()), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Apply code formatting using Google Java Format
String formattedContent = new Formatter().formatSource(content);
Files.write(javaFile.toPath(), formattedContent.getBytes(StandardCharsets.UTF_8));
if (content.equals(formattedContent)) {
getLog().info("[OK] " + javaFile.getPath());
} else {
getLog().info("[FIXED] " + javaFile.getPath());
}
} catch (Exception e) {
throw new MojoExecutionException(
"Failed to fix code in the file: " + javaFile.getPath() + " due to " + e.getMessage());
}
}
At the final step we will create a parameter to define the path of the file which we want to reformat. This will be very useful in case if we want to reformat a specific file. We will also update the execute() method.
@Parameter(property = "fix.path", required = true)
private String path;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Running Java code fixer...");
try {
if (path.equals("src/main/java")) {
// Specify the source code directory
File sourceDirectory = new File("src/main/java");
// Retrieve all Java source files in the source directory
List<File> javaFiles = findJavaFiles(sourceDirectory);
if (javaFiles.isEmpty()) {
getLog().info("No java files found");
} else {
for (File javaFile : javaFiles) {
formatFile(javaFile);
}
}
} else {
File file = new File(path);
if (!isJavaFile(file.getName())) {
throw new MojoExecutionException("Not a java file: " + file.getPath());
}
formatFile(file);
}
} catch (MojoExecutionException e) {
throw new MojoExecutionException(e.getMessage());
}
}
Our final JavaCodeFixerMojo.java looks something like this
package com.coditation;
import com.google.googlejavaformat.java.Formatter;
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;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
@Mojo(name = "fix", defaultPhase = LifecyclePhase.INITIALIZE)
public class JavaCodeFixerMojo extends AbstractMojo {
private List<File> javaFilesPresentInTheProject = new ArrayList<>();
@Parameter(property = "fix.path", required = true)
private String path;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Running Java code fixer...");
try {
if (path.equals("src/main/java")) {
// Specify the source code directory
File sourceDirectory = new File("src/main/java");
// Retrieve all Java source files in the source directory
List<File> javaFiles = findJavaFiles(sourceDirectory);
if (javaFiles.isEmpty()) {
getLog().info("No java files found");
} else {
for (File javaFile : javaFiles) {
formatFile(javaFile);
}
}
} else {
File file = new File(path);
if (!isJavaFile(file.getName())) {
throw new MojoExecutionException("Not a java file: " + file.getPath());
}
formatFile(file);
}
} catch (MojoExecutionException e) {
throw new MojoExecutionException(e.getMessage());
}
}
// Function to return the list of java files.
private List<File> findJavaFiles(File directory) {
for (File file : directory.listFiles()) {
if (file.isDirectory()) {
findJavaFiles(file);
}
// Checking if the file is a java file and adding only that file to the list
String fileName = file.getName();
if (isJavaFile(fileName)) {
javaFilesPresentInTheProject.add(file);
}
}
return javaFilesPresentInTheProject;
}
private void formatFile(File javaFile) throws MojoExecutionException {
try {
String content;
try {
content = new String(Files.readAllBytes(javaFile.toPath()), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Apply code formatting using Google Java Format
String formattedContent = new Formatter().formatSource(content);
Files.write(javaFile.toPath(), formattedContent.getBytes(StandardCharsets.UTF_8));
if (content.equals(formattedContent)) {
getLog().info("[OK] " + javaFile.getPath());
} else {
getLog().info("[FIXED] " + javaFile.getPath());
}
} catch (Exception e) {
throw new MojoExecutionException(
"Failed to fix code in the file: " + javaFile.getPath() + " due to " + e.getMessage());
}
}
//Checks if the file extension is java or not
private boolean isJavaFile(String fileName) {
boolean isJavaFile = false;
int index = fileName.lastIndexOf('.');
if (index > 0) {
if (fileName.substring(index + 1).equals("java")) {
isJavaFile = true;
}
}
return isJavaFile;
}
}
Now we install the plugin in your local maven repository and use it on in our other projects by following the steps below
Runmvn clean install
This will clean the existing target folder and will create a new one. Remember to run this command everytime you make any changes in the code of the plugin.
Configuring the pom.xml of your new project
Now we will add the our plugin in the pom.xml of another project
Run mvn javacodefixer:fix -Dfix.path=src/main/java if you want to run the run the plugin on all the files in src/main/java directory You can also run: mvn:${groupId}:${artifactId}:${goal} -Dfix.path=${filePath} mvn:com.coditation:javacodefixer-maven-plugin:fix -Dfix.path=src/main/java For running the plugin on a specific file: mvn:com.coditation:javacodefixer-maven-plugin:fix -Dfix.path=src/main/java/Main.java or mvn javacodefixer:fix -Dfix.path=src/main/java/Main.java mvn <prefix>:<goal>
Benefits of creating custom Maven Plugins
Creating custom Maven plugins can bring numerous benefits to your software development process.
Customization: Every project is unique and has its own set of requirements. By creating a custom plugin you can tailor the build process to fit your specific needs. It allows you to define your own goals and tasks that align with your project's development workflow. This customization enables you to automate repetitive tasks, reduce manual effort, and save time.
Automation: Maven plugins automate various aspects of the software development lifecycle. With custom plugins, you can automate tasks such as code generation, documentation generation, running tests, deploying applications, or performing any other project-specific operations
Reusability: Custom Maven plugins can be shared and reused across multiple projects. Once you create a plugin that solves a particular problem, you can use it in different projects with minimal effort. This reusability promotes consistency and standardization across projects, ensuring that best practices and conventions are applied consistently. It also saves development time by eliminating the need to reinvent the wheel for similar functionality in each project.
Integration: You can integrate your plugin with other tools, such as version control systems, continuous integration servers, or code quality analysis tools. This integration enables a smooth flow of data and actions between Maven and other tools, enhancing collaboration and streamlining the development workflow.
Contribution: Creating custom Maven plugins allows you to contribute to the broader Maven ecosystem. As you gain expertise and develop plugins to solve specific problems, you can share them with the community. By sharing your plugins, you not only help others facing similar challenges but also receive feedback and suggestions from the community to improve and enhance your plugins. It fosters collaboration and knowledge sharing within the developer community.
Hi, I am Ankit Pradhan. I am a Java backend developer specialized in Spring Framework. In love with solving business related problems with the help of code. In my free time, you will either find me watching a thriller movie or reading a technical blog.
Want to receive update about our upcoming podcast?