Пишем плагин для Maven

  • Tutorial
Есть у меня на некоторых maven-проектах профиль, с помощью которого производится копирование shared-библиотек с последующим перезапуском сервера Tomcat.
Maven profile
<profile>
	<id>deploy-deps</id>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-dependency-plugin</artifactId>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<useSubDirectoryPerScope>true</useSubDirectoryPerScope>
							<excludeGroupIds>исключаем некоторые группы, попадающие в war-архив</excludeGroupIds>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<executions>
					<execution>
						<id>05-stop-tomcat</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>${tomcat.dir.root}/bin/shutdown.sh</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
					<execution>
						<id>10-clean-shared-jars</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>rm</argument>
								<argument>-Rf</argument>
								<argument>${tomcat.dir.shared}/*.jar</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
					<execution>
						<id>15-upload-shared-jars</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-scp</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${project.build.directory}/dependency/compile/*.jar</argument>
								<argument>${ssh.user}@${ssh.host}:${tomcat.lib.shared}/</argument>
							</arguments>
							<executable>pscp</executable>
						</configuration>
					</execution>
					<execution>
						<id>20-start-tomcat</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>"${putty.key}"</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>bin/startup.sh</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</profile>

отходя в сторону, поведаю для чего сей профиль
В части проектов используется связка Nginx+Tomcat. Для данной связки реализовано следующее:
  1. Для всего статичного контента используется некий каталог за пределами webapps. В этот каталог «смотрит» Nginx и отдаёт по web-пути "/static/*"
  2. Все shared java-библиотеки (редко изменяемые) грузятся в каталог ${catalina.home}/shared, и в Tomcat в файле conf/catalina.properties настроена для этого переменная «shared.loader»
  3. Для каждого инстанса Tomcat создан свой системный пользователь
  4. Для доступа по SSH используются ключи и у каждого разработчика он свой

Соответственно, загрузка статичного контента и shared-библиотек это отдельные профили. Всё остальное собирается в war-архив и устанавливается через стандартный web-manager Tomcat-а.
А чтобы не плодить конфигураций, используется PAgent, в который уже и добавленые нужные нам private keys. Они же используются для подключения через Putty

Лежит себе профиль в pom.xml, не кусается вроде бы, даже пашет потихоньку на благо программера, но вот только есть в нём пара «минусов» — занимает много места при развёрнутом pom.xml да ещё и в новые проекты приходится вставлять.
И если от второго минуса можно избавиться написав шаблон в любимая_IDE или свой архетип наваять, то от первого минуса не так-то просто избавить.

Точно ли не так просто? может «обернём» этот профиль в виде плагина для Maven? Сказано, сделано.

Шаг 1. Создаём заготовку проекта для плагина maven


, в котором указываем тип сборки «maven-plugin». Так же нам понадобятся зависимости:
1) org.apache.maven.plugin-tools:maven-plugin-annotations для возможности указания Mojo классов не через JavaDoc, а с помощью аннотаций
2) org.twdata.maven:mojo-executor для возможности запуска других плагинов из нашего.
Пока зависимостей достаточно — пора приступать собственно к реализации самого Mojo класса.
commit

Шаг 2. Пишем Mojo-класс


Заготовка класса
@Mojo(name = "deploy-deps", defaultPhase = LifecyclePhase.PROCESS_SOURCES, threadSafe = true)
public class DeployDepsMojo extends AbstractMojo {

	@Component
	protected MavenProject                      mavenProject;
	@Component
	protected MavenSession                      mavenSession;
	@Component
	protected BuildPluginManager                pluginManager;
	protected MojoExecutor.ExecutionEnvironment _pluginEnv;

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		_pluginEnv = executionEnvironment(mavenProject, mavenSession, pluginManager);
	}
}


commit

Нам потребуется генерация mojo тегов из аннотаций (commit):
Заготовка класса
<build>
	<!-- ... -->
	<plugins>
		<plugin>
			<artifactId>maven-plugin-plugin</artifactId>
			<executions>
				<execution>
					<id>help-goal</id>
					<goals>
						<goal>helpmojo</goal>
					</goals>
				</execution>
				<execution>
					<id>mojo-descriptor</id>
					<goals>
						<goal>descriptor</goal>
					</goals>
				</execution>
			</executions>
			<configuration>
				<skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
			</configuration>
		</plugin>
	</plugins>
</build>


Добавляем копирование зависимостей
было
<plugin>
	<artifactId>maven-dependency-plugin</artifactId>
	<executions>
		<execution>
			<phase>package</phase>
			<goals>
				<goal>copy-dependencies</goal>
			</goals>
			<configuration>
				<useSubDirectoryPerScope>true</useSubDirectoryPerScope>
				<excludeGroupIds>исключаем некоторые группы, попадающие в war-архив</excludeGroupIds>
			</configuration>
		</execution>
	</executions>
</plugin>

стало
@Mojo(name = "deploy-deps",
      requiresDependencyResolution = ResolutionScope.TEST,
      defaultPhase = LifecyclePhase.PROCESS_SOURCES, threadSafe = true)
public class DeployDepsMojo extends AbstractMojo {

// ...

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		_pluginEnv = executionEnvironment(mavenProject, mavenSession, pluginManager);
		copyDependencies();
	}

	private void copyDependencies() throws MojoExecutionException {
		// TODO expects corrections https://github.com/TimMoore/mojo-executor/issues/18
		Plugin pluginDependency = plugin("org.apache.maven.plugins", "maven-dependency-plugin", "2.8");

		final Xpp3Dom cfg = configuration(element(name("useSubDirectoryPerScope"), "true"));

		executeMojo(pluginDependency, goal("copy-dependencies"), cfg, _pluginEnv);
	}
}

commit
Кратко:
  1. «requiresDependencyResolution = ResolutionScope.TEST» требуется для получения списка зависимостей — без этого плагин maven-dependency-plugin не произведёт их копирование
  2. «threadSafe = true» указывает на то, что данный Mojo можно запускать в отдельном потоке — он самодостаточен
  3. статический метод executeMojo позволяет выполнить любой goal для любого доступного плагина с описанием конфигурации окружения. В данном случае окружение остаётся тем же (переменная _pluginEnv)

Добавляем метод для остановки сервера Tomcat
было
<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>exec-maven-plugin</artifactId>
	<executions>
		<execution>
			<id>05-stop-tomcat</id>
			<phase>package</phase>
			<goals>
				<goal>exec</goal>
			</goals>
			<configuration>
				<arguments>
					<argument>-ssh</argument>
					<argument>-4</argument>
					<argument>-agent</argument>
					<argument>-i</argument>
					<argument>${putty.key}</argument>
					<argument>${ssh.user}@${ssh.host}</argument>
					<argument>${tomcat.dir.root}/bin/shutdown.sh</argument>
				</arguments>
				<executable>plink</executable>
			</configuration>
		</execution>
		<!-- ... -->
	</executions>
</plugin>

стало
public class DeployDepsMojo extends AbstractMojo {
	public static final String  PLG_EXEC_CFG_ARGUMENTS  = "arguments";
	public static final Xpp3Dom PLG_EXEC_CFG_EXEC_PLINK = element(name("executable"), "plink").toDom();
	public static final String  PLG_EXEC_GOAL_EXEC      = goal("exec");
	public static final String  PLG_EXEC_PROTOCOL_SSH   = "-ssh";
	
	// ...
	
	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		_pluginEnv = executionEnvironment(mavenProject, mavenSession, pluginManager);
		_pluginExec = plugin("org.codehaus.mojo", "exec-maven-plugin", "1.2.1");
		copyDependencies();
		tomcatShutdown();
	}
	
	private void tomcatShutdown() throws MojoExecutionException {
		Xpp3Dom cfg = getBaseConfigExec(PLG_EXEC_PROTOCOL_SSH);
		final Xpp3Dom arguments = cfg.getChild(PLG_EXEC_CFG_ARGUMENTS);
		arguments.addChild(element(name("argument"), "${ssh.user}@${ssh.host}").toDom());
		arguments.addChild(element(name("argument"), "bin/shutdown.sh").toDom());
		cfg.addChild(PLG_EXEC_CFG_EXEC_PLINK);

		executeMojo(_pluginExec, PLG_EXEC_GOAL_EXEC, cfg, _pluginEnv);
	}
	
	private Xpp3Dom getBaseConfigExec(String protocol) {
		final Element el0 = element(name("argument"), protocol);
		final Element el1 = element(name("argument"), "-4");
		final Element el2 = element(name("argument"), "-agent");
		final Element el3 = element(name("argument"), "-i");
		final Element el4 = element(name("argument"), "${putty.key}");
		return configuration(element(name(PLG_EXEC_CFG_ARGUMENTS), el0, el1, el2, el3, el4));
	}
}


Добавляем оставшиеся методы

По аналогии с предыдущим пунктом, добавляем методы для удалённой очистки каталога tomcat.lib.shared, копирования в него новых библиотек и последующего запуска сервера Tomcat.
commit

Шаг 3. Устанавливаем плагин в репозитарий и правим конфигурацию Maven-проекта


Установка плагина в локальный репозитарий выполняется простой командой «mvn clean install»

И правим конфигурацию проекта:
было
<profile>
	<id>deploy-deps</id>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-dependency-plugin</artifactId>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<useSubDirectoryPerScope>true</useSubDirectoryPerScope>
							<excludeGroupIds>исключаем некоторые группы, попадающие в war-архив</excludeGroupIds>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<executions>
					<execution>
						<id>05-stop-tomcat</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>${tomcat.dir.root}/bin/shutdown.sh</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
					<execution>
						<id>10-clean-shared-jars</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>rm</argument>
								<argument>-Rf</argument>
								<argument>${tomcat.dir.shared}/*.jar</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
					<execution>
						<id>15-upload-shared-jars</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-scp</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>${putty.key}</argument>
								<argument>${project.build.directory}/dependency/compile/*.jar</argument>
								<argument>${ssh.user}@${ssh.host}:${tomcat.lib.shared}/</argument>
							</arguments>
							<executable>pscp</executable>
						</configuration>
					</execution>
					<execution>
						<id>20-start-tomcat</id>
						<phase>package</phase>
						<goals>
							<goal>exec</goal>
						</goals>
						<configuration>
							<arguments>
								<argument>-ssh</argument>
								<argument>-4</argument>
								<argument>-agent</argument>
								<argument>-i</argument>
								<argument>"${putty.key}"</argument>
								<argument>${ssh.user}@${ssh.host}</argument>
								<argument>bin/startup.sh</argument>
							</arguments>
							<executable>plink</executable>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</profile>

стало
<profile>
	<id>deploy-deps</id>
	<build>
		<plugins>
			<plugin>
				<groupId>info.alenkov.tools.maven</groupId>
				<artifactId>tomcat7-ewar-plugin</artifactId>
				<executions>
					<execution>
						<phase>process-sources</phase>
						<goals>
							<goal>deploy-deps</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</profile>

Описание maven-проекта можно ещё упростить, если удалить профиль «deploy-deps» и вызывать goal напрямую:
mvn clean process-sources tomcat7-ewar:deploy-deps

На этом процесс улучшения читабельности maven-проекта и выноса часто используемого «наружу» закончен (заключительный commit для этого поста) — maven-проект сократился на 106 строк. Впереди ещё оптимизация кода, добавление параметров и многое другое, но это уже совсем другая история, которую когда-нибудь поведаю хабражителям.

UPD: Опубликовал плагин в своём репозитарии. Планирую развивать его дальше, потому, если есть предложения/замечания, то принимаю их на github.
UPD: Обновил плагин до версии 1.1
UPD: Обновил плагин до версии 2.0 — избавился от использования Putty
  • +14
  • 10,8k
  • 7
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 7
  • 0
    Вдруг кому пригодится, опубликовал плагин во внешний репозитарий
    • –1
      Ну вы блин даете я когда за завтраком в ленте ридереа увидел код maven проекта чуть не это… рвотный рефлекс так сказать.
      • 0
        который из — исходный или конечный?
        И отчего с желудком плохо стало, что не захотел усвоить и решил вертать взад?
        • 0
          Не я про первый, во многих ридерах скрытие теги не работают, так что весь этот код на три экрана и далее виден.
          • 0
            ясно. т.е. претензия была не к коду, а к тому, что habr сворачивает spoiler с помощью JS
      • 0
        UPD: обновил плагин до версии 1.1
        • 0
          UPD: обновил плагин до версии 2.0

          +) избавился от использования Putty
          -) почему-то Wagon не хочет использовать OpenSSH ключи — работает только связка логин/пароль.
          -) теперь команды нельзя писать с применением "~" — Wagon их не понимает.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.