Type something to search...
Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

En el artículo anterior, alcanzamos un hito crucial: construimos un plugin binario funcional en Java, completo con su propia configuración y tarea. Nuestro plugin “saludador” demostró que dominamos la estructura, pero su utilidad era meramente académica. Hoy, transformamos ese esqueleto en una herramienta de productividad real. Vamos a convertir nuestro plugin en un generador de proyectos.

El objetivo de este capítulo es tomar la configuración del usuario (como el nombre del proyecto y el paquete base) y, con una sola tarea de Gradle, materializar un esqueleto de proyecto Java completamente funcional. Para lograr esto, dejaremos atrás la simple impresión en consola y nos adentraremos en dos áreas clave: la manipulación del sistema de archivos y, lo más importante, el uso de un motor de plantillas. Presentamos a nuestro nuevo mejor amigo: Apache FreeMarker.


1. La Herramienta Adecuada: ¿Por qué un Motor de Plantillas?

Podríamos generar archivos concatenando String en Java, pero eso sería increíblemente frágil, difícil de leer y casi imposible de mantener. Un motor de plantillas separa el “qué” (la estructura y el contenido de un archivo) del “cómo” (los datos específicos que lo rellenan).

Elegimos FreeMarker por varias razones:

  • Madurez y Potencia: Es una biblioteca robusta y probada en batalla.
  • Diseñado para la Generación de Texto: A diferencia de otros motores más enfocados en HTML, FreeMarker es excelente para generar cualquier tipo de archivo de texto: .java, .xml, .properties, o nuestro build.gradle.
  • Lógica en Plantillas: Permite usar condicionales, bucles y otras lógicas directamente en los archivos de plantilla, algo que será vital cuando generemos código más complejo.

2. Integrando FreeMarker en Nuestro Plugin

El primer paso es hacer que nuestro plugin conozca FreeMarker.

a) Añadir la Dependencia

Abre el archivo build.gradle de nuestro plugin (nuestro-generador/plugin/build.gradle) y añade la dependencia de FreeMarker.

// nuestro-generador/plugin/build.gradle

plugins {
    id 'java-gradle-plugin'
}

repositories {
    mavenCentral()
}

// AÑADIMOS ESTE BLOQUE
dependencies {
    // Añadimos la implementación de FreeMarker
    implementation 'org.freemarker:freemarker:2.3.32'
}

gradlePlugin {
    plugins {
        // Renombraremos el plugin para que refleje su nuevo propósito
        projectGeneratorPlugin {
            id = 'com.miempresa.project-generator'
            implementationClass = 'com.miempresa.ProjectGeneratorPlugin'
        }
    }
}

b) Creación de las Plantillas

Las plantillas son el corazón de nuestro generador. Por convención, las colocaremos en src/main/resources/templates dentro de nuestro proyecto de plugin. Gradle las empaquetará automáticamente en el .jar final, haciéndolas accesibles desde el classpath.

Crea el directorio nuestro-generador/plugin/src/main/resources/templates/. Ahora, creemos algunas plantillas básicas. Nota el uso de la sintaxis ${...} para las variables.

templates/build.gradle.ftl:

plugins {
    id 'java'
    id 'application'
}

group = '${basePackage}'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

application {
    mainClass = '${basePackage}.Application'
}

templates/Application.java.ftl:

package ${basePackage};

public class Application {
    public static void main(String[] args) {
        System.out.println("¡Hola desde el proyecto '${projectName}'!");
    }
}

3. Expandiendo la Configuración

Nuestra extensión actual es demasiado simple. Necesitamos que el usuario nos proporcione la información necesaria para la generación. Vamos a renombrar y expandir nuestra clase de extensión.

  1. Renombra GreeterExtension.java a GeneratorExtension.java.
  2. Añade las nuevas propiedades.

plugin/src/main/java/com/miempresa/GeneratorExtension.java:

package com.miempresa;

public class GeneratorExtension {
    private String projectName = "mi-proyecto-generado";
    private String basePackage = "com.ejemplo.proyecto";

    public String getProjectName() {
        return projectName;
    }

    public void setProjectName(String projectName) {
        this.projectName = projectName;
    }

    public String getBasePackage() {
        return basePackage;
    }

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }
}

4. La Tarea de Generación: El Corazón de la Lógica

Aquí es donde ocurre la magia. Reemplazaremos nuestra antigua tarea greet por una nueva y potente tarea generateProject.

  1. Renombra GreetingPlugin.java a ProjectGeneratorPlugin.java.
  2. Actualiza la lógica para que use FreeMarker y cree los archivos.

plugin/src/main/java/com/miempresa/ProjectGeneratorPlugin.java:

package com.miempresa;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.gradle.api.Plugin;
import org.gradle.api.Project;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

