Plugins: Maven vs Gradle

¿Qué es un plugin?

Un plugin es aquella aplicación que, en un programa informático, añade una funcionalidad adicional o una nueva característica al software. En este caso puede nombrarse al plugin como un complemento o extensión de la herramienta Maven o Gradle.

¿Cuál es su utilidad?

En las herramientas de construcción de proyectos como son Maven o Gradle (además de ser gestores de dependencias) podemos extender o añadir funcionalidades en las diferentes fases de construcción, lo que nos permite realizar acciones como ejecutar tests, validar código, minificar archivos, compilar, empaquetar y arrancar la aplicación en un servidor… Estas acciones de validación, preprocesamiento o postprocesamiento que podemos hacer externamente desde scripts quedan integrados con la herramienta de construcción y por tanto tenemos un único punto desde donde ejecutar todas estas tareas. Además se gestionan como si de una dependencia se tratase, lo que nos permite importar y ejecutar los plugins que consideremos desde nuestro proyecto usando el gestor de dependencias, como por ejemplo, Nexus.

¿Cómo los ejecutamos?

Cuando en Maven ejecutamos mvn clean estamos ejecutando el plugin maven-clean-plugin que incorpora nativamente la herramienta. Este plugin tiene un único goal que son las acciones posibles a ejecutar, y en este caso ese goal es ‘clean’ que tiene por objetivo eliminar el directorio target de nuestros módulos. Además del goal, los plugins tienen una etiqueta executions que permite definir las diferentes ejecuciones del plugin, por ejemplo indicando diferentes fases donde ejecutarlo. Por otra parte, el plugin puede configurarse con variables parametrizadas asignándose los valores en cada ejecución o de manera global.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-clean-plugin</artifactId>
    <version>2.5</version>
    <executions>
        <execution>
            <phase>clean</phase>
            <goals>
                <goal>clean</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Para ejecutar un plugin solo tenemos que invocarlo desde el comando mvn indicando el plugin y goal o goals (ya que se pueden lanzar múltiples goals) a ejecutar. Siguiendo con el ejemplo anterior podemos ejecutar el siguiente comando:

mvn org.apache.maven.plugins:maven-clean-plugin:2.5:clean

Al ser un plugin propio de Maven (sigue la convención de nomenclatura oficial maven-name-plugin y pertenece al paquete org.apache.maven.plugins) podemos invocarlo solo indicando el nombre (name indicado entre las palabras maven y plugin), quedando la ejecución anterior como:

mvn clean:clean

Para saber los goals disponibles en un plugin podemos ejecutar otro plugin, propio de Maven, llamado help que con el goal describe y el parámetro plugin nos muestra la descripción del plugin:

mvn help:describe -Dplugin=clean

...

This plugin has 2 goals:

clean:clean
  Description: Goal which cleans the build.
    
    This attempts to clean a project's working directory of the files that were
    generated at build-time. By default, it discovers and deletes the
    directories configured in project.build.directory,
    project.build.outputDirectory, project.build.testOutputDirectory, and
    project.reporting.outputDirectory.
    
    
    Files outside the default may also be included in the deletion by
    configuring the filesets tag.

clean:help
  Description: Display help information on maven-clean-plugin.
    Call mvn clean:help -Ddetail=true -Dgoal=<goal-name> to display parameter
    details.
    
...

En Gradle ocurre algo similar. Tenemos por una parte la declaración del plugin y la declaración de la aplicación o ejecución del mismo. En el caso siguiente tenemos la declaración de un plugin que no incluye Gradle por defecto como es el desarrollado por SourceClear y por tanto primero, se define como dependencia y luego se aplica; por otro lado está la aplicación del plugin idea que incluye la herramienta.

plugins {
    id 'com.srcclr.gradle' version '2.0.1'
}

apply plugin: 'com.srcclr.gradle'
apply plugin: 'idea'

