Cambiar el comportamiento de la aplicación con configuración, anotaciones y condicionales en Spring Boot

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Aparte de un sistema de configuración muy flexible para proporcionar configuración de diferentes fuentes, Spring permite cambiar el comportamiento de la aplicación en base a los valores resueltos de las propiedades de configuración. Mediante configuración y sin realizar cambios en el código fuente el contenedor de inversión de dependencias determina las instancias y sus dependencias que crea.

Java

Spring

Programar sobre interfaces permite crear varias implementaciones diferentes y cambiar de una a otra sin cambiar ninguna de las clases que usan la interfaz. Una implementación puede ser para persistir o recuperar datos de una base de datos relacional como PostgreSQL o usando la misma interfaz guardar y recuperar los datos en una base de datos NoSQL como MongoDB, otro ejemplo es una interfaz de almacenamiento para guardar el contenido de un archivo en el sistema de archivos local o cambiando la implementación guardar los archivos en un sistema de almacenamiento en al nube.

Aunque los diferentes entornos de la aplicación en que se ejecuta son conocidos sigue siendo útil programar sobre interfaces, quizá en el entorno de producción se usa un almacenamiento en la nube pero en tiempo de desarrollo no es posible y es más fácil usar una implementación para guardar el contenido en el sistema de archivos local utilizando la misma interfaz que representa el sistema de almacenamiento. Con una interfaz la implementación queda encapsulada y cada sistema de almacenamiento tendrá la suya. También es útil para empezar a desarrollar con una implementación sencilla en memoria o en el sistema de archivos local y una vez en el despliegue en un entorno de pruebas o producción crear nuevas implementaciones de las interfaces adecuados para esos entornos.

Con varias implementaciones de las interfaces es necesario seleccionar de alguna forma que implementación en concreto a usar de entre las disponibles. En el entorno local se usará la implementación en memoria o el sistema de archivos local cuando en el entorno de producción se usará el almacenamiento en el sistema de almacenamiento de objetos del proveedor de la nube, en Amazon es S3 y en Google Cloud es Storage. La selección de la implementación a usar es recomendable realizarla en base a configuración en tiempo de ejecución con la intención de evitar de que solo cambie la configuración y no el código de modo que el mismo artefacto ejecutable sea el mismo en todos los entornos.

El framework Spring proporciona un mecanismo muy flexible para proporcionar y sobrescribir la configuración de la aplicación con varias fuentes de propiedades y una prioridad entre ellas del que coger el valor en caso de que en alguna esté presente la propiedad, ya sea en un archivo dentro del artefacto ejecutable, en un archivo externo, mediante propiedades del sistema o variables de entorno entre otras formas.

Anotaciones y condicionales para crear beans

Una de las funcionalidades proporcionadas por Spring es el contenedor de beans que realiza la construcción de las instancias mediante la inyección de dependencias. Mediante la inyección de dependencias la aplicación ya no es responsable de realizar la creación de la instancias sino que esta tarea es delegada al contenedor de beans que es capaz de elegir las implementaciones según diferentes condiciones y valores. A través del los perfiles de inicio de la aplicación, valores de configuración, de anotaciones y mediante un lenguaje de expresiones el contenedor de beans crea las instancias de las implementaciones de las interfaces deseadas según la configuración proporcionada a la aplicación en tiempo de ejecución.

En Spring los beans se definen con las siguientes anotaciones:

  • @Bean: se aplica sobre un método e indica que el método devuelve una instancia del tipo de retorno para que el contenedor la proporcione como dependencia a otra instancia que tenga como dependencia la del tipo devuelto.
  • @Component: permite definir una clase para que el contenedor cree una instancias de la clase y procese sus anotaciones cuando necesite proporcionar una instancia de la clase como dependencia.
  • @Controller, @Service, @Repository: estas anotaciones son especializaciones más semánticas de la anotación Component. Tienen el mismo comportamiento que la anotación Component.

En los sitios en los que se pueden aplicar las anotaciones anteriores es posible aplicar las siguientes anotaciones para que las instancias de esos beans se añadan al contenedor de forma condicional. Además de las anotaciones condicionales más importantes indicadas en esta lista en el paquete org.springframework.boot.autoconfigure.condition hay unas cuantas más.

  • @Profile: en caso de que el perfil indicado coincida con alguno de los perfiles de la aplicación el bean se añade al contenedor.
  • @ConditionalOnProperty: crea la instancia de forma condicional en base a una propiedad de configuración.
  • @ConditionalOnBean: crea la instancia de forma condicional en base a la existencia de una instancia de una interfaz.
  • @ConditionalOnClass: crea la instancia de forma condicional en base a la existencia de una clase.
  • @ConditionalOnMissingBean: crea la instancia de forma condicional cuando no existe de una instancia de una interfaz.
  • @ConditionalOnExpression: mediante un lenguaje de expresiones es posible crear condiciones complejas con las que decidir si una instancia de un bean se crea o no.
  • @ConditionalOnResource: crea una instancia de forma condicional cuando existe un recurso en el classpath.
  • @Primary: en caso de que a través de varias anotaciones sea posible construir una instancia con la misma interfaz este método permite eliminar la ambigüedad seleccionado la instancia donde se aplica esta anotación. Solo es posible aplicar la anotación Primary sobre la misma interfaz.

