20 de marzo de 2019

Spring Boot Parte 2 - Externalizando la Configuración de la Aplicación



Algo muy importante y fácil de olvidar cuando creamos una aplicación, es que (si todo sale bien), esta vera la luz en un mundo productivo y será ahí cuando comenzará su vida útil, en un mundo fuera de la computadora del desarrollador.

Antes de llegar a su destino final (el ambiente de producción) una aplicación sana debería pasar por al menos dos ambientes. El primero es el ambiente del desarrollador, la computadora o computadoras en las que nunca nada falla ni sale mal :). El segundo ambiente debería ser el ambiente de integración continua; aquí será una computadora configurada en la que la aplicación se compilará y se ejecutarán una serie de pruebas automáticas que van desde las pruebas funcionales, hasta pruebas de carga y estrés. El tercer ambiente es un ambiente de testing, aquí un profesional altamente capacitado (el tester) hará su mayor esfuerzo por hacer que nuestra aplicación falle o "se rompa" en formas que nunca imaginamos al momento de escribir nuestro código productivo o de pruebas unitarias.

Una vez que nuestra aplicación sobrevive en los ambientes anteriores, llegará finalmente al ambiente productivo. En este ambiente es donde pasará su vida útil sirviendo a usuarios que no estuvieron involucrados dentro del proceso de construcción.
Como podemos ver, nuestra aplicación pasara a través de varios ambientes, algunos de los cuales no tendremos control y serán distintos al ambiente creado dentro de las máquinas de desarrollo. Debido a esto es muy importante tener un mecanismo que nos permita externalizar la configuración de nuestras aplicaciones Spring Boot y que se ejecute de la misma forma sin importar el ambiente en el que se encuentre. Algunos elementos que comúnmente cambian dependiendo del ambiente en el que ejecutemos la aplicación son: rutas, usuarios de conexiones a base de datos, puertos de funcionamiento, nombres de servidores, etc.

En este tutorial aprenderemos las formas que Spring Boot proporciona para colocar esta información de configuración fuera del código fuente de la aplicación (evitando con esto que tengamos que recompilarla cada vez que vayamos a moverla de ambiente), y las prioridades o precedencias que Spring Boot asigna a cada una de estas formas.

Para realizar esta configuración Spring Boot proporciona varios mecanismos como son: archivos de propiedades, archivos YAML, variables de entorno y argumentos de líneas de comando. El externalizar esta configuración es una muy buena práctica, ya que evita que tengamos que recompilar nuestra aplicación cada vez que cambiemos de ambiente; hacer esta nueva configuración será tan fácil como agregar una nueva opción dentro de un archivo, indicar que se usará un perfil de aplicación diferente, o tener un valor distinto en una variable de entorno. Nuestra aplicación debe estar preparada para leer estas fuentes de configuración en vez de tener los valores escritos directamente en el código fuente de la misma.


Lista de Precedencias de Fuentes de Configuración en Spring Boot

Spring Boot leerá cada uno de los valores de configuración de distintas fuentes, que se muestran a continuación, en el orden que están indicados. Esto quiere decir que, si colocamos el mismo valor en dos de las fuentes el valor de la fuente que se encuentra más abajo en la lista sobreescribirá el valor de la fuente que se encuentra más arriba. Spring Boot lee los valores de los elementos de configuración en el siguiente orden:
  1. Valores por default de las propiedades ( "SpringApplication.setDefaultProperties").
  2. Anotaciones "@PropertySource" en clases decoradas con "@Configuration".
  3. Valores en el archivo "application.properties" o archivo "application.yaml" que se encuentren dentro el jar de nuestra aplicación.
  4. Valores en el archivo "application.properties" o archivo "application.yaml" que se encuentren fuera del jar de la aplicación.
  5. Propiedades específicas al perfil de la aplicación que se encuentren en archivo de propiedades o YAML dentro del jar de la aplicación ("application-{profile}.properties").
  6. Propiedades específicas al perfil de la aplicación que se encuentren en archivo de propiedades o YAML fuera del jar de la aplicación ("application-{profile}.properties").
  7. Variables de entorno del Sistema Operativo.
  8. Propiedades Java de Sistema (las que se obtienen con "System.getProperties()").
  9. Atributos JNDI de "java:comp/env".
  10. Parámetros de inicialización en "ServletContext".
  11. Parámetros de inicialización en "ServletConfig".
  12. Propiedades de "SPRING_APPLICATION_JSON" (un JSON embebido en una variable de entorno o propiedad del sistema).
  13. Argumentos de línea de comandos.
  14. Atributos "properties" de nuestras pruebas.
  15. Anotaciones "@TestPropertySource" en nuestras pruebas.
  16. Propiedades de configuración globales de las Devtools en nuestro directorio home ("~/.spring-boot-devtools.properties") cuando devtools está activo.
Como vemos Spring Boot tiene muchas fuentes de propiedades, y el conocer este orden es importante ya que si queremos sobreescribir el valor de una fuente (como el archivo "application.properties") solo deberemos escribirlo en una fuente que se encuentre más abajo en la lista (como una variable de ambiente).

Para no hacer el tutorial largo y aburrido, veremos algunas de estas fuentes en este tutorial y algunas otras las dejaremos para otros a lo largo de esta serie.

Comencemos pues con el código.