public class ProjectGeneratorPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        final GeneratorExtension extension = project.getExtensions().create("generator", GeneratorExtension.class);

        project.getTasks().register("generateProject", task -> {
            task.setGroup("Generacion");
            task.setDescription("Genera un nuevo esqueleto de proyecto Java.");
            task.doLast(t -> {
                try {
                    generate(project, extension);
                } catch (IOException | TemplateException e) {
                    // Lanzamos una excepción para que el build falle si algo va mal
                    throw new RuntimeException("Fallo al generar el proyecto", e);
                }
            });
        });
    }

    private void generate(Project project, GeneratorExtension extension) throws IOException, TemplateException {
        String projectName = extension.getProjectName();
        String basePackage = extension.getBasePackage();
        project.getLogger().lifecycle("Iniciando generación del proyecto: {}", projectName);

        // 1. Configurar FreeMarker
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(ProjectGeneratorPlugin.class, "/templates");
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

        // 2. Crear el modelo de datos para las plantillas
        Map<String, Object> model = new HashMap<>();
        model.put("projectName", projectName);
        model.put("basePackage", basePackage);

        // 3. Crear directorios
        File projectDir = new File(project.getProjectDir(), projectName);
        File packageDir = new File(projectDir, "src/main/java/" + basePackage.replace('.', '/'));
        if (!packageDir.mkdirs()) {
            throw new IOException("No se pudieron crear los directorios base.");
        }
        new File(projectDir, "src/test/java").mkdirs();

        // 4. Procesar plantillas y generar archivos
        generateFile(cfg, model, "build.gradle.ftl", new File(projectDir, "build.gradle"));
        generateFile(cfg, model, "Application.java.ftl", new File(packageDir, "Application.java"));
        
        project.getLogger().lifecycle("Proyecto '{}' generado exitosamente en: {}", projectName, projectDir.getAbsolutePath());
    }

    private void generateFile(Configuration cfg, Map<String, Object> model, String templateName, File output) throws IOException, TemplateException {
        Template template = cfg.getTemplate(templateName);
        try (Writer writer = new FileWriter(output)) {
            template.process(model, writer);
        }
    }
}

5. Probándolo Todo Junto

Ya estamos listos para la prueba final.

  1. Actualiza el build.gradle del proyecto de prueba para usar el nuevo ID del plugin y la nueva extensión generator.

    nuestro-generador/proyecto-de-prueba/build.gradle:

    plugins {
        // Usamos el nuevo ID del plugin
        id 'com.miempresa.project-generator'
    }
    
    // Usamos la nueva extensión 'generator'
    generator {
        projectName = 'mi-primera-app'
        basePackage = 'com.acme.app'
    }
  2. Ejecuta la tarea de generación desde el directorio raíz (nuestro-generador/).

    ./gradlew :proyecto-de-prueba:generateProject

Si todo fue correcto, verás los mensajes de log en tu consola y, lo más importante, ¡un nuevo directorio llamado mi-primera-app habrá aparecido! Dentro, encontrarás un proyecto Gradle funcional, listo para ser importado en tu IDE y ejecutado.

nuestro-generador/
├── mi-primera-app/
   ├── build.gradle
   └── src/main/java/com/acme/app/Application.java
├── plugin/
└── ...

Conclusión y Siguientes Pasos

¡Hemos dado un salto cuántico! Nuestro plugin ha pasado de ser un juguete a una herramienta de productividad. Ahora puede tomar una configuración declarativa y generar un proyecto Java completo y funcional. Hemos aprendido a integrar una biblioteca de terceros, a gestionar y procesar archivos de plantillas desde el classpath y a escribir una lógica de tarea compleja que interactúa con el sistema de archivos.

Nuestro generador es potente, pero su estructura es estática. Siempre genera el mismo tipo de proyecto. ¿Y si pudiéramos llevarlo más allá? ¿Y si pudiéramos describir una entidad de negocio —como “Producto” o “Cliente”— y el plugin generara automáticamente todo el código CRUD (Crear, Leer, Actualizar, Borrar) para ella, siguiendo las mejores prácticas de la industria?

En el próximo artículo, nos adentraremos en el fascinante mundo del Domain-Driven Design (DDD) y la arquitectura hexagonal. Haremos que nuestro plugin lea una definición de modelo y genere dinámicamente todas las capas necesarias, desde la entidad de dominio hasta el controlador REST, llevando nuestra capacidad de automatización a un nivel completamente nuevo.

Related Posts

Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

En el vertiginoso universo del desarrollo de software, la eficiencia no es un lujo, es una necesidad. Dedicar tiempo a tareas repetitivas como compilar código, ejecutar pruebas, empaquetar la aplicaci

Leer más
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

En nuestro artículo anterior, desmitificamos Gradle y sentamos las bases para entender su funcionamiento. Aprendimos a crear proyectos, ejecutar tareas y comprendimos el rol fundamental de los plugins

Leer más
Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Introducción En el mundo de Java, trabajar con colecciones de datos solía ser sinónimo de bucles interminables, condicionales anidados y código repetitivo. Pero con la llegada de Java Streams

Leer más
Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

El Costo Oculto de Traer Demasiada Información En el desarrollo de aplicaciones que interactúan con bases de datos, una tarea fundamental es la recuperación de datos. Al usar Object-Relational Map

Leer más
Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

En el desarrollo de aplicaciones Java, el manejo de respuestas a solicitudes —especialmente aquellas que involucran operaciones asincrónicas, procesamiento de datos o comunicación con servicios extern

Leer más
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo t

Leer más
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

En el desarrollo de software moderno, escribir código que funciona perfectamente en escenarios ideales es solo el primer paso. La verdadera fortaleza de un sistema se manifiesta cuando debe enfrentar

Leer más
El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

En el vertiginoso universo del desarrollo de software, donde la seguridad es un pilar innegociable y la agilidad es la moneda de cambio, nos enfrentamos a desafíos que van más allá de la simple lógica

Leer más
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

En el universo del desarrollo de software empresarial, nos enfrentamos a un dilema constante: cómo manejar información transversal —ese rastro de datos vitales como IDs de correlación, información del

Leer más
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC Imagina a un desarrollador frontend consumiendo tu API. En un endpoint, recibe un objeto JSON. En otro, una simple lista. S

Leer más