Цель статьи - рассказать об опыте разработки плагина.
Базовые понятия
@Mojo – исполняемый класс maven плагина.
AbstractMojo – базовый абстрактный класс maven .
@Parameter – параметры, которые указываются при вызове плагина.
Простой пример. Плагин для проверки наличия каких либо файлов в проекте

project-file-verifier-maven-plugin – подмодуль, содержащий mojo и логику проверки наличия файла readme.md. Описывает простое взаимодействие плагина с CDI контейнером.
verify-checks – подмодуль выполняющий ряд проверок работы project-file-verifier-maven-plugin для демонстрации.
CDI
Помимо readme.md существуют и другие базовые файлы в проекте которые необходимо валидировать. Чтобы расширить проверки понадобится добавить еще несколько реализаций интерфейса CheckService. Но встает вопрос, как мы будем создавать объекты этих классов, т.к. все привыкли к практикам CDI. Я выбирал для себя контейнер weld-se-core.
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>3.1.4.Final</version>
</dependency>
<dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
Инициализация такого контейнера:
Weld weld = new Weld();
WeldContainer container = weld.initialize();
Инициализация бинов в контейнере:
WeldContainer.current().select(clazz).get()
Для удобства я вынес эти строки в static блок отдельного класса Util
public class CDIContainerUtil {
static void initIfNotExist() {
if (WeldContainer.getRunningContainerIds().size() == 0) {
Weld weld = new Weld();
weld.initialize();
Reflections reflections = new Reflections("chigarout.tutoral");
Set<Class<?>> set = reflections.getTypesAnnotatedWith(javax.inject.Singleton.class);
for (Class class1 : set)
WeldContainer.current().select(class1);
}
}
}
Соответственно добавив аннотацию @Singlton для сервиса и @Inject для привязки должны получить работающий плагин уже на базе CDI контейнера. Добавляем следующий mojo для проверки наличия gitignore.
import chigarout.tutorial.project_file_verifier.service.CheckService;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import javax.inject.Inject;
import javax.inject.Named;
@Mojo(name = "gitignore_check", threadSafe = false)
public class GitIgnoreCheckMojo extends AbstractMojo {
@Inject
@Named("GitIgnoreCheckService")
CheckService mdParser;
public GitIgnoreCheckMojo() {
CDIContainerUtil.initIfNotExist();
}
public void execute() throws MojoExecutionException, MojoFailureException {
if (!mdParser.check()) throw new MojoFailureException("Необходимо добавить gitignore в проект.");
getLog().info(String.format("Файл gitignore существует в проекте"));
}
}
Mojo для проверки наличия readme.md.
@Mojo(name = "check", threadSafe = false)
public class CheckMojo extends AbstractMojo {
CheckService mdParser;
public CheckMojo(){
mdParser = new MDCheckService();
}
public void execute() throws MojoExecutionException, MojoFailureException {
File mdFile = new File("readme.md");
if(!mdParser.check(mdFile)) throw new MojoFailureException("Необходимо добавить readme.md в проект.");
getLog().info(String.format("Файл %s существует в проекте", mdFile.getAbsoluteFile()));
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>chigarout.tutoral</groupId>
<artifactId>cdi-base-tutorial-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>project-file-verifier-maven-plugin</artifactId>
<packaging>maven-plugin</packaging>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.5.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.4</version>
</plugin>
</plugins>
</build>
</project>
Тестирование.
Когда плагин разросся или содержит сложную логику может быть проблематично выполнять отладку. Как следует отладив плагин можно потратить уйму времени. Есть возможность протестировать плагин через JUinit. Для этого на понадобится maven-plugin-testing-harness.
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
Внутри плагина содержится AbstractMojoTestCase от которого нужно будет создавать имплементации тестов. Тест кейс не воспринимает аннотации @Test @Before и т.д. Вместо этого он поддерживает именование методов с приставкой test. Вместо @Before нужно переопределить метод protected void setUp() от AbstractMojoTestCase. Если мы хотим использовать CDI то нужно пользоваться методом protected Mojo lookupMojo(String goal, File pom) throws Exception.
· goal – наименование цели.
· File – тестовый pom файл. Можно указать и основной, но я добавил отдельный pom test/resources.
Есть непонятное поведение плагина. При том что мы указываем pom из папки ресурсов тестов плагин все равно будет смотреть в корневой pom чтобы взять оттуда <artifactId>. Далее по этому artifactId он будет искать плагин в <build> для запуска уже в тестовом pom, что не совсем верно в случае если мы хотим тестировать плагин как внешнее api, т.е. если тесты находятся в verify-checks, а не в project-file-verifier-maven-plugin. Для этого я добавил в переопределение переменной basedir в тесте.
protected void setUp() throws Exception {
String oldpath = System.getProperty("user.dir");
System.setProperty("basedir",oldpath+"/src/test/resources");
super.setUp ();
}
В добавок пришлось в тестовом pom выставить artifactId в project-file-verifier-maven-plugin, что не совсем логично. Тем не менее после такого костыля. Получаем рабочий тест.
public class SampleMavenPluginTest extends AbstractMojoTestCase {
protected void setUp() throws Exception {
String oldpath = System.getProperty("user.dir");
System.setProperty("basedir",oldpath+"/src/test/resources");
super.setUp ();
}
public void testReadMeExist() throws Exception {
File pom = getTestFile("pom.xml" );
MDCheckMojo mojo = (MDCheckMojo) lookupMojo( "md_check", pom );
mojo.execute();
}
public void testGitIgnoreExist() throws Exception {
File pom = getTestFile("pom.xml" );
GitIgnoreCheckMojo mojo = (GitIgnoreCheckMojo) lookupMojo( "gitignore_check", pom )
mojo.execute();
}
}

Модуль проверок verify-checks:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>chigarout.tutoral</groupId>
<artifactId>cdi-base-tutorial-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>verify-checks</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>chigarout.tutoral</groupId>
<artifactId>project-file-verifier-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-compat</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-component-annotations</artifactId>
<version>1.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Т.к все мои проверки делаю в тестовой среде, то в ресурсах лежит дополнительный pom.xml со следующим содержимым
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>chigarout.tutoral</groupId>
<artifactId>cdi-base-tutorial-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>project-file-verifier-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>chigarout.tutoral</groupId>
<artifactId>project-file-verifier-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-compat</artifactId>
<version>3.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>chigarout.tutoral</groupId>
<artifactId>project-file-verifier-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration></configuration>
<executions>
<execution>
<configuration></configuration>
<phase>package</phase>
<goals>
<goal>md_check</goal>
<goal>gitignore_check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Запуск mvn clean install вызовет goal md_check и gitignore_check.
Далее после сборки получаем ошибку
[ERROR] Failed to execute goal chigarout.tutoral:project-file-verifier-maven-plugin:1.0-SNAPSHOT:gitignore_check (default) on project verify-checks: Необходимо добавить gitignore в проект. -> [Help 1]
Плагин отработал штатно. Добавив .gitignore получим успешную сборку.
Отладка
Если посмотреть как выполняет отладку IntelJIdea то можно увидеть следующий вызов.
"...\java.exe" -Dmaven.multiModuleProjectDirectory=...\maven-plugin-example "-Dmaven.home=...\plugins\maven\lib\maven3" "-Dclassworlds.conf=...\plugins\maven\lib\maven3\bin\m2.conf" "-Dmaven.ext.class.path=...\plugins\maven\lib\maven-event-listener.jar" "-javaagent: ...\lib\idea_rt.jar=54536: ...\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath "...\plugins\maven\lib\maven3\boot\plexus-classworlds-2.6.0.jar" org.codehaus.classworlds.Launcher -Didea.version2019.3.5 clean
Вызывается Java с добавлением параметров для дебага и с указанием main класса. org.codehaus.classworlds.Launcher – это оболочка запускающая все команды в maven.
Кроме этого у maven есть mvnDebug.exe, который лежит на уровне с mvn.exe. Запуск выглядит аналогично как и с mvn.
mvnDebug clean install.
mvnDebug будет ждать пока мы включим debug соединение в idea и после этого запустит процесс сборки.

Summary
За то время когда сам пытался разбираться с вопросами maven плагинов находил только отдельные куски нужной информации на stackoverflow и habr. В статье постарался показать как можно безболезненно сделать свой mojo, применяя устоявшиеся практики CDI и тестирования. Ничего даже не мешает вести разработку через тестирование (TDD).
Приведенный пример опубликован на github .