Cada plugin define una tareas (los goals de manera homóloga a Maven) y por tanto, la aplicación de un plugin implica la posibilidad de ejecutar estas tareas. Si queremos ejecutarlas en una fase concreta, o hacerla dependiente de la ejecución de otra tarea la podremos definir en el archivo build.gradle como sigue:

clean.dependsOn(cleanIdea)

clean.finalizedBy(cleanIdea)

Y podemos ejecutar cualquier tarea ejecutando el comando gradle seguido del nombre de la tarea:

gradle idea

¿Cuándo se ejecutan?

En Maven, los plugins se ejecutan en las diferentes fases de construcción del proyecto agrupadas en tres ciclos de vida diferentes: clean, default y site. Sin entrar en más detalle (véase Maven Lifecycle Reference) tenemos fases dentro del ciclo de vida default como son process-resources (momento en el cual se copian los recursos en el directorio target), compile, test, package (empaquetado del código compilado en formato JAR, por ejemplo) o verify (comprobación de que los distintos controles como test unitarios, de integración o cuaquier otra comprobación se completa correctamente cuando se procesa el empaquetado cumpliendo así los criterios de calidad). La etiqueta que nos permite definir la fase de ejcución es phase y estará definida dentro de la etiqueta de ejecución, en las múltiples ejecuciones posibles a definir.

<executions>
    <execution>
        <phase>process-resources</phase>
        <goals>
            <goal>compress</goal>
        </goals>
    </execution>
</executions>

En Gradle solo hay tres fases, de inicialización, de configuración y de ejecución. Sin embargo aquí se introducen las tareas o Tasks las cuales ejecutan acciones. Estas tareas pueden ser ejecutadas según el orden deseado e introduciendo dependencias de orden entre ellas. Los plugins introducen nuevas tareas que son ejecutadas en el momento de ejecución y nuestras acciones pueden asociarse a la ejecución posterior o anterior a estas tareas.

compileJava.finalizedBy(combineJs, minifyJs)

¿Cómo los configuramos?

Los plugins aceptan parámetros y por tanto estos deben ser definidos, configurando así la ejecución del plugin. En el caso de Maven, tenemos la etiqueta configuration que nos permite declarar los valores con los que queremos inicializar los parámetros que expone el plugin. Si lo hacemos a nivel de la etiqueta execution estos valores solo se inicializarán cuando se ejecute esta ejecución, sin embargo, también se nos permite declarar esta configuración en el nivel anterior, lo que hará que se utilicen de manera global estos valores.

<configuration>
    <inputDir>${basedir}/src/main/webapp/</inputDir>
    <output>${build.outputDirectory}</output>
</configuration>

En Gradle tenemos también una manera de configurar el plugin y por tanto definir los valores de los parámetros que expone dicho plugin.

idea {
    module {
        iml {
            generateTo = file('secret-modules-folder')
        }
    }
}

Desarrollando plugins con Java

Maven

Lo primero que tenemos que definir en el pom.xml de nuestro proyecto plugin (el cual deberá llamarse name-mave-plugin para cumplir con la nomenclatura propuesta por Maven y diferenciar de los plugins oficiales de Maven que serán maven-_name_-plugin) es el tipo de empaquetado que hará Maven que será de tipo maven-plugin:

<groupId>groupId</groupId>
<artifactId>myname-maven-plugin</artifactId>
<version>1.0.0</version>
<packaging>maven-plugin</packaging>

Por otra parte es necesario importar las siguientes librerías de Maven:

<dependencies>
    <dependency>
        <groupId>org.apache.maven</groupId>
        <artifactId>maven-plugin-api</artifactId>
        <version>3.0</version>
    </dependency>

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

Finalmente cumpliremos con la API de los plugins de Maven implementando una clase Mojo (Maven plain Old Java Object) la cual extenderá de AbstractMojo e implementará el método execute. Para definir la clase como Mojo deberemos indicar la anotación sobre el nombre de la clase, indicando el nombre del Mojo (esto será el nombre del goal) y la fase por defecto en la que se ejecutará (si no indicamos ninguna fase el plugin no se ejecutará cuando por ejemplo se ejecute un build y solo se ejecutará si lo lanzamos manualmente).

