Environment Abstraction

Abstracción de entorno

La interfaz Environment es una abstracción integrada en el contenedor que modela dos aspectos clave del entorno de una aplicación: los perfiles y las propiedades.

Un perfil es un grupo lógico y con nombre de definiciones de beans que se registra en el contenedor solo si dicho perfil está activo. Los beans pueden asignarse a un perfil tanto si se definen mediante XML como mediante anotaciones. La función del objeto Environment respecto a los perfiles es determinar qué perfiles están activos en ese momento y cuáles deben estar activos de forma predeterminada.

Las propiedades desempeñan un papel importante en casi todas las aplicaciones y pueden proceder de distintas fuentes: archivos de propiedades, propiedades del sistema de la JVM, variables de entorno del sistema, JNDI, parámetros del contexto de servlet, objetos Properties creados ad hoc, objetos Map, etc.

La función del objeto Environment respecto a las propiedades es proporcionar una interfaz de servicio práctica para configurar fuentes de propiedades y resolver propiedades a partir de ellas.

Perfiles de definiciones de beans

Los perfiles de definiciones de beans proporcionan un mecanismo en el contenedor principal que permite registrar beans distintos en entornos diferentes.

El término “entorno” puede tener significados distintos según el usuario, y esta funcionalidad cubre múltiples casos de uso, entre ellos:

  • Usar una fuente de datos en memoria durante el desarrollo y buscar esa misma fuente de datos mediante JNDI en control de calidad o producción.
  • Registrar infraestructura de monitorización solo al desplegar una aplicación en un entorno de rendimiento.
  • Registrar implementaciones personalizadas de beans para despliegues del cliente A frente a los del cliente B.

Considera el primer caso de uso en una aplicación que requiere un DataSource. En un entorno de prueba, la configuración podría ser la siguiente:

@Bean
public DataSource dataSource() {
	return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("my-schema.sql")
			.addScript("my-test-data.sql")
			.build();
}

Ahora considera cómo puede desplegarse la aplicación en control de calidad o producción, suponiendo que la fuente de datos de la aplicación esté registrada en el directorio JNDI del servidor de aplicaciones de producción. El bean dataSource sería entonces el siguiente:

@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
	Context ctx = new InitialContext();
	return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

@Bean(destroyMethod = "") desactiva la inferencia predeterminada del método de destrucción.

Como se ha mencionado anteriormente, con los métodos @Bean normalmente debes elegir búsquedas JNDI programáticas, usando los asistentes JndiTemplate o JndiLocatorDelegate de Spring, o el uso directo de InitialContext de JNDI mostrado antes.

No se recomienda utilizar la variante JndiObjectFactoryBean, porque obligaría a declarar el tipo de retorno como FactoryBean.

El problema es cómo alternar entre estas dos variantes según el entorno actual. Con el tiempo, los usuarios de Spring han creado distintas formas de resolverlo, normalmente mediante una combinación de variables de entorno del sistema y sentencias XML <import/> con marcadores ${placeholder} que resuelven la ruta correcta del archivo de configuración según el valor de una variable de entorno.

Los perfiles de definiciones de beans son una funcionalidad del contenedor principal que proporciona una solución a este problema.

Si generalizamos el caso de uso anterior de definiciones de beans específicas de un entorno, surge la necesidad de registrar ciertas definiciones de beans en determinados contextos y no en otros. Podrías decir que quieres registrar un perfil de definiciones de beans en la situación A y un perfil distinto en la situación B.

Uso de @Profile

La anotación @Profile permite indicar que un componente es apto para registrarse cuando uno o varios perfiles especificados están activos.

Usando el ejemplo anterior, la configuración de dataSource puede reescribirse de la siguiente forma:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.addScript("classpath:com/bank/config/sql/test-data.sql")
				.build();
	}
}
@Configuration
@Profile("production")
public class JndiDataConfig {

	@Bean(destroyMethod = "")
	public DataSource dataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

La cadena del perfil puede contener un nombre de perfil simple, como production, o una expresión de perfil. Una expresión de perfil permite expresar lógica más compleja, por ejemplo, production & us-east.

Los operadores admitidos en las expresiones de perfil son:

  • !: negación lógica del perfil.
  • &: conjunción lógica de perfiles.
  • |: disyunción lógica de perfiles.

No puedes combinar los operadores & y | sin usar paréntesis. Por ejemplo, production & us-east | eu-central no es una expresión válida. Debe escribirse como production & (us-east | eu-central).

Puedes utilizar @Profile como meta-anotación para crear una anotación compuesta personalizada. El siguiente ejemplo define una anotación @Production que puede usarse como sustituto directo de @Profile("production"):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

Si una clase @Configuration está marcada con @Profile, todos sus métodos @Bean y sus anotaciones @Import asociadas se omiten salvo que uno o varios de los perfiles especificados estén activos.

Si una clase @Component o @Configuration se marca con @Profile({"p1", "p2"}), esa clase no se registra ni procesa salvo que se haya activado el perfil p1 o p2.

Si un perfil se prefija con el operador NOT (!), el elemento anotado solo se registra si ese perfil no está activo. Por ejemplo, con @Profile({"p1", "!p2"}), el registro se realizará si el perfil p1 está activo o si el perfil p2 no está activo.

@Profile también puede declararse a nivel de método para incluir solo un bean concreto de una clase de configuración, por ejemplo, para variantes alternativas de un bean determinado:

@Configuration
public class AppConfig {

	@Bean("dataSource")
	@Profile("development")
	public DataSource standaloneDataSource() {
		return new EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.addScript("classpath:com/bank/config/sql/test-data.sql")
				.build();
	}

	@Bean("dataSource")
	@Profile("production")
	public DataSource jndiDataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

El método standaloneDataSource solo está disponible en el perfil development. El método jndiDataSource solo está disponible en el perfil production.

Con @Profile en métodos @Bean se aplica un escenario especial: en el caso de métodos @Bean sobrecargados con el mismo nombre de método Java, una condición @Profile debe declararse de forma coherente en todos los métodos sobrecargados.

Si las condiciones no son coherentes, solo importa la condición de la primera declaración entre los métodos sobrecargados. Por tanto, @Profile no puede utilizarse para seleccionar un método sobrecargado con una firma de argumentos concreta frente a otro.

La resolución entre todos los métodos de fábrica para el mismo bean sigue el algoritmo de resolución de constructores de Spring en el momento de creación.

Si quieres definir beans alternativos con condiciones de perfil diferentes, utiliza nombres de método Java distintos que apunten al mismo nombre de bean mediante el atributo name de @Bean, como en el ejemplo anterior.

Si todas las firmas de argumentos son iguales —por ejemplo, si todas las variantes tienen métodos de fábrica sin argumentos—, esta es la única forma de representar esa disposición en una clase Java válida, ya que solo puede existir un método con un nombre y una firma de argumentos determinados.

Perfiles de definiciones de beans XML

La contraparte XML es el atributo profile del elemento <beans/>. La configuración de ejemplo anterior puede reescribirse en dos archivos XML:

<beans profile="development"
		xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:jdbc="http://www.springframework.org/schema/jdbc"
		xsi:schemaLocation="...">

	<jdbc:embedded-database id="dataSource">
		<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
		<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
	</jdbc:embedded-database>
</beans>
<beans profile="production"
		xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:jee="http://www.springframework.org/schema/jee"
		xsi:schemaLocation="...">

	<jee:jndi-lookup id="dataSource"
			jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

También es posible evitar esa separación y anidar elementos <beans/> dentro del mismo archivo:

<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:jdbc="http://www.springframework.org/schema/jdbc"
		xmlns:jee="http://www.springframework.org/schema/jee"
		xsi:schemaLocation="...">

	<!-- Otras definiciones de beans. -->

	<beans profile="development">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
			<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
		</jdbc:embedded-database>
	</beans>

	<beans profile="production">
		<jee:jndi-lookup id="dataSource"
				jndi-name="java:comp/env/jdbc/datasource"/>
	</beans>
</beans>

El esquema spring-beans.xsd restringe estos elementos para permitirlos solo al final del archivo. Esto ofrece flexibilidad sin añadir desorden a los archivos XML.

La contraparte XML no admite las expresiones de perfil descritas anteriormente. Sin embargo, es posible negar un perfil mediante el operador !. También es posible aplicar un “y” lógico anidando los perfiles:

<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:jdbc="http://www.springframework.org/schema/jdbc"
		xmlns:jee="http://www.springframework.org/schema/jee"
		xsi:schemaLocation="...">

	<!-- Otras definiciones de beans. -->

	<beans profile="production">
		<beans profile="us-east">
			<jee:jndi-lookup id="dataSource"
					jndi-name="java:comp/env/jdbc/datasource"/>
		</beans>
	</beans>
</beans>

En el ejemplo anterior, el bean dataSource se expone si están activos tanto el perfil production como us-east.

Activar un perfil

Después de actualizar la configuración, debes indicar a Spring qué perfil está activo. Si iniciaras la aplicación de ejemplo en ese momento, se lanzaría una NoSuchBeanDefinitionException, ya que el contenedor no podría encontrar el bean de Spring llamado dataSource.

Un perfil puede activarse de varias maneras, pero la más directa es hacerlo mediante programación con la API Environment disponible a través de un ApplicationContext:

AnnotationConfigApplicationContext ctx =
		new AnnotationConfigApplicationContext();

ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

Además, puedes activar perfiles de forma declarativa mediante la propiedad spring.profiles.active. Puede especificarse mediante variables de entorno del sistema, propiedades del sistema de la JVM, parámetros del contexto de servlet en web.xml o incluso como una entrada de JNDI.

En pruebas de integración, los perfiles activos pueden declararse mediante la anotación @ActiveProfiles del módulo spring-test.

Los perfiles no son una elección excluyente. Puedes activar varios perfiles a la vez. Mediante programación, puedes proporcionar varios nombres de perfil al método setActiveProfiles(), que acepta argumentos variables String...:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

De forma declarativa, spring.profiles.active puede aceptar una lista de nombres de perfil separada por comas:

-Dspring.profiles.active="profile1,profile2"

Perfil predeterminado

El perfil predeterminado representa el perfil que se activa si no hay ningún perfil activo. Considera el siguiente ejemplo:

@Configuration
@Profile("default")
public class DefaultDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.build();
	}
}

Si no hay ningún perfil activo, se crea el dataSource. Puedes verlo como una forma de proporcionar una definición predeterminada para uno o varios beans. Si se activa cualquier perfil, el perfil predeterminado no se aplica.

El nombre del perfil predeterminado es default. Puedes cambiarlo mediante setDefaultProfiles() en Environment o, de forma declarativa, mediante la propiedad spring.profiles.default.

Abstracción PropertySource

La abstracción Environment de Spring proporciona operaciones de búsqueda sobre una jerarquía configurable de fuentes de propiedades. Considera el siguiente ejemplo:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();

boolean containsMyProperty = env.containsProperty("my-property");

System.out.println(
		"¿Mi entorno contiene la propiedad 'my-property'? " + containsMyProperty);

En el ejemplo anterior se observa una forma de alto nivel de preguntar a Spring si la propiedad my-property está definida para el entorno actual. Para responder, el objeto Environment busca en un conjunto de objetos PropertySource.

Un PropertySource es una abstracción simple sobre cualquier fuente de pares clave-valor. StandardEnvironment de Spring se configura con dos objetos PropertySource: uno representa el conjunto de propiedades del sistema de la JVM (System.getProperties()) y otro representa las variables de entorno del sistema (System.getenv()).

Estas fuentes de propiedades predeterminadas están presentes en StandardEnvironment, para aplicaciones independientes. StandardServletEnvironment se rellena con fuentes de propiedades predeterminadas adicionales, como la configuración del servlet, los parámetros del contexto de servlet y un JndiPropertySource si JNDI está disponible.

En concreto, cuando utilizas StandardEnvironment, la llamada env.containsProperty("my-property") devuelve true si en tiempo de ejecución existe una propiedad del sistema o variable de entorno llamada my-property.

La búsqueda es jerárquica. De forma predeterminada, las propiedades del sistema tienen prioridad sobre las variables de entorno. Por tanto, si my-property se define en ambos sitios durante una llamada a env.getProperty("my-property"), gana y se devuelve el valor de la propiedad del sistema.

Los valores de las propiedades no se combinan, sino que una entrada anterior sobrescribe completamente a otra posterior.

Para un StandardServletEnvironment habitual, la jerarquía completa es la siguiente, con las entradas de mayor prioridad arriba:

  1. Parámetros de ServletConfig, si procede; por ejemplo, en un contexto de DispatcherServlet.
  2. Parámetros de ServletContext, es decir, entradas context-param de web.xml.
  3. Variables de entorno JNDI, es decir, entradas java:comp/env/.
  4. Propiedades del sistema de la JVM, como argumentos de línea de comandos -D.
  5. Entorno del sistema de la JVM, es decir, variables de entorno del sistema operativo.

Lo más importante es que todo el mecanismo es configurable. Puedes integrar una fuente de propiedades personalizada implementando e instanciando tu propio PropertySource y añadiéndolo al conjunto de PropertySources del Environment actual:

ConfigurableApplicationContext ctx = new GenericApplicationContext();

MutablePropertySources sources =
		ctx.getEnvironment().getPropertySources();

sources.addFirst(new MyPropertySource());

En el código anterior, MyPropertySource se ha añadido con la prioridad más alta de la búsqueda. Si contiene una propiedad my-property, esta se detecta y devuelve con preferencia sobre cualquier otra propiedad my-property de otros PropertySource.

La API MutablePropertySources expone varios métodos que permiten manipular con precisión el conjunto de fuentes de propiedades.

Uso de @PropertySource

La anotación @PropertySource ofrece un mecanismo práctico y declarativo para añadir un PropertySource al Environment de Spring.

Dado un archivo llamado app.properties que contiene el par clave-valor testbean.name=myTestBean, la siguiente clase @Configuration usa @PropertySource de forma que una llamada a testBean.getName() devuelve myTestBean:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

	@Autowired
	Environment env;

	@Bean
	public TestBean testBean() {
		TestBean testBean = new TestBean();
		testBean.setName(env.getProperty("testbean.name"));
		return testBean;
	}
}

Los marcadores ${…} presentes en una ubicación de recurso de @PropertySource se resuelven con el conjunto de fuentes de propiedades ya registradas en el entorno:

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

	@Autowired
	Environment env;

	@Bean
	public TestBean testBean() {
		TestBean testBean = new TestBean();
		testBean.setName(env.getProperty("testbean.name"));
		return testBean;
	}
}

Si my.placeholder está presente en una de las fuentes de propiedades ya registradas —por ejemplo, en las propiedades del sistema o variables de entorno—, el marcador se resuelve con su valor correspondiente.

De lo contrario, se utiliza default/path como valor predeterminado. Si no se especifica un valor predeterminado y no puede resolverse una propiedad, se lanza una IllegalArgumentException.

@PropertySource puede utilizarse como anotación repetible. También puede utilizarse como meta-anotación para crear anotaciones compuestas personalizadas con sobrescrituras de atributos.

Resolución de marcadores en sentencias

Históricamente, el valor de los marcadores de posición en elementos solo podía resolverse con propiedades del sistema de la JVM o variables de entorno. Esto ya no es así.

Como la abstracción Environment está integrada en todo el contenedor, es fácil dirigir la resolución de marcadores a través de ella. Esto significa que puedes configurar el proceso de resolución como quieras.

Puedes cambiar la prioridad de búsqueda entre las propiedades del sistema y las variables de entorno, o eliminarlas por completo. También puedes añadir tus propias fuentes de propiedades según corresponda.

En concreto, la siguiente sentencia funciona sin importar dónde se defina la propiedad customer, siempre que esté disponible en Environment:

<beans>
	<import resource="com/bank/service/${customer}-config.xml"/>
</beans>