Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker
- Mauricio ECR
- Devops
- 25 Jun, 2025
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 nuestrobuild.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.
- Renombra
GreeterExtension.javaaGeneratorExtension.java. - 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.
- Renombra
GreetingPlugin.javaaProjectGeneratorPlugin.java. - 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.
-
Actualiza el
build.gradledel proyecto de prueba para usar el nuevo ID del plugin y la nueva extensióngenerator.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' } -
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.