Si usaste alguna vez Spring Boot, seguramente te preguntaste, "¿cómo funciona Spring Boot?". O siendo un poquito mas específicos, "¿cómo sabe Spring que tiene que instanciar ciertos beans con ciertas configuraciones que puse en el archivo de properties?". Lo que busco que te lleves en este tutorial es no solo cómo crear un starter, sino también entender un poco más sobre qué esta pasando cuando usás uno de los starters ya existentes (como spring-boot-web-starter o spring-boot-thymeleaf-starter).

Si no sabes como usar Spring (¡o para que sirve este framework Java!), te recomiendo que te pases por el curso de Introducción al desarrollo Java EE 7 y después vuelvas para acá, porque voy a hablar de algunas cosas que capaz vayas a entender mejor si alguna vez lo usaste.

El porqué de los Starters

Cuando trabajamos con proyectos complejos, es inevitable terminar usando muchas librerías. Incluso aunque sea un proyecto chico, seguro vas a terminar usando alguna dependencia. Ahora imaginate que tuvieses que buscar todas esas dependencias cuando crees un nuevo proyecto, además de configurarlas. Creo que esta claro el problema ¿no? Los Starters de Boot fueron creados para resolver esta problemática. Nos abstraen de las dependencias que tenemos en nuestro proyecto y como configurarlas, ya que en la mayoría de los casos, estas se configuran siempre de la misma manera (Un datasource siempre va a usar una url de conexión, un usuario, una contraseña...)

Preparando el proyecto

Lo primero que tenes que hacer es descargar el proyecto de ejemplo que prepare para esta guía. Vas a ver que tiene varios submodulos, los cuales definí en el pom que hay en la carpeta padre de los tres modulos. También tenés ya todo como para empezar a trabajar (no quiero que pierdas tiempo leyendo y copiando poms para inicializar el proyecto, me interesa más que los entiendas). Por último en el mismo repositorio te deje otro pequeño proyecto que hace las suertes de "Hola Mundo!". Este va a ser la dependencia de la cual vamos a hacer un starter. Sin más, ¡empecemos!

El proyecto de autoconfiguracion

Entendiendo el Pom parent

Lo primero y antes de tirarnos a hacer nuestro Starter, es entender que es un Parent Pom, o en otras palabras, el pom que esta en el directorio spring-boot-starter-saludador. Un Parent Pom es simplemente un pom, pero difiere del de un proyecto individual porque define dos secciones mas, que son las que van a facilitarnos la administracion de nuestro proyecto. La seccion modules nos permite definir las partes de nuestro proyecto, para que Maven, a la hora de ejecutar sus comandos sobre este proyecto padre, sepa cuales son los modulos donde va a tener que ejecutarlo tambien (por ejemplo, si haces un clean and build sobre el proyecto padre, Maven va a ejecutar tambien ese clean and build sobre los hijos) Tambien tenemos la seccion de dependencyManagement que nos permite centralizar toda la informacion referida a las dependencias de nuestro proyecto. En proyectos donde solo es un modulo el que lo compone, es sencillo esto, pero en los casos de proyectos multimodulos (como en nuestro caso) esto puede ser bastante molesto.

Paso 1: SaludadorProperties

Como bien su nombre lo indica, este proyecto se va a encargar de hacer toda la configuración automática de nuestra dependencia. Pero lo primero que vamos a necesitar es saber que valores necesita la librería para inicializarse. Para esto, vamos a ver nuestro proyecto Saludador y vamos a ver que tiene dos valores que se asignan al momento de crear una instancia de Saludador. Estos atributos son los que queremos configurar por medio de un archivo de propiedades, por lo cual vamos a crear una clase en nuestro proyecto de autoconfiguracion que las represente. Esta clase no va a alcanzar para mapear las properties por si sola, pero por suerte, Spring nos da herramientas para hacerlo. Podemos acceder a los valores de un archivo properties de 3 maneras:

Con @Value permite inyectar 1 propiedad a una variable

Con Enviroment (una abstraccion del ambiente sobre el cual corre nuestra aplicacion)

Con @ConfigurationProperties, el cual podemos usar para asignar las propiedades a un objeto compuesto.

Esta última es la que vamos a utilizar en este caso. Obviamente, y como no sabemos leer mentes no podemos predecir el nombre de la propiedad que quiere poner el usuario de nuestro Starter. Por esto, Spring supone que el nombre de la propiedad es el nombre del atributo de la clase (en tu caso, si tenes un atributo nombre, Spring va a buscar una propiedad cuya clave es nombre). ¿Pero qué pasa si justo elegimos un nombre muy comun?. ConfigurationProperties tiene un atributo para poder configurar con que prefijo debe de buscarse la propiedad. Llendo a un ejemplo más concreto, te voy a mostrar como quedó mi clase de configuración.

@ConfigurationProperties(prefix = "com.dosideas.saludador")
public class SaludadorProperties {

    private String hola;
    private String nombre;

    public String getHola() {
        return hola;
    }

    public void setHola(String hola) {
        this.hola = hola;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
}

¡Listo! Con esto alcanza para que Spring sepa como transformar tu archivo de propiedades a tu clase. Próximo paso... Definamos un Bean de Saludador.

Paso 2: SaludadorAutoConfiguration

Viene el momento de la verdad, esta clase es la que va a terminar haciendo la magia, o capaz ya no sea tan magia al final de todo esto ¿no?

¿Cuál es la idea de esta clase? Es la encargada de decirle a Spring "Acá tenes un bean de esta clase, asique si te piden inyectar una instancia de esta clase, usa este".

Vamos a usar tres anotaciones a nivel de clase.

@Configuration: Indica que la clase va a declarar uno más métodos anotados con @Bean, y que estos deben ser procesados por Spring para crear las definiciones y servicios para estos beans en tiempo de ejecución.

@ConditionalOnClass: activa la configuración solo si las clases indicadas se encuentran en el classpath.

@EnableConfigurationProperties: habilita el soporte para las clases anotadas con@ConfigurationProperties

Tambien vamos a tener que crear un atributo a nuestra clase que va a ser del tipo properties que creamos en el paso anterior, y anotarlo con @Autowired (para que Spring inyecte una instancia del mismo).

Además, y te recomiendo que lo hagas, crees un atributo logger, para que puedas darle información al usuario si es que falla al inicializarse nuestro bean.

Por último, y no menos importante, vamos a crear un método que se va a encargar de devolver la instancia de Saludador. En mi caso, también le agregue validaciones para evitar que se use sin las propiedades ingresadas. Este método va a estar anotado con dos anotaciones muy importantes, si queremos que funcione correctamente:

@Bean: Esta anotación indica que el método devuelve un bean que debe ser manejado por Spring

@ConditionalOnMissingBean: Nuevamente una condición, en este caso, sirve para que Spring solo use este bean, en caso de que el usuario no cree uno nuevo en su código, en dicho caso, Spring va a preferir el del usuario antes que el del Starter

Si todo fue bien, la clase debería quedarte algo como esto:

@Configuration
@ConditionalOnClass({Saludador.class})
@EnableConfigurationProperties(SaludadorProperties.class)
public class SaludadorAutoConfiguration {

    private static Log log = LogFactory.getLog(SaludadorAutoConfiguration.class);

    @Autowired
    private SaludadorProperties saludadorProperties;

    @Bean
    @ConditionalOnMissingBean
    public Saludador saludador() {
        if (saludadorProperties.getHola() == null || saludadorProperties.getNombre() == null) {
            log.error("Las propiedades para Saludador no estan configuradas correctamente");
            throw new RuntimeException("Las propiedades para Saludador no estan configuradas correctamente");
        }
        return new Saludador(saludadorProperties.getHola(), saludadorProperties.getNombre());
    }
}

spring.factories

Este archivo es lo último que necesitamos en este proyecto. El mismo debe ubicarse en la carpeta META-INF dentro de resources. En el mismo vamos a agregar la propiedad org.springframework.boot.autoconfigure.EnableAutoConfiguration, siendo su valor el nombre (con el paquete) de nuestra clase. Esto es lo que va a indicarle a Spring que clase debe usar como clase de AutoConfiguracion. En mi caso me quedó esto:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.dosideas.spring.boot.autoconfigure.SaludadorAutoConfiguration

Recapitulemos

Hasta ahora vimos como configurar el primer proyecto. Vimos las anotaciones necesarias (¡que no son las únicas!) y vimos que archivos hacen falta. Definimos el bean que queremos que viva en el contexto de Spring, y lo configuramos con las properties gracias a nuestra clase que las representa.

Con esto concluye la parte "dificil" de un starter. Si, solo con esto. La verdad es que lo complejo de los starters de Spring boot, es el módulo de autoconfiguracion, ya que implica definir todos los beans necesarios, las clases para las properties, etc.

Paso 3: El Starter

El Starter de Spring Boot, o mejor dicho el proyecto que llamamos Starter consiste en un archivo que indica que librerías provee. Para lograr esto en nuestro Starter, vamos a ir a la carpeta META-INF, nuevamente ubicada en la carpeta Resources del proyecto. Ahí vamos a crear un archivo llamado spring.provides, y allí vamos a definir cual es la libreria que el starter va a proveer, en este caso el proyecto donde se encuenta nuestro Saludador, es decir ProyectoPrueba. En el archivo te va a quedar esto:

provides: ProyectoSaludador

¡Y listo! Ya podes usar tu starter como cualquier otro de Spring Boot. Igualmente, para que puedas ver que esta funcionando, ya te deje un proyecto donde este se esta usando el starter, en el mismo repositorio que estuvimos usando.

En el pom de este proyecto puse la dependencia del Starter, que en mi caso fue

<dependency>
    <groupId>com.dosideas</groupId>
    <artifactId>saludador-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependency>

Tambien tenemos que definir las propiedades que hayamos elegido en un archivo de properties, para que Spring vaya a buscarlas a la hora de crear el Bean. Y finalmente haciendo simplemente un @Autowired sobre un atributo del tipo Saludador, empezar a usarlo. Te dejo el ejemplo de como seria esto.

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

    @Autowired
    private Saludador saludador;
    
	@Test
	public void contextLoads() {
            System.out.println(saludador.saludar());
            Assert.assertEquals("Hola Eduardo", saludador.saludar());
	}

}

Conclusiones

Aunque este fue un caso muy sencillo, podemos ver que los starters no son cosas que salen del conocimiento que podemos tener cualquier usuario de Spring. Son módulos muy sencillos y a la vez muy prácticos. También pudimos ver porque podemos sobrescribir un Bean que proveen los Starters de Boot. Ahora solo queda buscar alguna, o algunas, dependencias que estes configurando en varios proyectos a mano, y hacer la prueba.

Inspiración.

"Si tú tienes una manzana y yo tengo una manzana e intercambiamos las manzanas, entonces tanto tú como yo seguiremos teniendo una manzana cada uno. Pero si tú tienes una idea y yo tengo una idea, e intercambiamos las ideas, entonces ambos tendremos dos ideas"

Bernard Shaw