Lo primero que haremos será crear un proyecto usando el sitio de Spring Initializr (que por cierto acaba de actualizarse hace unos días) justo como lo hicimos en el primer tutorial de la serie. En mi caso colocaré la siguiente configuración, donde la parte más importante es que seleccionaré como dependencias "Web" (igual que en el primer tutorial de la serie) y "Lombok" que es una herramienta para simplificar la escritura de código y que vimos en el último tutorial.



Hacemos clic en el botón "Generate Project", descargamos el zip del proyecto y lo importamos en nuestro IDE, en mi caso Eclipse. Una vez que nuestro código esté en el IDE podemos comenzar.

Lo primero será comprobar que efectivamente la precedencia anterior es correcta. Spring Boot tiene básicamente tres formas de inyectar los valores de las propiedades en nuestros beans; la primera es usando la anotación "@Value" en la propiedad en la que queremos que quede el valor del parámetro; la segunda es acceder al valor a través del objeto "Environment" de Spring; y la tercera es usando algo llamado "objetos estructurados" usando la anotación "@ConfigurationProperties". Para mantener las cosas simples iniciaremos inyectando las propiedades con "@Value" y al final del tutorial veremos las otras dos formas.

Lo primero será crear los paquetes con los que estaremos trabajando, crearemos dos paquetes, un paquete para las clases de configuración ("config") y un paquete con los controladores ("controllers"), estos últimos serán los componentes de Spring que usarán los valores de las propiedades inyectadas. Crearemos estos dos paquetes debajo del paquete principal "com.javatutoriales.springboot.configuracion", de la siguiente forma:



Dentro del paquete "config" crearemos una clase llamada "EjemploConfig".

En la lista de precedencias podemos ver que la primera son los valores por default de Spring Boot y la segunda son los valores que provengan de anotaciones "@PropertySource" en clases decoradas con "@Configuration"; esto será lo que probaremos.


Configurando con @PropertySource

Decoramos nuestra clase con la anotación "@Configuration", esta le dice al contenedor de beans de Spring que esta clase contiene elementos de configuración, y que por lo tanto debe procesarla antes de iniciar la aplicación. También decoraremos la clase con la anotación "@PropertySource", esta anotación es un complemento de la anterior, y le indica a Spring en dónde se encuentran los valores que se usarán, lo normal es que estos se encuentren en un archivo, ya sea dentro del jar de nuestra aplicación o en algún directorio de la computadora en la que se ejecutará la aplicación. Nosotros indicamos que el archivo se encuentra en el classpath de la aplicación, en un archivo llamado "tutorial.properties" dentro de un directorio llamado "config", este último debe estar en el directorio "src/main/resources" de nuestra aplicación, ya sea que usemos Gradle o Maven:



Aprovechemos para poner el siguiente contenido en el archivo, que indica que el valor de la propiedad "demo.valor" es "Mundo":

demo.valor = Mundo

Hasta ahora nuestra clase se ve de la siguiente forma:

package com.javatutoriales.springboot.configuracion.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:config/tutorial.properties")
public class EjemploConfig {

}

Lo siguiente será colocar la propiedad que leeremos. Comenzaremos con algo sencillo, leeremos el valor de una cadena de una propiedad que llamaremos "demo.valor", para indicar que esta es la propiedad que queremos leer la colocaremos en la anotación "@Value" en el atributo en el que se colocará el valor, de la siguiente forma:

@Value("${demo.valor}")
private String valor;

Para terminar con esta parte aprovecharemos que estamos usando Lombok para agregar un getter del valor usando la anotación "@Getter". Nuestra clase completa queda de la siguiente forma:

package com.javatutoriales.springboot.configuracion.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import lombok.Getter;

@Configuration
@PropertySource("classpath:config/tutorial.properties")
@Getter
public class EjemploConfig {
	@Value("${demo.valor}")
	private String valor;
}

Lo siguiente será crear el componente que hará uso de "EjemploConfig". En nuestro caso ese componente será un controlador de Spring MVC, que nos permitirá regresar los valores que lea como respuesta de las peticiones del usuario, esto nos ayudará a ver rápidamente el resultado de los cambios de este valor.

Creamos una clase llamada "DemoController" dentro del paquete "controllers" de nuestra aplicación:



Decoraremos esta clase con la anotación "@RestController" para indicar que este componente es un controlador de Spring MVC y luego colocaremos una propiedad de tipo "EjemploConfig", la cual será inyectada por Spring usando un setter que colocaremos de forma automática con Lombok. Para indicarle a Spring que debe inyectar la instancia de "EjemploConfig" usando inyección de setter (esto es una buena práctica que ayuda a hacer más eficiente el tiempo de arranque de las aplicaciones y ayuda al momento de la creación de pruebas unitarias), decoraremos dicho setter con la anotación "@Autowired", como aprendimos a hacerlo en el tutorial de Lombok. Hasta ahora, nuestra clase se ve de la siguiente forma:

@RestController
public class DemoController {
    @Setter (onMethod=@__(@Autowired))
    private EjemploConfig config;
}

El último paso es agregar nuestro método manejador de peticiones, el cual leerá el valor del parámetro y lo regresará en la respuesta a la petición hecha por el usuario. Este será un manejador muy simple:

@GetMapping
public String saluda() {
    return "Hola " + config.getValor();
}

Nuestra clase completa queda de la siguiente forma:

