Tags (71)
- Agile
- Alta disponibilidad
- Alternativas cloud
- Aop
- Arquitectura
- Arquitectura distribuida
- Automatizacion
- Azure devops
- Base de datos
- Buenas practicas
- Cloud
- Colas
- Competing consumers
- Convenciones
- Copilot
- Diseno
- Docker
- Docker compose
- Documentacion
- Eda
- Equipos
- Escalabilidad
- Flujo de negocio
- Flujo de trabajo
- Flyway
- Git
- Gradle
- Herramientas digitales
- Ia
- Iam
- Infraestructura
- Java
- Jerarquia tecnica
- Jpa
- Jsonb
- Kafka
- Kubernetes
- Liderazgo en software
- Lineamientos
- Log
- Logging
- Microservicios
- Mongodb
- Monitoreo
- Nosql
- Observabilidad
- Open source
- Plugins
- Postgresql
- Privacidad
- Programacion funcional
- Programacion reactiva
- Rabbitmq
- Rotacion de talento
- Saga
- Scrum
- Security
- Seguridad
- Self hosting
- Sistemas legados
- Spring boot
- Spring mvc
- Sql
- Streams
- Threadlocal
- Trazabilidad
- Versionado
- Web
- Webflux
- Websockets
- Zero trust
Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado
- Mauricio ECR
- Snippets
- 30 Jun, 2025
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
Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado
- Mauricio ECR
- Snippets
- 30 Jun, 2025
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 externos— requiere estructuras robustas, claras y reutilizables. Aunque Optional<T> de Java es útil para representar valores potencialmente ausentes, su semántica está limitada a la presencia o ausencia de un valor, sin ofrecer un contexto de estado (como éxito, error, pendiente) ni información adicional como mensajes de error.
Este artículo tiene como objetivo presentar una implementación técnica detallada de una clase ResponseWrapper<T> en Java. Esta clase encapsula un valor de respuesta, un estado (Status) y una lista de errores, replicando y extendiendo las capacidades de Optional<T>. A través de esta herramienta, se busca proveer una estructura genérica que mejore la expresividad, manejabilidad y trazabilidad de las respuestas dentro de aplicaciones Java, especialmente en contextos de desarrollo backend, servicios REST, o flujos de validación de datos.
El contenido está orientado a desarrolladores de software, arquitectos de aplicaciones y diseñadores de APIs que deseen integrar una solución flexible y extensible para el manejo de respuestas estructuradas.
Implementación Técnica de ResponseWrapper
Motivación y Limitaciones de Optional<T>
El uso de Optional<T> es común para evitar null y sus efectos colaterales. Sin embargo, presenta limitaciones:
- No permite almacenar información contextual sobre por qué el valor está ausente.
- No diferencia entre un valor ausente por error y uno ausente por diseño (por ejemplo, un valor aún no calculado).
- No soporta transporte de metadatos como mensajes de error, códigos de estado, o indicadores de transición.
Por tanto, es útil extender su concepto en una clase personalizada que mantenga las siguientes características:
- Presencia opcional de un valor
- Estado de la operación (
SUCCESS,FAILURE,PENDING) - Listado de errores informativos o técnicos
- Soporte para operaciones tipo
map,flatMap,orElseyifPresent
Estructura de Código
La clase ResponseWrapper y el enum Status pueden definirse como sigue:
Archivo Status.java
public enum Status {
SUCCESS,
FAILURE,
PENDING
}
Archivo ResponseWrapper.java
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class ResponseWrapper<T> {
private final T value;
private final Status status;
private final List<String> errors;
private ResponseWrapper(T value, Status status, List<String> errors) {
this.value = value;
this.status = status;
this.errors = errors != null ? new ArrayList<>(errors) : new ArrayList<>();
}
public static <T> ResponseWrapper<T> of(T value) {
return new ResponseWrapper<>(value, Status.SUCCESS, null);
}
public static <T> ResponseWrapper<T> empty() {
return new ResponseWrapper<>(null, Status.PENDING, null);
}
public static <T> ResponseWrapper<T> ofError(List<String> errors) {
return new ResponseWrapper<>(null, Status.FAILURE, errors);
}
public boolean isPresent() {
return value != null;
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
public void ifPresent(Consumer<? super T> consumer) {
if (value != null) {
consumer.accept(value);
}
}
public <U> ResponseWrapper<U> map(Function<? super T, ? extends U> mapper) {
if (!isPresent()) {
return empty();
}
return ResponseWrapper.of(mapper.apply(value));
}
public <U> ResponseWrapper<U> flatMap(Function<? super T, ResponseWrapper<U>> mapper) {
if (!isPresent()) {
return empty();
}
return mapper.apply(value);
}
public Status getStatus() {
return status;
}
public List<String> getErrors() {
return new ArrayList<>(errors);
}
public boolean isSuccess() {
return status == Status.SUCCESS;
}
public boolean isFailure() {
return status == Status.FAILURE;
}
public boolean isPending() {
return status == Status.PENDING;
}
@Override
public String toString() {
return value != null
? String.format("ResponseWrapper[%s, %s, %s]", value, status, errors)
: String.format("ResponseWrapper.empty[%s, %s]", status, errors);
}
}
Aplicaciones Prácticas
Caso de Uso 1: Servicio RESTful
En una API REST, ResponseWrapper puede encapsular una respuesta sin tener que lanzar excepciones para errores esperados:
@GetMapping("/usuarios/{id}")
public ResponseEntity<ResponseWrapper<Usuario>> obtenerUsuario(@PathVariable Long id) {
Optional<Usuario> usuario = usuarioService.buscarPorId(id);
if (usuario.isPresent()) {
return ResponseEntity.ok(ResponseWrapper.of(usuario.get()));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ResponseWrapper.ofError(List.of("Usuario no encontrado")));
}
}
Caso de Uso 2: Validación de datos
public ResponseWrapper<String> validarEntrada(String input) {
if (input == null || input.isBlank()) {
return ResponseWrapper.ofError(List.of("Entrada vacía o nula"));
}
return ResponseWrapper.of(input.trim());
}
Caso de Uso 3: Procesamiento Encadenado
ResponseWrapper<String> resultado = validarEntrada(" hola ")
.map(String::toUpperCase)
.flatMap(this::procesarTexto);
if (resultado.isFailure()) {
log.warn("Errores: {}", resultado.getErrors());
}
Conclusiones
El patrón ResponseWrapper representa una evolución práctica del uso de Optional<T> en Java, permitiendo no solo modelar valores opcionales, sino también asociar metainformación esencial como estado y errores. Esta estructura permite escribir código más legible, declarativo y resiliente ante fallos predecibles.
Su versatilidad lo hace útil en diversos escenarios: servicios web, validaciones, transformaciones funcionales y pruebas. Además, su diseño extensible admite futuras adaptaciones como códigos de error tipados, trazabilidad de auditoría o integración con frameworks de serialización JSON.
Referencias y Recursos Adicionales
Auditoría: Un Enfoque Auto-Declarativo
- Mauricio ECR
- Auditoria
- 28 Jun, 2025
La Necesidad de Logs que Cuentan Historias En el ciclo de vida de cualquier plataforma digital, llega un momento crucial en el que responder a la pregunta "¿Quién hizo qué y cuándo?" deja de ser u
Auditoría: Un Enfoque Auto-Declarativo
- Mauricio ECR
- Auditoria
- 28 Jun, 2025
La Necesidad de Logs que Cuentan Historias
En el ciclo de vida de cualquier plataforma digital, llega un momento crucial en el que responder a la pregunta "¿Quién hizo qué y cuándo?" deja de ser una opción y se convierte en una necesidad. Los logs de auditoría son la crónica de nuestra aplicación, un registro inmutable de las acciones significativas.
Esta guía presenta la versión 0.1 de nuestros lineamientos de auditoría, un enfoque diseñado con un objetivo primordial: la legibilidad inmediata. Partimos de un principio fundamental que guiará todas las decisiones en esta fase inicial:
Principio Fundamental V0.1: Cada entrada de log debe ser una historia completa y comprensible por sí misma, sin requerir consultas a sistemas externos o la decodificación de identificadores (
ID) opacos.
El alcance de esta versión se centra deliberadamente en registrar acciones realizadas por usuarios autenticados (logueados). Los eventos anónimos o puramente sistémicos (ej. "base de datos iniciada") quedan fuera de este marco inicial para mantener la simplicidad y el enfoque.
2. ⚠️ El Compromiso Central de la V0.1: Legibilidad vs. Exposición de Datos
Para alcanzar la meta de logs auto-declarativos, la V0.1 adopta una postura de diseño que representa un compromiso consciente. Nos desviamos temporalmente de la práctica de seguridad estándar de minimizar la exposición de datos para maximizar la utilidad inmediata.
Al implementar esta guía, la organización acepta y gestiona conscientemente el siguiente riesgo:
Aceptación de Riesgo: Para facilitar la identificación inequívoca del actor sin consultas externas, se registrará información de identificación personal (PII), como el nombre completo y el correo electrónico del usuario, en texto plano dentro de los logs de auditoría. Esta decisión aumenta el riesgo inherente en caso de una brecha de seguridad que exponga dicho sistema de logs.
En consecuencia, el acceso a los repositorios de logs de auditoría debe ser considerado un privilegio extremadamente elevado. Dicho acceso debe estar protegido por múltiples capas de seguridad, ser rigurosamente controlado mediante listas de acceso explícitas y ser monitoreado de forma continua.
3. Anatomía del Evento de Auditoría (UserAuditEvent)
Cada evento de auditoría generado debe ser una instancia del siguiente modelo JSON. La estructura está optimizada para la descriptividad humana, favoreciendo campos claros sobre códigos crípticos.
{
"eventTimestamp": "2025-06-27T17:22:35.123Z",
"user": {
"email": "[email protected]",
"name": "Carlos Pérez",
"role": "Administrador"
},
"action": {
"type": "DOCUMENT_DOWNLOADED",
"description": "El usuario descargó el reporte financiero correspondiente al Q2 2025."
},
"resource": {
"type": "Reporte Financiero",
"name": "Reporte Financiero Q2 2025.pdf"
},
"context": {
"clientIp": "203.0.113.55",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"details": {
"pageUrl": "/dashboard/reports",
"downloadId": "b1b2a38c-1e24-4a2b-8c6c-a9e9e1f2f3f4"
}
}
}
3.1. Desglose de Campos
| Campo | Descripción y Contenido Esperado | Razón de Ser |
|---|---|---|
eventTimestamp |
Fecha y hora UTC del evento en formato ISO 8601. Obligatorio. | Establece una línea de tiempo universal e inequívoca para todos los eventos. |
user |
Objeto que describe al actor. Prohibido el uso de IDs internos en esta V0.1. | Identifica al "quién" de la historia de forma inmediata. |
user.email |
Correo electrónico principal del usuario que realizó la acción. | Es el identificador humano único más común en sistemas digitales. |
user.name |
Nombre completo del usuario para una rápida identificación visual. | Aporta contexto humano y reduce la carga cognitiva del revisor. |
user.role |
El rol o perfil del usuario en el instante de la acción (ej. "Cliente", "Admin"). | Ayuda a evaluar si la acción era apropiada para el nivel de permisos del actor. |
action |
Objeto que describe la acción realizada, el "qué" de la historia. | Es el verbo de la narrativa del log. |
action.type |
Clasificador de la acción según la taxonomía definida. Obligatorio. | Permite la búsqueda, el filtrado y la creación de alertas automatizadas. |
action.description |
Descripción legible por humanos. Debe ser clara, concisa y completa. Obligatorio. | Es el corazón del principio auto-declarativo. Debe contar la historia. |
resource |
Objeto que describe el recurso o entidad afectada, el "sobre qué". | Aporta el objeto directo de la acción, completando la frase narrativa. |
resource.type |
Tipo de recurso en lenguaje natural (ej. "Factura", "Proyecto", "Usuario"). | Clasifica el objeto afectado para facilitar el análisis. |
resource.name |
Nombre o identificador legible del recurso (ej. "Factura #12345", "Proyecto Alpha"). | Especifica la instancia exacta del recurso que fue alterada. |
context |
Información técnica y ambiental para entender el "cómo" y el "dónde". | Proporciona pistas cruciales para investigaciones de seguridad o depuración. |
context.clientIp |
Dirección IP de origen de la solicitud. Esencial para análisis de seguridad. | Permite geolocalizar el origen de la acción y detectar patrones anómalos. |
context.userAgent |
Cadena del agente de usuario del cliente (navegador, app móvil, etc.). | Ayuda a identificar el tipo de dispositivo o software utilizado. |
context.details |
Objeto flexible para datos de contexto adicionales que completen la historia. | Un "cajón de sastre" para información valiosa que no encaja en otros campos. |
4. Taxonomía de Acciones: Un Vocabulario Controlado (action.type)
Para garantizar la consistencia y permitir un análisis de datos efectivo, todas las acciones deben clasificarse utilizando la siguiente taxonomía plana. La elección de un action.type correcto es fundamental.
action.type |
Cuándo Usarlo |
|---|---|
USER_LOGIN_SUCCESS |
Un usuario se autentica exitosamente en la plataforma. |
USER_LOGIN_FAILURE |
Falla un intento de inicio de sesión (por contraseña incorrecta, usuario inexistente, etc.). |
USER_LOGOUT |
Un usuario finaliza su sesión de forma explícita. |
PROFILE_DATA_UPDATED |
El usuario modifica la información de su propio perfil (ej. nombre, teléfono). |
PASSWORD_UPDATED |
El usuario completa exitosamente un cambio de su propia contraseña. |
DOCUMENT_VIEWED |
Se visualiza o se accede al contenido de un documento, archivo o reporte. |
DOCUMENT_DOWNLOADED |
Se inicia la descarga de un documento o archivo a un dispositivo local. |
ENTITY_CREATED |
Creación de un nuevo recurso de negocio (ej. un proyecto, una tarea, un cliente). |
ENTITY_UPDATED |
Modificación de un recurso de negocio existente. |
ENTITY_DELETED |
Eliminación (lógica o física) de un recurso de negocio. |
ADMIN_ACTION |
Acción privilegiada que afecta a otros usuarios o a la configuración global del sistema. |
5. El Arte de la Descripción: Creando Logs que Cuentan Historias
El campo action.description es el pilar de la legibilidad. Su propósito es verbalizar el evento de una manera que sea inmediatamente comprensible para un ser humano. Los demás campos actúan como datos estructurados que apoyan y enriquecen esta narrativa.
Ejemplo 1: Un administrador reasigna un rol.
La descripción debe ser la protagonista, explicando la acción de forma concisa.
{
"eventTimestamp": "2025-06-27T18:10:00Z",
"user": { "email": "[email protected]", "name": "Admin General", "role": "Superadmin" },
"action": {
"type": "ADMIN_ACTION",
"description": "El administrador reasignó el rol del usuario '[email protected]' de 'Editor' a 'Administrador'."
},
"resource": { "type": "Cuenta de Usuario", "name": "[email protected]" },
"context": {
"clientIp": "203.0.113.100",
"userAgent": "Mozilla/5.0...",
"details": { "adminPanelUrl": "/admin/users/edit/ana.gomez" }
}
}
Ejemplo 2: Un cliente actualiza campos específicos de su perfil.
Note cómo details enriquece la descripción sin sobrecargarla.
{
"eventTimestamp": "2025-06-27T19:30:15Z",
"user": { "email": "[email protected]", "name": "Juan Rodríguez", "role": "Cliente" },
"action": {
"type": "PROFILE_DATA_UPDATED",
"description": "El usuario actualizó su dirección de facturación personal."
},
"resource": { "type": "Perfil de Usuario", "name": "Juan Rodríguez" },
"context": {
"clientIp": "198.51.100.20",
"userAgent": "Mozilla/5.0...",
"details": { "changedFields": ["addressLine1", "city", "postalCode"] }
}
}
6. Disciplinas y Anti-Patrones de la V0.1
Incluso en una primera versión simplificada, la disciplina es clave para no generar ruido en lugar de señales.
Protección de Credenciales Sensibles: Categóricamente Prohibido. Nunca, bajo ninguna circunstancia, se deben registrar contraseñas, tokens de sesión, claves de API o cualquier otro secreto en texto plano. La descripción debe indicar el evento ("El usuario cambió su contraseña"), no el valor del secreto.
Principio de Concisión Selectiva: Evitar el Vuelco de Datos. El campo
context.detailses para añadir contexto, no para volcar objetos enteros de la base de datos o payloads de API completos. Seleccione manualmente 2 o 3 piezas de información clave que realmente aporten valor a la historia del log.La Plaga de la Generalidad: Exigir Descripciones Específicas. Una
action.descriptioncomo "Entidad actualizada" o "Registro modificado" es inaceptable por su inutilidad. Debe ser específica: "El usuario actualizó el número de teléfono del contacto 'Juan Pérez'" o "Se cambió el estado del proyecto 'Alpha' a 'Completado'".
7. Conclusión: La Hoja de Ruta hacia la V1.0
La versión 0.1 de estos lineamientos es un paso táctico y deliberado. Su objetivo es entregar valor inmediato a los equipos de seguridad, soporte y producto, proporcionando una trazabilidad clara y legible desde el primer día. Es un fundamento sólido sobre el cual construir.
La evolución natural hacia una versión 1.0, más robusta y segura, debe contemplar la siguiente hoja de ruta:
- Minimizar la Exposición de PII: El paso más crítico será reemplazar los datos personales explícitos (
user.email,user.name) por identificadores únicos y estables (user.id). Esta transición es fundamental para alinearse con las mejores prácticas de seguridad y requerirá el desarrollo de una herramienta interna segura que permita a personal autorizado resolver estos IDs. - Industrializar la Generación de Logs: Introducir un SDK de Auditoría o una librería centralizada. Esto estandarizará la creación de eventos, reducirá errores de implementación en los distintos servicios y facilitará futuras actualizaciones del formato del log.
- Enriquecer el Contexto para la Observabilidad: Incorporar identificadores de correlación (
traceId,requestId) en elcontext. Esto permitirá vincular un evento de auditoría específico con los logs de aplicación, métricas y trazas correspondientes, creando una visión de 360 grados de cada solicitud. - Formalizar y Escalar la Taxonomía: A medida que la plataforma crezca, la taxonomía plana actual deberá evolucionar. Un modelo jerárquico (ej.
Domain.Resource.ActioncomoUSER_MANAGEMENT.ACCOUNT.ROLE_CHANGED) permitirá una organización más granular y potente de los miles de eventos futuros.
Estos lineamientos V0.1 no son el destino final, sino el primer y más importante paso en el camino hacia una cultura de auditoría y responsabilidad madura.
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
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.
Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios
- Mauricio ECR
- Convenciones
- 18 Jun, 2025
En el ecosistema de aplicaciones web modernas, la arquitectura de microservicios distribuidos se ha consolidado como un paradigma dominante. Su flexibilidad, escalabilidad y resiliencia son innegables
Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios
- Mauricio ECR
- Convenciones
- 18 Jun, 2025
En el ecosistema de aplicaciones web modernas, la arquitectura de microservicios distribuidos se ha consolidado como un paradigma dominante. Su flexibilidad, escalabilidad y resiliencia son innegables. Sin embargo, esta distribución introduce una complejidad significativa en la gestión de estados y, especialmente, en el manejo de errores. Cuando una operación involucra a múltiples servicios, identificar la causa raíz de un fallo puede convertirse en una tarea titánica, afectando la experiencia del usuario, los tiempos de resolución y la mantenibilidad del sistema.
Este documento establece un estándar completo y directamente aplicable para la codificación, documentación y gestión de errores visibles al usuario final. El objetivo es crear un marco de trabajo robusto que, aunque inicialmente se implementará en un entorno de backend con Java (Spring Boot) y frontend con Angular, ha sido diseñado con un enfoque agnóstico al lenguaje para garantizar su longevidad y adaptabilidad a futuras tecnologías.
Adoptar este estándar permitirá no solo presentar mensajes de error claros y útiles al usuario, sino también establecer una base documental sólida que facilite la observabilidad, la trazabilidad y la colaboración entre equipos de desarrollo, QA y soporte técnico.
El Estándar Detallado
1. Filosofía y Principios Fundamentales
Antes de detallar la implementación, es crucial entender los principios que guían este estándar:
- El Error como Ciudadano de Primera Clase: Los errores no son un caso excepcional, sino una parte inherente del flujo de una aplicación. Deben ser diseñados, documentados y probados con el mismo rigor que las funcionalidades exitosas.
- Claridad para el Usuario, Detalle para el Desarrollador: La información presentada al usuario final debe ser concisa, clara, traducible y orientada a la acción. Por el contrario, la información registrada para los equipos técnicos debe ser rica en detalles para permitir un diagnóstico rápido y preciso.
- Seguridad por Opacidad: Los códigos y mensajes de error públicos nunca deben revelar detalles de la infraestructura interna, nombres de clases, trazas de pila (stack traces) o cualquier información que pueda ser explotada por un actor malicioso.
- Trazabilidad Extrema: Cada error debe ser unívocamente identificable y correlacionable a través de los distintos servicios y sistemas de observabilidad (logs, métricas, trazas distribuidas).
2. Formato del Código de Error
Todo error visible al usuario final o que cruce los límites de un microservicio deberá ser identificado por un código único y opaco. Este código actúa como una clave inmutable que desacopla el error en sí de su representación (el mensaje).
La estructura propuesta es: MSS-ECNNN[-EXTCODE]
MSS(MicroService Short-identifier): Identificador numérico único de 3 dígitos asignado a cada microservicio. Esta asignación debe ser gestionada en un registro centralizado para evitar colisiones.- Ejemplo:
101para "Servicio de Autenticación",205para "Servicio de Pedidos".
- Ejemplo:
EC(Error Category): Código alfabético de 2 letras que clasifica la naturaleza del error. Esto permite un filtrado y análisis rápido. (Ver sección 3 para el catálogo de categorías).- Ejemplo:
IVpara "Validación de Entrada",BRpara "Regla de Negocio".
- Ejemplo:
NNN(Numeric Sequence): Secuencia numérica de 3 dígitos, única dentro del microservicio para esa categoría. Se recomienda iniciar en001y aumentar de forma secuencial.- Ejemplo:
001,002,047.
- Ejemplo:
[EXTCODE](External Code - Opcional): Componente opcional para encapsular errores originados en servicios de terceros. Proporciona una correlación directa sin exponer el formato original del tercero.- Formato:
EXT_<ID_SERVICIO>_<CODIGO_ERROR_ORIGINAL> ID_SERVICIO: Un identificador corto y predefinido para el servicio externo (ej.STRIPE,SENDGRID,AWS_S3).CODIGO_ERROR_ORIGINAL: El código de error devuelto por el servicio externo, sanitizado para ser compatible con la URL (ej. reemplazando espacios o caracteres especiales por_).- Ejemplo:
205-DP003-EXT_STRIPE_card_declined
- Formato:
Ejemplo completo: 205-BR001 representa el primer error de "Regla de Negocio" definido en el microservicio "Pedidos" (ID 205).
3. Clasificación de Errores Comunes
La estandarización de categorías (EC) es fundamental para la observabilidad y la generación de métricas. El catálogo inicial es el siguiente:
| Código | Categoría | Descripción |
|---|---|---|
IV |
Input Validation | Errores relacionados con la validación de datos de entrada (formato, rango, campos obligatorios). Suelen ser errores del tipo 400 Bad Request. |
AU |
Authentication & Authorization | Fallos de autenticación (token inválido, credenciales incorrectas) o autorización (sin permisos para la acción). Errores 401 Unauthorized o 403 Forbidden. |
BR |
Business Rule | Violación de una regla de negocio específica. La solicitud es sintácticamente correcta pero semánticamente inválida en el contexto actual (ej. stock insuficiente). Suele ser un 409 Conflict o 422 Unprocessable Entity. |
DP |
Dependency Failure | Un servicio externo del que depende el microservicio no está disponible o ha devuelto un error (otra API interna, base de datos, sistema de colas). Errores del tipo 502 Bad Gateway o 503 Service Unavailable. |
SY |
System Error | Errores inesperados o no controlados en el servidor (excepciones RuntimeException, NullPointerException, etc.). Siempre deben ser investigados. Corresponden a un 500 Internal Server Error. |
NT |
Network & Timeout | Errores de comunicación, ya sea porque una dependencia no respondió a tiempo o por problemas en la red. Corresponde a un 504 Gateway Timeout. |
CF |
Configuration Error | El servicio no puede iniciarse o funcionar correctamente debido a una configuración faltante o incorrecta (variables de entorno, secretos, etc.). |
NF |
Not Found | El recurso solicitado no existe. Corresponde a un 440 Not Found. |
4. Catálogo Inicial de Códigos de Error
A continuación, se presenta un catálogo de ejemplo con 10 códigos para dos microservicios hipotéticos: Gestión de Usuarios (ID: 101) y Procesamiento de Pedidos (ID: 205).
| Código | Mensaje usuario | Descripción técnica | Severidad | Estado | Acción esperada | Servicio externo (si aplica) |
|---|---|---|---|---|---|---|
| 101-IV001 | El correo electrónico proporcionado no tiene un formato válido. Por favor, revísalo. | El campo email en el DTO de registro de usuario no supera la validación de la expresión regular ^(.+)@(.+)$. |
Baja | Activo | Usuario: Corregir el formato del email. Frontend: Realizar validación en cliente para prevenir el error. | N/A |
| 101-AU001 | Tu sesión ha expirado. Por favor, inicia sesión de nuevo. | El JWT recibido en la cabecera Authorization ha caducado. La fecha de expiración (exp) es anterior a la fecha actual. |
Media | Activo | Sistema: Redirigir al usuario a la página de login. Usuario: Volver a introducir sus credenciales. | N/A |
| 101-AU002 | No tienes permisos para realizar esta acción. Contacta al administrador si crees que es un error. | El usuario autenticado, extraído del token, no posee el rol requerido (ROLE_ADMIN) para acceder al endpoint. |
Media | Activo | Frontend: Ocultar o deshabilitar la opción que provoca el error. Soporte: Verificar los roles del usuario si este levanta un ticket. | N/A |
| 101-BR001 | El correo electrónico ya está registrado en nuestra plataforma. Intenta iniciar sesión. | Se intentó crear un usuario con un email que ya existe en la tabla users de la base de datos (violación de UNIQUE constraint). |
Baja | Activo | Usuario: Usar la opción "recuperar contraseña" o iniciar sesión. Frontend: Ofrecer un enlace directo a la página de login. | N/A |
| 205-IV001 | El identificador del producto no es válido. Debe ser un número positivo. | El productId recibido en la línea de un pedido es nulo, cero o negativo. La validación @Positive falló en el DTO. |
Baja | Activo | Usuario: Informar del error. Es un caso raro que indica un bug en el frontend. Desarrollo: Investigar cómo se pudo enviar un ID inválido desde el cliente. | N/A |
| 205-BR001 | No hay suficiente stock para el producto 'Nombre del Producto'. Solo quedan X unidades. | La cantidad solicitada de un producto es mayor que el stock disponible registrado en la base de datos para ese productId. |
Media | Activo | Usuario: Reducir la cantidad del producto o eliminarlo del carrito. Sistema: El mensaje debe incluir el stock actual para ser útil. | N/A |
| 205-BR002 | No se pueden añadir productos de diferentes vendedores en un mismo pedido. | La lógica de negocio impide mezclar productos de sellerId distintos en una sola transacción para simplificar la logística. |
Media | Activo | Usuario: Finalizar la compra actual y crear un nuevo pedido para los otros productos. Frontend: Mostrar una advertencia al intentar añadir un producto incompatible. | N/A |
| 205-DP001 | El servicio de inventario no está respondiendo. Por favor, inténtalo de nuevo en unos minutos. | La llamada HTTP al microservicio de Inventario (ID 310) para verificar el stock ha resultado en un timeout o error 5xx. |
Alta | Activo | Sistema: Implementar un reintento con exponential backoff. Si persiste, activar un circuit breaker. Soporte: Revisar el estado del servicio de Inventario. | N/A |
| 205-DP002 | Tu pago no pudo ser procesado por el proveedor. Razón: Fondos insuficientes. | La pasarela de pagos (Stripe) devolvió un error 402 Request Failed con el código card_declined y el motivo insufficient_funds. |
Media | Activo | Usuario: Intentar con otro método de pago o verificar los fondos de su tarjeta. Sistema: No reintentar automáticamente. Invalidar el pedido. | EXT_STRIPE_insufficient_funds |
| 205-SY001 | Ha ocurrido un error inesperado al procesar tu pedido. Nuestro equipo ya ha sido notificado. | Se ha capturado una NullPointerException no esperada en la clase OrderProcessingService al calcular los impuestos. Se ha creado una alerta en Sentry/Datadog. |
Alta | Activo | Sistema: Registrar el trace_id y toda la información de la petición. Desarrollo: Priorizar la corrección de este bug. Usuario: Reintentar más tarde o contactar a soporte con el trace_id si se le muestra. |
N/A |
5. Documentación y Centralización
La documentación es lo que transforma este estándar de una idea a una herramienta útil.
- Estructura de Archivos: Cada microservicio debe incluir en su repositorio un archivo
docs/errors/errors.md. Este archivo contendrá la tabla completa de errores que el servicio puede generar. - Control de Versiones: El archivo
errors.mddebe ser versionado junto con el código fuente del microservicio. Cualquier adición o modificación de un código de error debe formar parte de un Pull Request, permitiendo su revisión. - Formato de Documentación: Se utilizará la tabla Markdown definida en la sección anterior. Este formato es fácil de leer para humanos y de parsear por máquinas.
Ejemplo de microservice-orders/docs/errors/errors.md:
Catálogo de Errores - Servicio de Pedidos
- Nombre Lógico: Servicio de Procesamiento de Pedidos
- ID del Microservicio: 205
| Código | Mensaje usuario | Descripción técnica | Severidad | Estado | Acción esperada | Servicio externo (si aplica) |
|---|---|---|---|---|---|---|
| 205-IV001 | El identificador del producto no es válido. Debe ser un número positivo. | El productId recibido en la línea de un pedido es nulo, cero o negativo. La validación @Positive falló en el DTO. |
Baja | Activo | Usuario: Informar del error. Desarrollo: Investigar cómo se pudo enviar un ID inválido. | N/A |
| ... | ... | ... | ... | ... | ... | ... |
6. Recomendaciones para Centralización y Escalabilidad
Para que el estándar sea efectivo en una organización grande, se recomienda:
- Repositorio Central de Errores: Un pipeline de CI/CD debería agregar los archivos
errors.mdde todos los microservicios tras cada release exitosa en un repositorio central o una página de Confluence/Wiki. Esto crea un único punto de verdad para que los equipos de QA, Soporte y Frontend puedan consultar cualquier código de error sin necesidad de acceder a los repositorios individuales. - Automatización y Linters: El pipeline de CI/CD debe incluir pasos para:
- Validar la Unicidad: Asegurar que no existan códigos
MSS-ECNNNduplicados dentro del mismo microservicio. - Validar el Formato: Comprobar que todos los nuevos códigos sigan la estructura definida.
- Detectar Nuevos Servicios Externos: Lanzar una advertencia si se añade un
EXTCODEcon un<ID_SERVICIO>no registrado previamente, para asegurar que se documente centralmente.
- Validar la Unicidad: Asegurar que no existan códigos
- Generación de Artefactos: Se pueden crear scripts que lean estos archivos
.mdpara generar automáticamente artefactos útiles, como:- Enums de TypeScript para el frontend, permitiendo un manejo de errores tipado (
case ErrorCodes.USER_EMAIL_EXISTS:). - Clases de constantes en Java para el backend.
- Plantillas para sistemas de tickets (Jira, Zendesk).
- Enums de TypeScript para el frontend, permitiendo un manejo de errores tipado (
Conclusión
Este estándar de codificación y gestión de errores proporciona un marco de trabajo unificado y resiliente, diseñado para escalar con la complejidad de una arquitectura de microservicios. Al tratar los errores como un componente central del diseño de software, logramos múltiples beneficios:
- Mejora la Experiencia del Usuario (UX): Los mensajes son claros, consistentes y orientados a la acción.
- Agiliza la Resolución de Incidencias: Los códigos únicos y la documentación detallada permiten a los equipos de soporte y desarrollo identificar y solucionar problemas rápidamente.
- Potencia la Observabilidad: La estructura de códigos y categorías facilita la creación de dashboards, alertas y métricas significativas sobre la salud del sistema.
- Aumenta la Seguridad: La opacidad de los códigos previene la fuga de información sensible de la infraestructura.
Futuras Líneas de Aplicación:
- Integración con Plataformas de Observabilidad: Enriquecer automáticamente las trazas distribuidas (ej. en Jaeger o Datadog) con la "Descripción Técnica" del error a partir de su código.
- Desarrollo de un "Servicio de Errores": Una pequeña API central que, dado un código de error, devuelva su documentación completa, incluyendo el mensaje de usuario localizado en diferentes idiomas.
- Generación de SDKs de Cliente: Automatizar la creación de librerías para diferentes lenguajes (JS/TS, Python, Go) que encapsulen la lógica de manejo de estos códigos de error estandarizados.
La adopción disciplinada de este estándar no es una carga adicional, sino una inversión estratégica en la calidad, mantenibilidad y escalabilidad a largo plazo de la plataforma.
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle
- Mauricio ECR
- DevOps
- 16 Jun, 2025
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
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle
- Mauricio ECR
- DevOps
- 16 Jun, 2025
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 como consumidores. Hoy, damos el salto más emocionante: pasaremos de ser meros usuarios a ser creadores. Vamos a construir nuestro propio plugin binario desde cero, utilizando Java para la lógica y el Groovy DSL para nuestros scripts de build.
¿Por qué un plugin binario? Porque es el estándar profesional. A diferencia de los scripts sueltos, un plugin binario es un artefacto compilado (.jar), versionable, fácilmente distribuible y, sobre todo, mucho más robusto y testeable. Es el vehículo perfecto para encapsular la lógica compleja que nuestro futuro generador de código necesitará.
En este capítulo, construiremos un plugin "saludador" (Greeter). Será sencillo en su función —mostrar un mensaje configurable— pero nos enseñará la anatomía completa de un plugin en un entorno Java: su estructura, su punto de entrada, cómo hacerlo configurable y, finalmente, cómo probarlo. ¡Es hora de arremangarse y empezar a programar nuestro build!
1. La Anatomía de un Plugin: Estructura del Proyecto
Para empezar, necesitamos un entorno de trabajo. La mejor manera de desarrollar y probar un plugin es con un build multi-proyecto. Crearemos una estructura que contenga tanto la lógica del plugin como un proyecto de ejemplo que lo consumirá.
Abre tu terminal y crea la siguiente estructura de directorios:
nuestro-generador/
├── plugin/ # Directorio para el código de nuestro plugin
└── proyecto-de-prueba/ # Un proyecto simple para aplicar y probar el plugin
Ahora, configuremos cada parte.
a) Configurando el Proyecto del Plugin (/plugin)
Este es el corazón de nuestro trabajo. Dentro del directorio plugin, crea un archivo build.gradle y un settings.gradle.
plugin/settings.gradle:
rootProject.name = 'mi-plugin-saludador'
plugin/build.gradle:
Este archivo es crucial. Le dice a Gradle que estamos construyendo un plugin de Gradle con código Java.
plugins {
// Plugin esencial para desarrollar plugins de Gradle en Java
id 'java-gradle-plugin'
}
repositories {
mavenCentral()
}
// Este bloque configura los detalles de nuestro plugin
gradlePlugin {
plugins {
// "greeterPlugin" es el nombre que le damos a nuestra configuración
greeterPlugin {
id = 'com.miempresa.greeter' // El ID único que los usuarios usarán
implementationClass = 'com.miempresa.GreetingPlugin' // La clase Java que implementa la lógica
}
}
}
b) El Código Fuente del Plugin
Gradle necesita saber dónde está el código. Con el plugin java-gradle-plugin, asumirá la estructura estándar de Java.
Crea la clase del Plugin: Dentro de
plugin/, crea la ruta de directoriossrc/main/java/com/miempresa/. Dentro, crea el archivoGreetingPlugin.java.El archivo de propiedades: El plugin
java-gradle-pluginy el bloquegradlePluginse encargan de generar automáticamente el archivo de propiedades necesario (META-INF/gradle-plugins/com.miempresa.greeter.properties). ¡Magia!
2. El Punto de Entrada: La Interfaz Plugin<Project>
Todo plugin binario debe implementar la interfaz Plugin<Project>. Su método apply(Project project) es la puerta de entrada, el equivalente al main de una aplicación. Es aquí donde toda nuestra lógica se conectará al proyecto que use el plugin.
Edita tu archivo plugin/src/main/java/com/miempresa/GreetingPlugin.java:
package com.miempresa;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class GreetingPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// El objeto "project" es nuestra puerta de acceso al build del consumidor.
// Aquí registraremos tareas, extensiones, etc.
project.getLogger().lifecycle("¡Plugin 'greeter' aplicado con éxito!");
}
}
Ya tenemos un esqueleto funcional. ¡Pero un plugin que no hace nada no es muy útil!
3. Haciendo tu Plugin Configurable con Extensiones
Rara vez querremos que un plugin se comporte siempre igual. Necesitamos una forma de que el usuario lo configure. En lugar de pasar parámetros de forma engorrosa, Gradle nos ofrece un mecanismo elegante: las Extensiones.
Una extensión no es más que un POJO (Plain Old Java Object) cuyas propiedades serán configurables desde el build.gradle del consumidor a través de sus getters y setters.
Crea la clase de la Extensión: Dentro de
com.miempresa, crea un nuevo archivoGreeterExtension.java.package com.miempresa; public class GreeterExtension { // En Java, las propiedades se modelan con campos privados y getters/setters públicos. private String message = "Hola por defecto desde el plugin Java"; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }Registra la Extensión: Ahora, volvamos a
GreetingPlugin.javay registremos la extensión en el métodoapply.// ... en GreetingPlugin.java @Override public void apply(Project project) { // Registramos una extensión llamada "greeter" que usa nuestra clase POJO. // El usuario la configurará con un bloque `greeter { ... }` en su build.gradle GreeterExtension extension = project.getExtensions().create("greeter", GreeterExtension.class); // ... más lógica vendrá aquí }
4. Dando Vida al Plugin: Tareas Personalizadas
El objetivo final de un plugin es, generalmente, añadir nuevas tareas. Vamos a crear una tarea greet que use el mensaje de nuestra extensión.
Registra la Tarea: Dentro del método
applydeGreetingPlugin.java, después de registrar la extensión, registraremos la tarea.// ... en GreetingPlugin.java, dentro del método apply() @Override public void apply(Project project) { // La variable debe ser final (o efectivamente final) para ser usada en la lambda final GreeterExtension extension = project.getExtensions().create("greeter", GreeterExtension.class); // Registramos una nueva tarea llamada "greet" project.getTasks().register("greet", task -> { task.setGroup("Saludos"); // Agrupa la tarea en la lista de `./gradlew tasks` task.setDescription("Muestra un saludo configurable"); // La acción que se ejecutará cuando se llame a la tarea task.doLast(t -> { // Usamos la extensión capturada del scope exterior project.getLogger().lifecycle(extension.getMessage()); }); }); }
Hemos conectado todo: El plugin registra una extensión para recibir configuración y una tarea que lee esa configuración para ejecutar una acción.
5. ¡A Probar! Aplicando y Verificando el Plugin Localmente
Es el momento de la verdad. Vamos a usar nuestro proyecto-de-prueba para consumir el plugin que acabamos de crear.
Modifica el
settings.gradleprincipal: Necesitamos decirle a Gradle que nuestro build tiene dos subproyectos y que uno (plugin) es un "build incluido" que provee plugins. Crea un archivosettings.gradleen el directorio raíz (nuestro-generador/).// nuestro-generador/settings.gradle rootProject.name = 'generador-completo' // Incluye el proyecto que usará el plugin include 'proyecto-de-prueba' // Incluye el build que contiene nuestro plugin includeBuild 'plugin'Configura el
proyecto-de-prueba: Ahora, crea un archivobuild.gradledentro deproyecto-de-prueba/.// nuestro-generador/proyecto-de-prueba/build.gradle plugins { // ¡Aplicamos nuestro plugin usando el ID que definimos! id 'com.miempresa.greeter' } // Configuramos la extensión que creamos en el plugin. // El Groovy DSL llama a setMessage() automáticamente. greeter { message = '¡Mi primer plugin en Java funciona! 🎉' }Ejecuta la Tarea: Vuelve a la raíz (
nuestro-generador/) en tu terminal y ejecuta la tarea, especificando la ruta completa del proyecto:gradle :proyecto-de-prueba:greet
Si todo ha ido bien, deberías ver la salida:
Task :proyecto-de-prueba:greet ¡Mi primer plugin en Java funciona! 🎉
Y si ejecutas gradle :proyecto-de-prueba:tasks verás tu tarea listada bajo el grupo "Saludos".
Conclusión y Siguientes Pasos
¡Lo has conseguido! Has trascendido la barrera de simple usuario para convertirte en un desarrollador de plugins de Gradle usando Java. Hoy has aprendido el ciclo de vida completo de la creación de un plugin: desde la estructura del proyecto y el uso del plugin java-gradle-plugin, pasando por la implementación de la interfaz Plugin<Project>, la creación de una extensión POJO para hacerlo configurable, y el registro de una tarea personalizada que materializa su lógica.
Ya tenemos una base robusta y profesional. Nuestro plugin puede ser configurado y puede ejecutar acciones. Ahora que dominamos la estructura, el siguiente paso es hacerlo realmente útil.
En la próxima entrega de esta serie, tomaremos este esqueleto y le añadiremos músculos. Integraremos un motor de plantillas como FreeMarker, y modificaremos nuestra tarea para que, en lugar de imprimir un simple mensaje, genere una estructura completa de directorios y archivos. La aventura de nuestro generador de código está a punto de dar su paso más importante.
Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java
- Mauricio ECR
- DevOps
- 14 Jun, 2025
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
Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java
- Mauricio ECR
- DevOps
- 14 Jun, 2025
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ón y gestionar dependencias es un lastre para la productividad. Aquí es donde entran en juego los sistemas de automatización de construcción, y hoy, nos enfocaremos en uno de los más potentes y flexibles del ecosistema Java: Gradle.
Este artículo es el punto de partida de una serie en la que no solo aprenderemos a usar Gradle, sino que construiremos nuestra propia herramienta avanzada: un plugin capaz de generar esqueletos de proyectos Java y módulos CRUD completos bajo la filosofía de Domain-Driven Design (DDD). Pero antes de correr, debemos aprender a caminar. ¡Acompáñanos en este primer paso para sentar unas bases sólidas y duraderas con Gradle!
¿Qué es Gradle y por qué Debería Importarte?
Imagina a Gradle como el director de orquesta de tu proyecto. Es un sistema de automatización de construcción de código abierto que toma tu código fuente, las librerías de las que depende, y una serie de instrucciones, y produce un artefacto final (como un archivo .jar o .war).
Si vienes del mundo de Java, es probable que hayas oído hablar de Maven o incluso del venerable Ant. ¿Qué hace diferente a Gradle?
- Frente a Maven: Mientras que Maven se rige por la "convención sobre configuración" con una estructura rígida definida en archivos
pom.xml, Gradle ofrece una flexibilidad inmensa. Su filosofía se basa en un DSL (Domain Specific Language), un lenguaje específico para el dominio de la construcción de software, que se escribe en Groovy o Kotlin. Esto transforma tus scripts de construcción de simples archivos de configuración a potentes programas. - Frente a Ant: Ant también usa scripts (XML), pero es mucho más imperativo. Le dices qué hacer y cómo hacerlo. Gradle es más declarativo; describes qué quieres lograr, y Gradle, con su modelo de grafos de dependencias, se encarga de la manera más eficiente de lograrlo.
Conceptos Clave para Empezar
Para hablar el idioma de Gradle, necesitas conocer su vocabulario esencial:
- Proyectos (Projects): Un proyecto es cualquier componente que quieres construir. Puede ser una librería (
.jar) o una aplicación web completa. Un repositorio puede contener un único proyecto o múltiples subproyectos. - Tareas (Tasks): Son las unidades de trabajo en Gradle. Una tarea puede ser compilar código (
compileJava), ejecutar pruebas (test), crear un archivo (build) o cualquier acción que definas. - Plugins: Son extensiones que añaden nuevas capacidades y tareas a tu proyecto. Por ejemplo, el plugin de Java añade tareas para compilar y probar código Java. Son el corazón de la reusabilidad en Gradle.
- Dependencias (Dependencies): Son las librerías o módulos externos que tu proyecto necesita para funcionar. Gradle se encarga de descargarlas de repositorios (como Maven Central) y hacerlas disponibles en tu proyecto.
Preparando el Terreno: Tu Entorno de Desarrollo 🛠️
Antes de escribir una sola línea, asegúrate de tener las herramientas adecuadas.
- JDK (Java Development Kit): Gradle se ejecuta sobre la JVM, por lo que necesitas un JDK instalado. La versión 17 o superior es una excelente elección para proyectos modernos.
- Instalación de Gradle: Aunque puedes descargarlo manualmente, la forma más recomendada es usar un gestor de versiones como SDKMAN! (para Linux/macOS) o simplemente usar el Gradle Wrapper, una pequeña utilidad que se incluye en los proyectos Gradle y que descarga la versión correcta automáticamente. ¡No te preocupes, lo veremos en acción ahora mismo!
- IDE (Entorno de Desarrollo Integrado): IntelliJ IDEA ofrece una integración con Gradle que es simplemente espectacular. Visual Studio Code, con la extensión "Gradle for Java", es también una alternativa fantástica y ligera.
¡Manos a la Obra! Tu Primer Proyecto con Gradle
La teoría está muy bien, pero la magia sucede en la práctica. Vamos a crear un proyecto Java desde cero. Abre tu terminal en una carpeta vacía y ejecuta:
gradle init
Gradle te hará algunas preguntas:
- Select type of project to generate: Elige
2: application. - Select implementation language: Elige
3: Java. - Split functionality across multiple subprojects?: Elige
1: no. - Select build script DSL: Elige
1: Groovy(es un excelente punto de partida, aunque también podrías elegir Kotlin). - Generate build using new APIs and behavior?: Elige
nopor ahora para mantenerlo simple. - Select test framework: Elige
4: JUnit Jupiter. - Project name y Source package: Presiona Enter para aceptar los valores por defecto.
¡Y listo! 🎉 Gradle ha creado una estructura de proyecto funcional:
.
├── build.gradle // El script de construcción principal
├── gradle
│ └── wrapper
├── gradlew // El ejecutable del Wrapper para Linux/macOS
├── gradlew.bat // El ejecutable del Wrapper para Windows
├── settings.gradle // Configuración de proyectos/subproyectos
└── src
├── main // Código fuente de la aplicación
│ └── java
└── test // Código fuente de las pruebas
El archivo más importante aquí es build.gradle. Ábrelo y verás algo así:
// build.gradle
plugins {
id 'java'
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
implementation 'com.google.guava:guava:32.1.3-jre' // Guava ya viene incluido
}
application {
mainClass = 'org.example.App'
}
Puedes ver los conceptos en acción: se aplican los plugins java y application, se declara el repositorio mavenCentral() para buscar dependencias y se especifica la dependencia de JUnit para las pruebas.
Ahora, ejecutemos algunas tareas básicas desde la terminal:
./gradlew build: Compila, prueba y empaqueta tu aplicación../gradlew run: Ejecuta la aplicación../gradlew clean: Borra el directoriobuildcon todos los artefactos generados.
El Corazón de Gradle: Un Vistazo a las Tareas
Todo lo que hace Gradle es a través de tareas. Puedes definir las tuyas de forma muy sencilla. Añade esto al final de tu build.gradle:
// Tarea personalizada
task miPrimeraTarea {
doLast {
println '¡Hola desde mi primera tarea en Gradle!'
}
}
Ahora ejecuta ./gradlew miPrimeraTarea y verás tu mensaje. Las tareas tienen un ciclo de vida, siendo doFirst y doLast las acciones que puedes añadir al principio o al final de su ejecución.
Más importante aún es cómo las tareas se relacionan. Puedes hacer que una tarea dependa de otra:
task tareaB {
doLast { println 'Soy la tarea B' }
}
task tareaA(dependsOn: tareaB) {
doLast { println 'Soy la tarea A, y me ejecuto después de la B' }
}
Esta capacidad de crear un grafo de dependencias es lo que permite a Gradle ser tan eficiente. Además, gracias a los inputs y outputs de las tareas, Gradle sabe si el trabajo ya está hecho (UP-TO-DATE) y puede saltarse la ejecución, ahorrando un tiempo valiosísimo.
El Poder de la Reutilización: Introducción a los Plugins
Escribir la misma lógica de construcción en cada proyecto es tedioso. Los plugins son la solución para encapsular y reutilizar esta lógica. Ya los has usado (id 'java'). Existen tres tipos principales:
- Script Plugins: Un script de Gradle (
otro.gradle) que importas en tubuild.gradle. Útil para lógicas simples dentro de un mismo proyecto. - Precompiled Script Plugins: Scripts
.gradle.ktso.gradleque se compilan y empaquetan, permitiendo una mejor organización. - Binary Plugins: El enfoque profesional. Son clases (en Java, Groovy o Kotlin) que implementan la interfaz
Plugin. Se distribuyen como archivos.jary son el objetivo final de nuestra serie de artículos.
Conclusión y Próximos Pasos
¡Felicidades! Has dado un paso gigante. Ahora entiendes que Gradle es un sistema de construcción programable y altamente flexible, has configurado tu entorno, has creado y ejecutado tu primer proyecto Java y has escarbado en la superficie de sus conceptos más importantes: las tareas y los plugins.
Esta base sólida es el cimiento sobre el que construiremos nuestro conocimiento. Hemos sentado las bases teóricas y prácticas para entender no solo qué hace Gradle, sino cómo piensa.
En nuestra próxima entrega, daremos el siguiente paso lógico y emocionante: comenzaremos a construir nuestro propio plugin binario desde cero. Exploraremos la estructura de un proyecto de plugin, aprenderemos a crear configuraciones personalizadas para que los usuarios puedan ajustarlo y definiremos nuestras primeras tareas encapsuladas. ¡La verdadera aventura de la automatización está a punto de comenzar!