Desarrollando bajo el ecosistema de Spring y Spring Boot, en algún momento habrás usado su inicializador Spring Initializr para crear la estructura de un proyecto base, seleccionado el lenguaje, el modo de construcción, la versión del framework, las librerías, el modo de empaquetación,… en este tutorial vamos a explorar la posibilidad de disponer de nuestro propio inicializador, para que responda a las necesidades de nuestros proyectos corporativos.
Contenido.
1. Introducción.
Para perder el miedo al folio en blanco a la hora de arrancar a desarrollar bajo un framework, más si se trata de un framework corporativo, se suelen proporcionar proyectos de ejemplo o, en el mejor de los casos, arquetipos de aplicaciones que permiten generar fácilmente un proyecto con el que empezar. Vamos a decir, suavemente, que esos ejemplos se suelen quedar muy rápidamente desactualizados a las versiones más actuales del framework o a los usos y costumbres de los equipos de desarrollo y, por otro lado, los arquetipos, sobre todo de maven, no es que sean muy amigables de mantener.
También puede ocurrir que, por las propias características de seguridad de la información y/o por política corporativa, sobre todo en administraciones públicas, no tengamos acceso libre a internet y nos veamos en la necesidad de proporcionar una herramienta interna de generación de proyectos o scaffolding inicial.
Sea de una manera o de otra, en este tutorial vamos a exponer cómo podemos implementar nuestro propio Initializr adaptado a las necesidades de nuestros propios proyectos corporativos.
La ventaja de disponer de nuestro propio inicializador de proyectos es que el mantenimiento será mucho más sencillo, puesto que no es más que un artefacto a desplegar en nuestra propia infraestructura, adaptado a las necesidades y versiones soportadas de la arquitectura con la que estemos trabajando.
2. Entorno.
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 16′ (Apple M1 Pro, 32GB DDR4).
- Sistema Operativo: Mac OS Sonoma 14.6.1
3. Nuestro propio Initializr.
Lo primero que necesitamos es paradójico, puesto que empezaremos por un proyecto vacío generado con o en base a los ejemplos de los que disponga nuestro framework de desarrollo, sino disponemos de uno, podemos generar un proyecto sin dependencias usando en propio Spring Initializr.
Voy a proponer modularizar el proyecto como sigue:
- un módulo con la propia aplicación de Spring Boot y su configuración personalizada para el inicializador
- un módulo con la interfaz de usuario, y
- un módulo con el generador personalizado.
Y aquí vemos la primera carencia del Inicializr de Spring y es que no soporta la generación de un proyecto multi-módulo; la idea es que nosotros podamos personalizar esa generación como mejor nos convenga.
Este sería el contenido del pom.xml del proyecto parent.
<project ... >
<modelVersion>4.0.0</modelVersion>
<groupId>com.izertis.initializr</groupId>
<artifactId>izertis-initializr</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<description>Building our custom Spring Initializr</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<modules>
<module>izertis-initializr-generator</module>
<module>izertis-initializr-client</module>
<module>izertis-initializr-app</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-bom</artifactId>
<version>0.21.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
A continuación el pom.xml del módulo app:
<project ... >
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.izertis.initializr</groupId>
<artifactId>izertis-initializr</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>izertis-initializr-app</artifactId>
<description>Building our custom Spring Initializer - Application module</description>
<dependencies>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-web</artifactId>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator-spring</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Y, por último, nos aseguramos que la clase de aplicación esté bajo un paquete específico de aplicación, ya explicaremos un poco más adelante el por qué.
package com.izertis.initializr.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Sin más, ya podríamos ejecutar la aplicación y dispondríamos de una serie de apis rest que nos permiten acceder a los metadatos de configuración
http://localhost:8080/metadata/config
y al api de generación de proyectos:
curl -G http://localhost:8080/starter.zip -d "groupId=com.izertis.my-proyect&artifactId=my-proyect&javaVersion=17" -o my-proyect.zip
curl -G http://localhost:8080/starter.tgz -d "groupId=com.izertis.my-proyect&artifactId=my-proyect&javaVersion=17" -o my-proyect.tgz
El artefacto que se genera es el artefacto más básico para Spring Boot al que se le da soporte a través del módulo https://github.com/spring-io/initializr/tree/main/initializr-generator-spring
4. Personalizando el inicializador.
4.1. Configuración de la aplicación.
Lo primero que vamos a personalizar son los metadatos de la aplicación, esto es, la información de lo que soportaremos en la generación, que devolverá a través del api y que servirá para personalizar la interfaz de usuario. Para ello en el application.yml del módulo de la aplicación configuraremos la siguiente información:
spring:
main:
banner-mode: off
application:
name: izertis-initializr
initializr:
javaVersions:
- id: 17
- id: 21
default: true
languages:
- name: Java
id: java
default: true
packagings:
- name: Jar
id: jar
default: true
group-id:
value: com.izertis.application
artifact-id:
value: izertis-application
description:
value: change the project description as appropriate
packageName:
value: com.izertis.application
bootVersions:
- id: 3.3.3
name: 3.3.3
default: true
types:
- name: Maven
id: maven-project
description: Generate a Maven based project archive
action: "/starter.zip"
tags:
build: maven
format: project
default: true
dependencies:
- name: Izertis
content:
- name: logging
id: logging
groupId: com.izertis.logging
artifactId: izertis-starter-logging
description: This starter allows you to be compliant with the traceability framework.
- name: langChain4j
id: langChain4j
groupId: com.izertis.ai
artifactId: izertis-ai-langchain4j
description: Our own AI library based on LangChain4j
Se pueden personalizar los lenguajes, en función de lo que soporte nuestro framework (java, kotlin, groovy), las versiones de los lenguajes, el modo de empaquetación, el framework de construcción (maven o gradle) y, lo más interesante son las dependencias propias que podemos añadir como metadatos al initializr para que se muestren en las búsquedas a través de la interfaz de usuario.
Os invito a que echéis un vistazo a cómo está configurado el propio site de start.spring.io aquí https://github.com/spring-io/start.spring.io/blob/main/start-site/src/main/resources/application.yml#L166
Una vez configurado el soporte que vamos a proporcionar podemos desplegar y probar a invocar al endpoint de metadatos para confirmar que devuelve dicha información.
http://localhost:8080/metadata/config
4.2. Nuestro propio generador.
Como os anticipaba, quién genera realmente el código de la empaquetación, en función de la selección del usuario, es el módulo initializr-generator, podéis revisar el código puesto que es bastante interesante la estrategia que utilizan para soportar, en función de si es kotlin, java,… la generación de unos fuentes u otros o, en función de las dependencias seleccionadas, la generación de un contenido u otro en los ficheros de configuración.
Quizás es algo complejo, por el nivel de personalización que requiere y, en algunos casos se puede solucionar dicha generación condicional, de una manera más sencilla, usando plantillas de mustache.
Si queremos generar un proyecto totalmente personalizado, porque tenemos nuestro propio parent, la recomendación es eliminar la dependencia del módulo initializr-generator y trabajar en el nuestro propio.
A continuación el pom.xml del módulo app:
...
<artifactId>izertis-initializr-app</artifactId>
<description>Building our custom Spring Initializer - Application module</description>
<dependencies>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-web</artifactId>
</dependency>
<!--
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator-spring</artifactId>
</dependency>
-->
<dependency>
<groupId>com.izertis.initializr</groupId>
<artifactId>izertis-initializr-generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.izertis.initializr</groupId>
<artifactId>izertis-initializr-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
...
Ahora, en el módulo del generador, debemos añadir un fichero de factorías de Spring /src/main/resources/META-INF/spring.factories donde configuraremos contributors, que vienen a ser los @Configuration del contexto de Spring pero con un ámbito acotado al contexto de generación, esa es la razón de mantener el Application.java de la aplicación del propio Initializr en una paquetería distinta a la del generador para que no auto escanee las clases de configuración del generador en el contexto de Spring. El contexto de estas clases de configuración se reconstruye en cada generación de proyecto (es un ejemplo de prototipado o de cómo usar una estrategia de creación de beans en Spring no siendo singletons).
El contenido del fichero spring.factories quedaría como sigue:
io.spring.initializr.generator.project.ProjectGenerationConfiguration=\
com.izertis.initializr.generator.parent.build.ReadmeGeneratorConfiguration,\
com.izertis.initializr.generator.parent.build.MavenProjectGenerationConfiguration,\
com.izertis.initializr.generator.parent.scm.git.GitIgnoreGeneratorConfiguration,\
com.izertis.initializr.generator.app.build.MavenProjectGenerationConfiguration,\
com.izertis.initializr.generator.app.code.CodeConfiguration,\
com.izertis.initializr.generator.app.config.AppConfigGeneratorConfiguration,\
com.izertis.initializr.generator.app.spec.OpenApiGeneratorConfiguration,\
Como podéis intuir, hemos seguido el patrón de creación de ficheros del módulo de Spring en nuestra personalización, de modo tal que tenemos una configuración por cada fichero o grupo de ficheros a generar.
4.2.1. Contenido estático.
Ahora no tenemos más que implementar nuestros propios project contributors, veamos un ejemplo sencillo:
package com.izertis.initializr.generator.parent.scm.git;
import io.spring.initializr.generator.project.ProjectGenerationConfiguration;
import io.spring.initializr.generator.project.contributor.SingleResourceProjectContributor;
import org.springframework.context.annotation.Bean;
@ProjectGenerationConfiguration
public class GitIgnoreGeneratorConfiguration {
@Bean
public SingleResourceProjectContributor gitIgnoreContributor() {
return new SingleResourceProjectContributor(".gitignore",
"classpath:templates/parent/scm/git/git.ignore");
}
}
Si veis el patrón de generación del fichero de .gitignore del módulo de Spring del propio Initializr es bastante complejo porque en base a la tecnología seleccionada configuran el .gitignore para maven o gradle. En nuestro caso lo pongo como ejemplo de contributor de fichero estático, es decir, no vamos a personalizar el contributor por tecnología porque no damos soporte más que a una. Si vuestro caso es distinto os invito a que echéis un vistazo por aquí: https://github.com/spring-io/initializr/tree/main/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/scm/git.
Es interesante analizar el soporte de testing que proporciona el propio framework de initializr y nos podemos plantear implementar un test unitario como sigue:
package com.izertos.initializr.generator.parent.scm.git;
import io.spring.initializr.generator.project.MutableProjectDescription;
import io.spring.initializr.generator.project.contributor.SingleResourceProjectContributor;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class GitIgnoreGeneratorConfigurationTest {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(GitIgnoreGeneratorConfiguration.class);
@Test
void gitIgnoreIsContributedToProject(@TempDir Path directory) {
final MutableProjectDescription description = new MutableProjectDescription();
final Path projectDirectory = this.projectTester.withDirectory(directory).generate(description, (context) -> {
final SingleResourceProjectContributor contributor = context.getBean(SingleResourceProjectContributor.class);
contributor.contribute(directory);
return directory;
});
assertThat(projectDirectory.resolve(".gitignore")).isRegularFile();
}
}
Solo quedaría personalizar en el directorio de plantillas del módulo app src/main/resources/templates/parent/scm/git nuestro propio git.ignore, con una configuración a distribuir por defecto.
Es interesante tener en cuenta que la generación de ficheros puede condicionarse a las dependencias seleccionadas por el cliente, como en el siguiente caso; solo generaremos un ejemplo representativo de openapi.yml si el proyecto incluye la dependencia etiquetada como rest.
@ProjectGenerationConfiguration
@ConditionalOnRequestedDependency("rest")
public class OpenApiGeneratorConfiguration {
4.2.2. Contenido basado en plantillas.
A continuación un ejemplo de código basado en una plantilla de mustache.
El módulo de Spring, por defecto, añade un application.properties; si preferimos un formato yaml no tenemos más que añadir un contributor como el siguiente:
@ProjectGenerationConfiguration
public class AppConfigGeneratorConfiguration {
@Bean
public ProjectContributor applicationYamlContributor(ProjectDescription projectDescription, TemplateRenderer templateRenderer) {
return new ProjectContributor() {
@Override
public void contribute(Path projectRoot) throws IOException {
Files.createDirectories(projectRoot.resolve(projectDescription.getArtifactId() + "-app/src/main/resources/config/"));
final Path file = Files.createFile(projectRoot.resolve(path + "/application.yml"));
try (final PrintWriter writer = new PrintWriter(Files.newBufferedWriter(file))) {
final Map<String, Object> model = new HashMap<>();
model.put("projectDescription", projectDescription);
writer.println(templateRenderer.render("app/config/application", model));
}
}
};
}
Y en el directorio src/main/resources/templates/app/config/ un fichero application.mustache con un contenido como el siguiente:
info:
spring:
application:
name: {{projectDescription.artifactId}}
Siguiendo con el soporte de tests unitarios, podríamos implementar un test como el siguiente:
class AppConfigGeneratorConfigurationTest {
@Test
void appConfigAreContributedToProject(@TempDir Path directory) {
final MutableProjectDescription projectDescription = new MutableProjectDescription();
projectDescription.setBuildSystem(new MavenBuildSystem());
projectDescription.setGroupId("com.izertis.qa.automator");
projectDescription.setArtifactId("qa-automator");
final ProjectAssetTester projectAssetTester = new ProjectAssetTester().withConfiguration(AppConfigGeneratorConfiguration.class);
projectAssetTester.withDirectory(directory)
.generate(projectDescription, (context) -> {
context.getBeansOfType(ProjectContributor.class).forEach((name, contributor) -> {
try {
contributor.contribute(directory);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return directory;
});
assertThat(directory.resolve(directory.resolve("qa-automator-app/config/application.yml")))
.isRegularFile()
.content().contains("name: qa-automator");
}
}
4.2.3. Generación de los ficheros de build.
Os recomiendo que exploréis directamente cómo lo hacen en el módulo de Spring aquí: https://github.com/spring-io/initializr/blob/main/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/build/maven/MavenProjectGenerationConfiguration.java y que trasladéis la estrategia que mejor os convenga.
5. Nuestra propia interfaz de usuario.
Por último, nos quedaría personalizar nuestra propia interfaz de usuario en el módulo client; aunque podríamos prescindir de ella si usamos el soporte para Inititializr del IDE.
Para ello, podemos construir nuestra propia interfaz de usuario en la tecnología corporativa o «inspirarnos» en el módulo https://github.com/spring-io/start.spring.io/tree/main/start-client, personalizando la interfaz con la imagen corporativa, haciendo nuestro dicho código.
Es cierto que ese módulo web no ha sido construido con la idea de su reutilización, de hecho, avisan de ello en el README.md del propio proyecto, pero con un par de retoques, nos puede quedar algo como lo que sigue:
6. Referencias.
- https://github.com/spring-io/initializr
- https://github.com/spring-io/start.spring.io
- https://github.com/alibaba/cloud-native-app-initializer/
7. Conclusiones.
En este tutorial hemos analizado la posibilidad de disponer de nuestro propio inicializador de proyectos, basado en el de Spring Boot, instalado en nuestros sistemas y también hemos visto la posibilidad de personalizar tanto el contenido de los artefactos generados como la interfaz de usuario a distribuir.