@RestController
public class DemoController {
    @Setter (onMethod=@__(@Autowired))
    private EjemploConfig config;
	
    @GetMapping
    public String saluda() {
        return "Hola " + config.getValor();
    }
}

Ahora que ya tenemos el código, el siguiente paso será ejecutar nuestra aplicación como una aplicación Java (Alt + Shift + X, J), con lo que se deberá iniciar la aplicación y debemos ver el banner de Spring Boot en la consola de Eclipse:



Si ahora entramos en la siguiente dirección:

http://localhost:8080/

Debemos ver esta salida:



Con esto podemos saber que le valor se leyó del archivo de manera correcta. El código que acabamos de escribir es el que usaremos prácticamente para el resto del tutorial.


Configurando con el Archivo application.properties Dentro de la Aplicación

Ahora veremos el siguiente elemento dentro de la precedencia de la configuración, el tercero de la lista anterior, los valores en el archivo "application.properties". Este es el archivo de configuración por defecto de Spring Boot, es aquí donde normalmente colocaremos los parámetros de configuración de nuestra aplicación. Spring busca este archivo en las siguientes ubicaciones (en el orden mostrado):
  1. Un subdirectorio "/config" dentro del directorio en el que se ejecuta la aplicación.
  2. El directorio en el que se ejecuta la aplicación.
  3. Un paquete "/config" en el classpath de la aplicación.
  4. La raíz del classpath de la aplicación.
Cuando creamos nuestra aplicación usando Spring Initializr, se creó este archivo ubicado en la raíz del classpath de la aplicación (el número 4 en la lista anterior):



Abriremos este archivo, que en este momento debe estar vacío, y colocaremos el siguiente contenido:


demo.valor = desde application.properties dentro del jar de la aplicación

Estamos colocando el mismo nombre de la propiedad, pero un valor diferente, con esto buscamos que este sea el valor que se muestre ahora como resultado de nuestra petición.

Volvemos a ejecutar nuestra aplicación y entramos nuevamente a la siguiente dirección:

http://localhost:8080/

Con esto veremos la siguiente pantalla:



Como podemos ver, sin tener que modificar el código de la aplicación pudimos sobreescribir el parámetro "demo.valor".


Configurando con el Archivo application.properties Fuera de la Aplicación

El siguiente ejemplo de nuestros niveles de precedencia será el uso de un archivo "application.properties" que se encuentre fuera del jar de la aplicación. Para esto será necesario crear el jar ejecutable de Spring Boot como lo hicimos en el tutorial anterior de la serie. Nuevamente, crear este jar ejecutable es muy sencillo con Gradle; lo único que debemos hacer es ir al panel de Gradle Tasks, encontrar el grupo de tareas "build", expandirlo, encontrar la tarea "bootJar", hacer clic derecho sobre la tarea, y seleccionar la opción "Run Gradle Task":



Vamos al archivo en el que se genero nuestro jar ("build\libs") y creamos un archivo llamado "application.properties":



Colocamos el siguiente contenido dentro del archivo:

demo.valor = desde application.properties fuera del jar de la aplicación

Y ejecutamos nuestra aplicación desde una terminar con el siguiente comando:

java -jar configuracion-0.0.1-SNAPSHOT.jar




Configurando con un Archivo de Perfil Específico fuera de la Aplicación

Aprovechemos para mostrar el elemento 6 de la lista, propiedades específicas al perfil de la aplicación que se encuentren en archivo de propiedades o YAML fuera del jar de la aplicación ("application-{profile}.properties").

En Spring Boot podemos tener varias configuraciones dependiendo del perfil con el que se ejecute la aplicación; esto es especialmente útil cuando colocaremos nuestra aplicación en un servicio en la nube como AWS o Azure, ya que podemos tener instancias de la aplicación en distintas localidades (por lo regular cada servicio tiene un precio distinto dependiendo de la localidad en el que lo instalemos), por lo que podemos tener una configuración para la instancia de eu-central y otra diferente para la instancia de eu-west. También es útil cuando vamos a ejecutar nuestra aplicación en distintos ambientes como desarrollo, pruebas o producción.

Para el ejemplo, crearemos un archivo llamado "application-prod.properties" en el mismo directorio que el archivo "application.properties" de hace un momento. Este archivo será leído cuando en nuestra aplicación esté activo el perfil "prod".



Colocamos el siguiente contenido dentro del archivo:

demo.valor = desde application.properties fuera del jar de la aplicación usando el perfil de producción

Tenemos dos formas de indicar a Spring Boot qué perfil o perfiles estará usando. La primera forma es como una propiedad de Java (un parámetro con -D), la cual recordemos que debe aparecer antes del nombre del jar de nuestra aplicación en la línea de comandos; de esta forma:

java -jar -Dspring.profiles.active=prod configuracion-0.0.1-SNAPSHOT.jar

La segunda forma es como un argumento de nuestra aplicación, en este caso el argumento debe aparecer después del nombre del jar de nuestra aplicación en la línea de comandos; de esta forma:

java -jar configuracion-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

Con cualquiera de las dos formas, al iniciar nuestra aplicación debemos ver en la línea de comandos la indicación de qué perfil está activo:



Al entrar nuevamente en la siguiente dirección:

http://localhost:8080/

Debemos ver la siguiente pantalla:



Con esto sabemos que el ejemplo funcionó correctamente.


Configurando con Variables de Entorno