Además de las anteriores anotaciones proporcionadas por Spring es posible crear anotaciones condicionales propias implementando una clase de la interfaz Condition y usando la anotación Conditional.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.springbootconfigconditional;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

import io.github.picodotdev.blogbitix.springbootconfigconditional.conditional.OnLinuxCondition;
import io.github.picodotdev.blogbitix.springbootconfigconditional.conditional.OperatingSystem;

@Component
public class Beans {

    @Bean
    @Conditional(OnLinuxCondition.class)
    OperatingSystem linuxSystemBean() {
        return new OperatingSystem("Linux");
    }

    @Bean
    @ConditionalOnMissingBean(value = OperatingSystem.class)
    OperatingSystem windowsSystemBean() {
        return new OperatingSystem("Other");
    }
}
Beans.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.blogbitix.springbootconfigconditional.conditional;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class OnLinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return System.getProperty("os.name").equals("Linux");
    }
}
OnLinuxCondition.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springbootconfigconditional.conditional;

public class OperatingSystem {

    private String name;

    public OperatingSystem(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
OperatingSystem.java

Ejemplo de beans condicionales de con Spring Boot

En este pequeño ejemplo de aplicación se definen dos implementaciones de una interfaz. En base al valor de una propiedad de configuración proporcionada por configuración y con la posibilidad de cambiar su valor en tiempo de ejecución con una variable de entorno se selecciona la implementación deseada utilizando las anotaciones condicionales.

La interfaz del ejemplo simplemente tiene un método que devuelve un mensaje según el idioma que le corresponde.

1
2
3
4
5
6
package io.github.picodotdev.blogbitix.springbootconfigconditional.service;

public interface Message {

    String get();
}
Message.java

Creando varias implementaciones de la interfaz de ejemplo cada implementación devuelve el mensaje en un idioma diferente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springbootconfigconditional.service;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty(prefix = "app.message", name = "implementation", havingValue = "spanish")
public class SpanishMessage implements Message {

    @Override
    public String get() {
        return "¡Hola mundo!";
    }
}
SpanishMessage.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springbootconfigconditional.service;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty(prefix = "app.message", name = "implementation", havingValue = "english")
public class EnglishMessage implements Message {

    @Override
    public String get() {
        return "Hello World!";
    }
}
EnglishMessage.java

Cambiando el valor de la propiedad Spring decide que implementación usar e inyectar en las dependencias. De esta forma el comportamiento de la aplicación cambia no cambiando el código sino cambiando la configuración de la aplicación y sin tener que implementar la lógica condicional mediante código propio sino usando las facilidades proporcionadas por Spring.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.github.picodotdev.blogbitix.springbootconfigconditional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import io.github.picodotdev.blogbitix.springbootconfigconditional.conditional.OperatingSystem;
import io.github.picodotdev.blogbitix.springbootconfigconditional.service.Message;

@SpringBootApplication
public class Main implements CommandLineRunner {

    @Autowired
    private Message message;

    @Autowired
    private OperatingSystem os;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("Operating system: " + os.getName());
        System.out.println("Message: " + message.get());
    }

    public static void main(String[] args) throws Exception {
      SpringApplication.run(Main.class, args);
    }
}
Main.java
1
2
3
4
5
6
7
spring:
  profiles:
    default: "local"

app:
  message:
    implementation: "spanish"
application.yml

En función del valor de la propiedad app.message.implementation proporcionado en la configuración y los argumentos del programa la implementación seleccionado por spring cambia y a su vez el comportamiento del programar sin necesidad de cambiar código.

1
2
$ ./gradlew run

gradlew-run.sh
1
2
Operating system: Linux
Message: ¡Hola mundo!
gradlew-run.out
1
2
$ ./gradlew run --args="--app.message.implementation=spanish"

gradlew-run-spanish.sh
1
2
Operating system: Linux
Message: ¡Hola mundo!
gradlew-run-spanish.out
1
2
$ ./gradlew run --args="--app.message.implementation=english"

gradlew-run-english.sh
1
2
Operating system: Linux
Message: Hello World!
gradlew-run-english.out
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: