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

Desarrollo de Software Implementando Gitflow

Desarrollo de Software Implementando Gitflow

Introducción El desarrollo de software requiere metodologías y flujos de trabajo que permitan un control eficiente del código fuente. Uno de los enfoques más utilizados para la gestión de versiones

Gitea: Cómo Montar tu Propio Servidor Git en Minutos

Gitea: Cómo Montar tu Propio Servidor Git en Minutos

A medida que los proyectos crecen y se diversifican, muchos desarrolladores empiezan a preguntarse si realmente necesitan depender de plataformas como GitHub o GitLab para gestionar su código. No es q

Explorando las Topologías de Infraestructura en la Nube: Desde el Monolito hasta la Inteligencia Artificial Distribuida

Explorando las Topologías de Infraestructura en la Nube: Desde el Monolito hasta la Inteligencia Artificial Distribuida

Introducción El crecimiento exponencial de las soluciones en la nube ha transformado la manera en que diseñamos, desplegamos y mantenemos nuestras aplicaciones. Ya no hablamos solo de servidores y

Observabilidad de Servidores y Contenedores Docker: Una Mirada Práctica con Prometheus, Grafana y cAdvisor

Observabilidad de Servidores y Contenedores Docker: Una Mirada Práctica con Prometheus, Grafana y cAdvisor

En el mundo de la infraestructura moderna, especialmente con la creciente adopción de contenedores y arquitecturas distribuidas, entender qué está sucediendo dentro de nuestros sistemas en tiempo real

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

En el ámbito del desarrollo de software, la elección de una estrategia de versionamiento eficiente y estable es fundamental para el éxito de un equipo. El Trunk Based Development (TBD) tradicional, do

Docker: La Revolución que Empaquetó tu Código

Docker: La Revolución que Empaquetó tu Código

En el dinámico mundo del desarrollo y la infraestructura de tecnologías de la información, la necesidad de empaquetar, distribuir y ejecutar aplicaciones de forma consistente ha llevado a la populariz

Exploración Profunda de Kubernetes: Componentes Clave y Principios Fundamentales

Exploración Profunda de Kubernetes: Componentes Clave y Principios Fundamentales

En el panorama tecnológico actual, caracterizado por arquitecturas de microservicios y despliegues en la nube, la gestión efectiva de aplicaciones contenedorizadas es un desafío crítico. Kubernetes su

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

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

Arquitectura de Confianza Cero en Docker Compose: Estrategias de Aislamiento y Segmentación de Redes

Arquitectura de Confianza Cero en Docker Compose: Estrategias de Aislamiento y Segmentación de Redes

Docker Compose ha democratizado el despliegue de aplicaciones gracias a una premisa seductora: la simplicidad. Con un archivo YAML y un comando, servicios complejos cobran vida, interconectados y list

El fin de la 'Jaula de Oro': recuperar el control de tu infraestructura

El fin de la 'Jaula de Oro': recuperar el control de tu infraestructura

Seguramente has pasado por esa etapa de "luna de miel" con las plataformas propietarias. Al principio, todo es idílico: lanzas una aplicación en Firebase en minutos, gestionas tus notas en Notion sin

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

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

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

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

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

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

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

En el desarrollo de aplicaciones reactivas modernas, especialmente bajo paradigmas como la Arquitectura Hexagonal, surgen desafíos que nos obligan a repensar cómo aplicamos conceptos transversales. Un

Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

En el desarrollo de APIs, el manejo de errores suele ser un ciudadano de segunda clase. Con frecuencia, nos conformamos con respuestas de error genéricas, inconsistentes o, en el peor de los casos, co

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

En entornos modernos de microservicios, la consistencia en las respuestas de una API es más que una cuestión de estética: es un factor crítico para la mantenibilidad, observabilidad y experiencia del

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

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