@Mojo(name = "goalName", defaultPhase = LifecyclePhase.COMPILE)
public class MyMojo extends AbstractMojo {
    @Parameter(property = "directory", defaultValue = "src/main/webapp")
    private String directory;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        //Llamada a la clase que ejecuta las acciones de nuestro plugin, 
        //donde podemos pasarle el parámetro de configuración directory
        //que puede venir con un valor o por defecto ser src/main/webapp
    }
}

Una vez ejecutemos mvn install ya tendremos disponible el plugin en el repositorio local de Maven. Por lo que podremos ejecutar el siguiente comando (podemos obviar la versión ya que Maven utilizará la última existente):

mvn groupId:myname-maven-plugin:1.0.0:goalName

Gradle

Para crear un plugin de Gradle debemos definir en el archivo de configuración build.gradle la dependencia a la api de Gradle:

dependencies {
    compile gradleApi()
}

Una vez definida la dependencia ya podremos crear la clase plugin que será la que defina las diferentes tareas que expone nuestro plugin:

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getExtensions().create("myplugin", MyExtension.class);
        project.getTasks().create("myplugin", MyTask.class);
    }
}

En la clase anterior estamos diciendo que nuestro plugin tendrá una tarea llamada ‘myplugin’ y una extensión (no es más que una configuración) que se definirá bajo la etiqueta ‘myplugin’ (por convencionalismos llamaremos a la configuración de la tarea con el mismo nombre que la tarea).

Necesitamos entonces definir la clase extensión que recibirá los valores iniciales de los parámetros configurables:

public class MyExtension {
    private String directory = "src/main/webapp";

    public String getDirectory() {
        return directory;
    }

    public void setDirectory(String directory) {
        this.directory = directory;
    }
}

Finalmente debemos definir nuestra tarea en la clase MyTask que hará uso de la configuración leída por la extensión anterior.

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

public class JugCsTask extends DefaultTask {
    @TaskAction
    public void myTaskAction() {
        MyExtension extension = getProject().getExtensions().findByType/MyExtension.class);
        if (extension == null) {
            extension = new MyExtension();
        }

        String directory = extension.getDirectory();

        //Llamada a la clase que ejecuta las acciones de nuestro plugin, 
        //donde podemos pasarle el parámetro de configuración directory
        //que puede venir con un valor o por defecto ser src/main/webapp
    }
}

Una vez tenemos todas las clases anteriores definidas solo nos queda indicar a gradle que esto es un plugin y la clase que define al plugin; concretamente la clase que define las tareas y extensiones de nuestro plugin, que en este caso es MyPlugin.java. Esto lo haremos creando un directorio gradle-plugins bajo el directorio src/main/resources/META-INF donde deberemos especificar en el archivo de properties (el nombre de este archivo debe ser el nombre completo con el que se llamará al plugin) la clase inicial donde se definen las tareas:

src/
  main/
    resoruces/
      META-INF/
        gradle-plugins/
          groupId.myplugin.properties

Este archivo contendrá la siguiente línea:

implementation-class=MyPlugin

Realizando la instalación en el repositorio local de MAven ejecutando el comando install del plugin de maven tendremos la posibilidad de importarlo, aplicarlo y usarlo desde otros proyectos. Para esto, en el build.gradle debemos tener el plugin de Maven y Java:

group 'groupId'
version '1.0.0'

apply plugin: 'java'
apply plugin: 'maven'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile gradleApi()
}

Y desde cualquier proyecto podremos importar este plugin de la siguiente manera:

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
        classpath("groupId:myplugin:1.0.0")
    }
}

apply plugin: 'groupId.myplugin' //Nombre del archivo bajo el directorio META-INF/gradle-plugins
Escrito el 14/03/2017