El siguiente es muy interesante, ya que ni siquiera deberemos modificar nada de nuestra aplicación, sino establecer el valor en una variable de entorno. En una terminar establecemos el siguiente valor de la variable, si están en Windows debe ser de esta forma:

SET demo.valor=Valor desde variable de entorno

Si están en Linux:

export demo.valor=Valor desde variable de entorno

Y en esa misma terminal ejecutar nuestra aplicación:

java -jar configuracion-0.0.1-SNAPSHOT.jar

Con esto si entramos nuevamente a la siguiente dirección:

http://localhost:8080/

Debemos ver la siguiente pantalla:




Configurando con Propiedades Java de Sistema

Para la precedencia de la posición 8 de la lista, propiedad de sistema Java, debemos detener nuestra aplicación y ejecutarla de la siguiente manera, pasando "demo.valor" como una propiedad Java. Recuerden que esta propiedad debe colocarse usando el prefijo -D y que este debe ponerse antes del nombre de nuestro jar, de la siguiente forma:

java -Ddemo.valor="Propiedad de sistema Java" -jar configuracion-0.0.1-SNAPSHOT.jar

Con esto, al ingresar nuevamente en la siguiente dirección:

http://localhost:8080/

Debemos ver la siguiente pantalla:




Configurando con Propiedades SPRING_APPLICATION_JSON

Para el siguiente nivel de precedencia pasaremos hasta el número 12 de la lista: propiedades "SPRING_APPLICATION_JSON", este es una cadena en formato JSON que podemos pasar de las siguientes tres formas, en cualquiera de las tres recuerden que deben tener un JSON bien formado:

  1. Como un JSON en la variable de entorno "SPRING_APPLICATION_JSON", de esta forma:

    SET SPRING_APPLICATION_JSON={"demo":{"valor": "Valor desde SPRING_APPLICATION_JSON" }}
    

  2. Como una propiedad Java de Sistema, de la siguiente forma, recuerden que esta propiedad debe aparecer antes que el nombre de nuestra aplicación:

    -Dspring.application.json="{\"demo\":{\"valor\": \"Valor desde SPRING_APPLICATION_JSON como propiedad\" }}"
    

  3. Como un argumento de línea de comandos, que debe aparecer después del nombre de nuestra aplicación:

    --spring.application.json="{\"demo\":{\"valor\": \"Valor desde SPRING_APPLICATION_JSON como parametro\" }}"
    

La precedencia de las opciones en la lista anterior también va de la que tiene menos precedencia a la que tiene más precedencia.

Si ejecutamos nuestra aplicación de la siguiente forma:

java -jar configuracion-0.0.1-SNAPSHOT.jar --spring.application.json="{\"demo\":{\"valor\": \"Valor desde SPRING_APPLICATION_JSON como parametro\" }}"

Y entramos en la siguiente dirección:

http://localhost:8080/

Debemos ver la siguiente pantalla:




Configurando con Argumentos de Línea de Comandos

Para terminar esta parte del tutorial veremos el nivel de precedencia número 13 de la lista anterior, el cual es pasar el valor como un argumento en la línea de comandos. Esto ya lo hemos visto algunas veces en este tutorial; para hacerlo basta con pasar el nombre del parámetro precedido por dos guiones medios, seguido el signo de "=" y a continuación el valor que le daremos a la variable. Este argumento debe aparecer después del nombre del jar de nuestra aplicación, de esta forma:

java -jar configuracion-0.0.1-SNAPSHOT.jar --demo.valor="valor como argumento de línea de comandos"

Si ejecutamos el comando anterior y entramos en la siguiente dirección:

http://localhost:8080/

Debemos ver la siguiente pantalla:



Con esto hemos recorrido las precedencias de todos los lugares en los que podemos establecer parámetros en Spring Boot; o bueno, casi. Dejamos de lado los casos de los parámetros de pruebas, pero esos los veremos en su tutorial correspondiente.


Leyendo Parámetros de Configuración con Environment

Al inicio del tutorial mencioné que existen tres formas en las que podemos leer los valores de las propiedades, y que "@Value" era el primero que veríamos. Ahora veremos la segunda forma que es usando el objeto "org.springframework.core.env.Environment" de Spring.

Usar este objeto es prácticamente igual al uso de "@Value", sólo que en este caso no debemos indicar de dónde se leerán las propiedades, ya que Spring Boot sabe cuáles son los lugares posibles (que son los de la lista de precedencias), por lo que en automático irá a buscar los valores en estas ubicaciones. Lo único que debemos hacer es inyectar un objeto de este tipo en nuestro controlador usando "@Autowired"; nuevamente lo haremos a través de la inyección de setter, y lo configuraremos usando Lombok de esta forma:

@Setter (onMethod=@__(@Autowired))
private Environment env;


*Nota: Como todos los atributos de nuestro controlador tienen setters, y todos estos tienen exactamente la misma configuración (@Setter (onMethod=@__(@Autowired))) podemos poner esta a nivel de clase; por claridad lo dejaremos como está en este tutorial, pero el lector puede tratar de hacer este cambio para comprobar que el resultado es el mismo.


A continuación, crearemos un manejador nuevo de peticiones, para leer el valor de una manera diferente y no tener que modificar el manejador que ya tenemos. Nuestro manejador responderá a las peticiones hechas a la ruta "/env" y leerá desde "Environment" el valor de una propiedad llamada "demo.valorEnv". El manejador queda por lo tanto de la siguiente forma

@GetMapping("/env")
public String saludaEnv() {
    return "Hola " + env.getProperty("demo.valorEnv");
}

Lo siguiente es colocar el valor de esta propiedad en cualquiera de los lugares desde los que ya hemos visto que Spring Boot puede leerlas; en mi caso (y lo más normal es hacerlo así) la colocaré en el archivo "application.properties" debajo de la propiedad que ya teníamos, de la siguiente forma:

demo.valorEnv = leyendo con Evironment desde application.properties

Ahora hay que ejecutar nuestra aplicación (Alt + Shift + X, J) y entrar en la siguiente ruta:

http://localhost:8080/env

Con esto debemos ver la siguiente pantalla:



Con lo cual podemos comprobar que el ejemplo ha funcionado correctamente.

Las precedencias en los parámetros de configuración usando "Environment" y usando "@PropertySource" son exactamente las mismas, por lo que todo lo que hemos aprendido hasta ahora continúa funcionando.


Leyendo Parámetros con @ConfigurationProperties

La última forma de leer las propiedades es de las más interesantes, ya que usando "@ConfigurationProperties" podemos asociar en automático todos los valores de las propiedades que tengan cierto prefijo, con los atributos de una clase Java. Esto es muy común, por ejemplo, cuando trabajamos con propiedades de bases de datos o de servidores de correo para ayudarnos a no tener que estar colocando cada una de las propiedades en un elemento "@Value" o leyéndolas con "env.getProperty("...")".

Para nuestro ejemplo comencemos colocando las siguientes propiedades ficticias de configuración para el envío de un correo electrónico:

mail.hostname=javatutoriales.com
mail.adminMail=programadorjavablog@gmail.com
mail.port=25
mail.from=robot@javatutoriales.com
mail.defaultRecipients[0]=admin@javatutoriales.com
mail.defaultRecipients[1]=owner@javatutoriales.com
mail.additionalHeaders.redelivery=true
mail.additionalHeaders.secure=true

Si usáramos alguno de los métodos pasados, deberíamos leer "a mano" cada uno de los 8 valores anteriores (6 si consideramos que "defaultRecipients" es una lista de cadenas y que "additionalHeaders" es un Map).

Usando "@ConfigurationProperties" este trabajo se simplificará enormemente.

Comencemos creando una nueva clase llamada "MailConfigurationProperties" en el paquete "config". En esta clase pondremos un atributo por cada uno de los 6 parámetros de configuración del mail. Como necesitaremos los setters y getters de estas propiedades aprovechemos para colocar estos elementos usando Lombok, con las anotaciones "@Setter", "@Getter" y "@ToString":

@Setter
@Getter
@ToString
public class MailConfigurationProperties {
    private String hostname;
    private int port;
    private String from;
    private List<String> defaultRecipients;
    private Map<String, String> additionalHeaders;
}

Lo que resta es indicarle a Spring que esta clase será usada como fuente de configuración, con la anotación "@Configuration" y que en los atributos de esta clase se deben colocar los valores de la configuración leída (de las fuentes y con la precedencia que ya conocemos); para esto último es que usamos la anotación "@ConfigurationProperties". Si se fijan en la lista de propiedades anteriores todas estas propiedades inician con el mismo prefijo: "mail", esto es importante para limitar el número de propiedades que serán usadas dentro de esta clase. Para indicar a Spring qué prefijo es el que pertenece a este objeto de configuración, usamos el atributo "prefix" en esa última anotación. Al final la clase queda de la siguiente forma:

@Setter
@Getter
@ToString
@Configuration
@ConfigurationProperties(prefix="mail")
public class MailConfigurationProperties {
    private String hostname;
    private int port;
    private String from;
    private List<String> defaultRecipients;
    private Map<String, String> additionalHeaders;
}

Lo siguiente es modificar nuestra clase "DemoController" para inyectar un objeto de tipo "MailConfigurationProperties" (nuevamente usando inyección de setter) y usándolo en un nuevo manejador de peticiones. Para simplificar este manejador de peticiones y hacer fácil el comprobar que los valores han quedados establecidos de forma correcta, lo único que haremos es usar el método toString (que generó Lombok) y regresar este como el resultado de la invocación del manejador. La declaración del objeto "MailConfigurationProperties" y el nuevo manejador de peticiones quedan de la siguiente forma:

@Setter (onMethod=@__(@Autowired))
private MailConfigurationProperties mailProperties;

@GetMapping("/mail")
public String muestraMail() {
    return mailProperties.toString();
}

Ahora, debemos iniciar nuestra aplicación y entrar en la siguiente dirección:

http://localhost:8080/mail

Con lo que debemos ver la siguiente pantalla:



Con esto podemos comprobar lo sencillo que es leer de esta forma un conjunto "grande" de propiedades de configuración. Esto nos sirve incluso si dentro de la clase decorada con "@ConfigurationProperties" tenemos referencias a otros objetos. Para entender esto vamos a crear una clase llamada "Credenciales" en el paquete "config". Esta clase tendrá tres propiedades sencillas de tipo String, una para el usuario, otra para la contraseña, y otra para el host al que se conectará. Como necesitaremos una serie de getters, setters y mostrar los valores cargados desde el archivo de configuración, aprovecharemos Lombok para que los genere usando las anotaciones "@Getter", "@Setter", y "@ToString". La clase debe quedar de la siguiente forma:

@Getter
@Setter
@ToString
public class Credenciales {
    private String user;
    private String password;
    private String host;
}

Ahora podemos poner un objeto de esta clase como atributo de "MailConfigurationProperties"; la clase completa queda de esta forma:

@Setter
@Getter
@ToString
@Configuration
@ConfigurationProperties(prefix = "mail")
public class MailConfigurationProperties {
	private String hostname;
	private int port;
	private String from;
	private List<String> defaultRecipients;
	private Map<String, String> additionalHeaders;
	
	private Credenciales credenciales;
}

Y colocamos estas propiedades adicionales en el archivo de configuración:

mail.credenciales.host=smtp.javatutoriales.com
mail.credenciales.user=admin
mail.credenciales.password=passwordSecreto

Si ahora volvemos a ejecutar nuestra aplicación y a entrar en la siguiente dirección:

http://localhost:8080/mail

Debemos ver la siguiente pantalla:



Como vemos, hemos podido leer los valores del archivo de configuración con tan solo un par de cambios en unos minutos. "@ConfigurationProperties" tiene las mismas precedencias con las que hemos estado trabajando de la lista anterior, así que todo lo que hemos aprendido hasta ahora podemos aplicarlo.


Validando los Valores de las Propiedades

Algo muy útil que podemos hace cuando trabajamos con "@ConfigurationProperties" es colocar validaciones de los valores que esperamos leer del archivo de configuración. Con esto evitaremos que algún valor obligatorio no sea proporcionado, o que este no siga el formato esperado, o que algún valor se salga de un rango esperado.

Para hacer esto utilizaremos las anotaciones de la especificación JSR-380 Bean Validation, las cuales se encuentran en el paquete "javax.validation.constraints". Este no será un tutorial de Bean Validation, por lo que solo usaremos algunas de las anotaciones más básicas para mostrar su funcionalidad:
  1. "@NotEmpty". Indica que el valor no puede ser vacío o nulo.
  2. "@Positive". Indica que el valor debe ser un número positivo (0 se considera un valor no válido). Los elementos nulos se consideran válidos.
  3. "@Email". El valor proporcionado debe tener el patrón de una dirección de correo electrónico.
  4. "@Size". La longitud debe estar dentro de los límites indicados.
Todas las anotaciones anteriores están en el paquete "javax.validation.constraints" y todas soportan una propiedad opcional "message" en la que podemos indicar un mensaje personalizado que se mostrará en caso de que la validación falle.

En el caso de a clase "Credenciales" solamente validaremos que la longitud de la contraseña sea de entre 8 y 10 caracteres, de la siguiente forma:

@Size(min=8, max=10)
private String password;

*Nota: Si quisiéramos implementar una validación más compleja, como que la contraseña incluya letras mayúsculas, minúsculas, números, caracteres especiales, etc. podemos usar la anotación "@Pattern" y definir el patrón que queramos que sea validado.

En el caso de la clase "MailConfigurationProperties" validaremos que se proporcione un "hostname", que el puerto sea un valor positivo y que el emisor y lo receptores sea correos electrónicos, de la siguiente forma (noten que la validación en el caso de "defaultRecipients" se coloca dentro del tipo de la lista):

@NotEmpty(message="El correo es un parámetro obligatorio.")
private String hostname;
	
@Positive
private int port;
	
@Email
private String from;
private List<@Email String> defaultRecipients;

Si ejecutamos nuestro ejemplo nuevamente debemos ver los siguientes errores en la consola:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'mail' to com.javatutoriales.springboot.configuracion.config.MailConfigurationProperties$$EnhancerBySpringCGLIB$$a30b536c failed:

    Property: mail.hostname
    Value: null
    Reason: El correo es un parámetro obligatorio.


Action:

Update your application's configuration

Si observan detenidamente la salida anterior hay un problema: aunque hemos indicado que la contraseña debe tener entre 8 y 10 caracteres, y la contraseña en el archivo de configuración tiene 15, no hemos recibido un error de validación de este campo, ¿Por qué?.... bien lo que pasa es que cuando tenemos validaciones en atributos de tipos complejos (o sea, que no son de tipos primitivos) debemos indicarle a Spring que también debe revisar las validaciones de los atributos de este objeto. Para eso debemos marcar el atributo con la anotación "@Valid", de esta forma:

@Valid
private Credenciales credenciales;

Si volvemos a ejecutar nuestra aplicación, debemos ver la siguiente salida:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'mail' to com.javatutoriales.springboot.configuracion.config.MailConfigurationProperties$$EnhancerBySpringCGLIB$$7174b4f2 failed:

    Property: mail.hostname
    Value: null
    Reason: El correo es un parámetro obligatorio.

    Property: mail.credenciales.password
    Value: passwordSecreto
    Reason: el tamaño tiene que estar entre 8 y 10


Action:

Update your application's configuration

Ahora la salida que obtenemos es la esperada. Todo lo que debemos hacer para poder volver a ejecutar nuestra aplicación es corregir los errores indicados en las validaciones y listo, todo debe volver a funcionar de forma correcta.


Cifrando Parámetros de Configuración

Algunas veces, como en el de las contraseñas, rutas, u otra información sensible, no querremos que cualquier persona que tenga acceso al archivo (sobre todo si el mismo es obtenido por un atacante o se coloca en un repositorio de código público) pueda leer el contenido en texto plano; en esto caso lo mejor es cifrar (o encriptar) estas propiedades en los archivos y descifrarlas una vez que se han leído.

Para esto podemos hacer uso de una interface de Spring Boot llamada "EnvironmentPostProcessor", la cual nos permite crear una clase que obtendrá los valores de las propiedades de configuración después de haberla leído de las distintas fuentes, pero antes de ponerla a disposición del resto de las clases de la aplicación.

En este tutorial no haremos un cifrado complejo ya que queda fuera del alcance del tutorial. Usaremos el cifrado Cesar, moviendo seis posiciones cada uno de los caracteres de la contraseña y del host.

La interface "EnvironmentPostProcessor" sólo tiene un método, con la siguiente firma:

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application)

Es dentro de este método, usando la variable "environment" que obtendremos los valores de las propiedades que queremos modificar, y posteriormente haremos el descifrado de la información. No entraré en mucho detalle de cómo funciona esta interface, ya que se puede volver un proceso muy complejo; para dejarlo lo más sencillo posible lo único que haremos será leer los valores, descifrarlos y colocarlos en otra fuente de propiedades a la que le daremos la prioridad más alta. "EnvironmentPostProcessor" es invocado antes incluso de que se ejecuten las validaciones, así que no hay que preocuparse si las cadenas cifradas no cumplen con las validaciones, sólo debemos asegurarnos que las cadenas descifradas sí sean válidas.

Cifraremos ambas propiedades usando un movimiento de 6 posiciones de las letras, por lo que las propiedades cifradas quedan de esta forma:

mail.hostname=pgbgzazuxogrky.ius
mail.credenciales.password=vgyycuxj

Si en este momento ejecutamos nuevamente la aplicación, y entramos en la siguiente dirección:

http://localhost:8080/mail

Debemos ver las propiedades tal cual las escribimos en el archivo:



Ahora, crearemos una nueva clase llamada "EncriptionEnvironmentPostProcessor" en el paquete "config", esta clase debe implementar la interface "EnvironmentPostProcessor".

public class EncriptionEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
	
    }
}

La implementación que haremos es muy sencilla: leeremos las propiedades que sabemos que estarán cifradas usando el argumento "environment", las descifraremos, las colocaremos dentro de una nueva fuente de propiedades de configuración, y luego colocaremos esta nueva fuente dentro del objeto "environment" dándole la prioridad más alta para asegurarnos que Spring use estos valores nuevos. Esto lo haremos con la siguiente implementación del método "postProcessEnvironment":

private static final String PROPERTY_SOURCE_NAME = "secretConfig";

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    Map<String, Object> map = new HashMap<String, Object>();
		
    map.put("mail.credenciales.password", descifra(environment.getProperty("mail.credenciales.password"), 6));
    map.put("mail.hostname", descifra(environment.getProperty("mail.hostname"), 6));
		
    environment.getPropertySources().addFirst(new MapPropertySource(PROPERTY_SOURCE_NAME, map));
}

El secreto está en la línea 10 y como podemos ver son tan solo 4 líneas de código las que nos permiten implementar esta funcionalidad. A propósito no he colocado la implementación del método "descrifra" para uno usar mucho espacio en el post, pero pueden encontrarlo en el ejemplo de código en GitHub al final del tutorial.

Para que Spring Boot use esta clase es necesario indicarle que esta será la implementación de "EnvironmentPostProcessor". Para esto debemos colocar un archivo llamado "spring.factories" en el directorio "META-INF", que en Gradle y Maven se coloca dentro del directorio "src/main/resources":



Este archivo debe tener el siguiente contenido:

org.springframework.boot.env.EnvironmentPostProcessor=com.javatutoriales.springboot.configuracion.config.EncriptionEnvironmentPostProcessor

Si ahora volvemos a ejecutar nuestra aplicación y entramos en la dirección:

http://localhost:8080/mail

Debemos ver la siguiente salida, en la que los valores ya podemos verlos sin el cifrado y en texto plano:




Configurando con Archivos YAML

Para terminar este tutorial veremos una alternativa a los archivos de propiedades. Hasta ahora hemos estado usando archivos de propiedades para colocar los parámetros de configuración, pero esta no es la única opción, Spring nos da como alternativa el uso de archivos YAML (YAML Ain't Markup Language – YAML no es un lenguaje de marcado) el cual es un lenguaje de serialización de datos, que tiene la particularidad de ser más fácil de entender y organizar que los archivos de propiedades, ya que no hay que estar repitiendo la misma llave o prefijo muchas veces, sino que se ayuda del anidamiento de los valores, esto permite organizarlos de forma jerárquica. Por ejemplo, en lugar de escribir los valores del archivo de propiedades de esta forma:

demo.valor = desde application.properties 
demo.valorEnv = leyendo con Evironment

mail.hostname=pgbgzazuxogrky.ius
mail.adminMail=programadorjavablog@gmail.com
mail.credenciales.host=smtp.javatutoriales.com
mail.credenciales.user=admin

En la que repetimos un par de veces "demo" y "mail" y "mail.credenciales", podemos escribirlo así en un archivo YAML:

demo:
  valor: desde application.yaml 
  valorEnv: leyendo con Evironment desde application.yaml


mail:
  hostname: pgbgzazuxogrky.ius
  adminMail: programadorjavablog@gmail.com  
  credenciales:
    host: smtp.javatutoriales.com
    user: admin

Como podemos ver, eliminamos los elementos repetitivos de los nombres de las propiedades y sigue siendo claro qué elemento hijo pertenece a qué elemento padre.

*Nota: cuando anidamos un elemento debemos usar un espacios y no tabs, el saber esto les ayudará a ahorrar muchos problemas.

Spring da preferencia a los archivos de propiedades sobre los yaml, esto quiere decir que si tenemos un archivo "application.yaml" y un archivo "application.properties", Spring leerá primero el archivo YAML y luego el archivo properties, sobreescibiendo los valores que se encuentren en el primero.

Cuando usamos un archivo YAML no es necesario hacer ninguna modificación en nuestro código, así que para nosotros es transparente. Para probar esto, vamos a crear un archivo "application.yaml" en el directorio "src/main/resources" (a la altura del archivo "application.properties", el cual por cierto renombraremos agregando un "_" al inicio del nombre del archivo, para que Spring Boot no lea este archivo"):



Y colocaremos el siguiente contenido en el archivo, que básicamente es la traducción del archivo de propiedades al formato yaml (noten que eliminé los acentos, ya que yaml no los soporta y el error que aparece si los dejamos no es claro):

demo:
  valor: desde application.yaml dentro del jar de la aplicacion
  valorEnv: leyendo con Evironment desde application.yaml
 
mail:
  hostname: pgbgzazuxogrky.ius
  adminMail: programadorjavablog@gmail.com
  port: 35
  from: robot@javatutoriales.com
 
  defaultRecipients:
   0: admin@javatutoriales.com
   1: owner@javatutoriales.com
 
  additionalHeaders:
   redelivery: true
   secure: true
  
  credenciales:
    host: smtp.javatutoriales.com
    password: vgyycuxj
    user: admin

Si ahora ejecutamos nuevamente nuestra aplicación y entramos a cualquiera de las rutas de nuestra aplicación, debemos ver que el contenido se está leyendo de este nuevo archivo sin necesidad de modificar nada en nuestro código:

http://localhost:8080/



http://localhost:8080/env



http://localhost:8080/mail




Con lo cual comprobamos que el ejemplo funciona correctamente. Las precedencias de la lista inicial siguen siendo válidas para los archivos yaml.

Una ventaja del uso de archivos YAML contra los archivos de propiedades, es que podemos poner múltiples perfiles en un mismo archivo usando la llave "spring.profile" para indicar el nombre del perfil que estamos configurando, y separando estos por tres guiones ("---").

server:
  address: 192.168.1.100
  port: 5000      
---
spring:
  profiles: development
  
server:
  address: 127.0.0.1
---
spring:
  profiles: produccion
  
server:
  address: 192.168.1.120

En el ejemplo anterior podemos ver una configuración inicial por default, en el que se coloca una URL y un puerto por el que trabajará nuestra aplicación. Después se indica la configuración del perfil "development" en el cual se cambia la URL (y se mantiene el número de puerto) y otro perfil llamado "producción" en el cual nuevamente se cambia la URL.

Incluso podemos indicar que cierta configuración NO aplicará a un perfil, usando la negación ("!") en el nombre del perfil:

spring:
  profiles: !pruebas
  security:
    user:
      password: facil123

Para terminar, esta es una lista de las propiedades que normalmente querremos modificar en nuestra aplicación para ajustarla a nuestras necesidades. Spring Boot tiene cientos de propiedades con valores por default, para ver la lista completa pueden visitar este sitio.

debug=false # Enable debug logs.
trace=false # Enable trace logs.

server.address= # Network address to which the server should bind.
server.port=8080 # Server HTTP port.

logging.config= # Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback.
logging.file.max-history=0 # Maximum of archive log files to keep. Only supported with the default logback setup.
logging.file.max-size=10MB # Maximum log file size. Only supported with the default logback setup.
logging.level.*= # Log levels severity mapping. For instance, `logging.level.org.springframework=DEBUG`.

spring.application.name= # Application name.

spring.profiles.active= # Comma-separated list of active profiles. Can be overridden by a command line switch.



Conclusión

Nuestras aplicaciones siempre necesitarán externalizar su configuración para que no sea necesario el recompilar el código cuando hay un cambio en alguno de estos valores, ya sea de alguna ruta, password, usuario, etc. Spring Boot nos da muchos lugares en los que podemos colocar estas configuraciones, y conociendo las precedencias de cada uno de estos lugares, nos será fácil sobrescribir los valores cuando tengamos que hacer un cambio, ya sea de forma permanente, para un ambiente específico, o para una prueba.

En este tutorial aprendimos varias formas de leer estos valores configuración, en los cuales no es necesario modificar ni una sola línea de nuestro código, también aprendimos una manera en la que podremos realizar el descifrado de ciertas propiedades sin importar en dónde se encuentren estas propiedades. Finalmente, aprendimos que además de los archivos de propiedades tradicionales de Java, también podemos usar archivos en formato YAML, los cuales ayudarán a reducir el tamaño de nuestros archivos y ayudarán en el mantenimiento de los mismos.

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También pueden seguir JavaTutoriales en las siguientes redes sociales:

Saludos y gracias.

Descarga los archivos de este tutorial desde aquí:

Entradas Relacionadas: