Bean Scopes

Ámbitos de los beans

Cuando creas una definición de bean, creas una receta para generar instancias reales de la clase definida por esa definición. La idea de que una definición de bean sea una receta es importante, porque significa que, al igual que con una clase, puedes crear muchas instancias de objeto a partir de una sola receta.

Puedes controlar no solo las diversas dependencias y valores de configuración que se aplican a un objeto creado a partir de una definición de bean determinada, sino también el ámbito de los objetos creados a partir de esa definición. Este enfoque es potente y flexible, porque te permite elegir el ámbito de los objetos mediante configuración, en lugar de incorporarlo en la clase Java. Los beans se pueden desplegar en uno de varios ámbitos.

Spring Framework admite seis ámbitos, cuatro de los cuales solo están disponibles si utilizas un ApplicationContext compatible con aplicaciones web. También puedes crear un ámbito personalizado.

Ámbito Descripción
singleton Predeterminado. Limita una definición de bean a una sola instancia de objeto por cada contenedor IoC de Spring.
prototype Permite crear cualquier número de instancias de objeto a partir de una única definición de bean.
request Limita una definición de bean al ciclo de vida de una sola petición HTTP. Cada petición HTTP tiene su propia instancia de un bean creado a partir de una misma definición. Solo es válido en un ApplicationContext de Spring compatible con aplicaciones web.
session Limita una definición de bean al ciclo de vida de una Session HTTP. Solo es válido en un ApplicationContext de Spring compatible con aplicaciones web.
application Limita una definición de bean al ciclo de vida de un ServletContext. Solo es válido en un ApplicationContext de Spring compatible con aplicaciones web.
websocket Limita una definición de bean al ciclo de vida de un WebSocket. Solo es válido en un ApplicationContext de Spring compatible con aplicaciones web.

Existe un ámbito de hilo, pero no se registra de forma predeterminada. Para más información, consulta la documentación de SimpleThreadScope. Para registrar este u otro ámbito personalizado, consulta la sección sobre el uso de un ámbito personalizado.

Ámbito singleton

Spring gestiona una única instancia compartida de un bean singleton. Todas las solicitudes de beans con un identificador que coincida con esa definición devuelven esa misma instancia desde el contenedor de Spring.

Dicho de otro modo, cuando defines una definición de bean con ámbito singleton, el contenedor IoC de Spring crea exactamente una instancia del objeto definido por esa definición. Esa instancia se almacena en una caché de beans singleton y todas las solicitudes y referencias posteriores a ese bean con nombre devuelven el objeto almacenado en caché.

El concepto de bean singleton de Spring es distinto del patrón Singleton definido en el libro de patrones de Gang of Four (GoF). El singleton de GoF fija en el código el ámbito de un objeto para que se cree una única instancia de una clase determinada por ClassLoader. El ámbito singleton de Spring se describe mejor como por contenedor y por bean.

Esto significa que, si defines un bean para una clase determinada en un único contenedor de Spring, el contenedor crea una sola instancia de la clase definida por esa definición. El ámbito singleton es el ámbito predeterminado de Spring.

Para definir un bean como singleton en XML:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- Equivalente, aunque redundante, porque singleton es el ámbito predeterminado. -->
<bean id="accountService"
      class="com.something.DefaultAccountService"
      scope="singleton"/>

Ámbito prototype

El ámbito no singleton prototype crea una nueva instancia del bean cada vez que se solicita ese bean concreto. Esto puede ocurrir al inyectarlo en otro bean o al solicitarlo mediante una llamada a getBean() en el contenedor.

Como regla general, utiliza el ámbito prototype para beans con estado y el ámbito singleton para beans sin estado.

El siguiente ejemplo define un bean como prototype en XML:

<bean id="accountService"
      class="com.something.DefaultAccountService"
      scope="prototype"/>

A diferencia de otros ámbitos, Spring no gestiona el ciclo de vida completo de un bean prototype. El contenedor crea, configura y ensambla el objeto prototype, y lo entrega al cliente sin conservar posteriormente un registro de esa instancia.

Aunque los métodos de devolución de llamada de inicialización se invocan en todos los objetos, independientemente de su ámbito, los métodos de destrucción configurados no se invocan para los beans prototype.

El código cliente debe limpiar los objetos con ámbito prototype y liberar los recursos costosos que mantengan. Para que el contenedor de Spring libere los recursos de beans prototype, puedes usar un BeanPostProcessor personalizado que conserve una referencia a los beans que deben limpiarse.

En cierto sentido, el papel del contenedor de Spring respecto a un bean prototype sustituye al operador new de Java. Toda la gestión del ciclo de vida posterior debe realizarla el cliente.

Beans singleton con dependencias de beans prototype

Cuando utilizas beans singleton que dependen de beans prototype, ten en cuenta que las dependencias se resuelven en el momento de la instanciación. Por tanto, si inyectas un bean prototype en un bean singleton, se crea una instancia prototype y se inyecta en el singleton. Esa instancia prototype es la única que se suministra al bean singleton.

Sin embargo, si quieres que el bean singleton obtenga repetidamente nuevas instancias del bean prototype en tiempo de ejecución, no puedes inyectar directamente el bean prototype, porque la inyección solo ocurre una vez: cuando el contenedor crea el singleton y resuelve sus dependencias. Cuando necesites una nueva instancia prototype varias veces durante la ejecución, utiliza inyección de métodos.

Ámbitos request, session, application y websocket

Los ámbitos request, session, application y websocket solo están disponibles si utilizas una implementación de ApplicationContext compatible con aplicaciones web, como XmlWebApplicationContext.

Si utilizas estos ámbitos con contenedores IoC de Spring normales, como ClassPathXmlApplicationContext, se lanza una IllegalStateException que indica que el ámbito de bean es desconocido.

Configuración web inicial

Para admitir beans con ámbito request, session, application y websocket, se requiere una configuración inicial menor antes de definir los beans. Esta preparación no es necesaria para los ámbitos estándar singleton y prototype.

La forma de realizar esta configuración depende de tu entorno Servlet.

Si accedes a beans con ámbito dentro de Spring Web MVC, es decir, dentro de una petición procesada por DispatcherServlet, no necesitas configuración adicional. DispatcherServlet ya expone todo el estado relevante.

Si utilizas un contenedor web Servlet y las peticiones se procesan fuera de DispatcherServlet de Spring, por ejemplo con JSF, debes registrar org.springframework.web.context.request.RequestContextListener, que implementa ServletRequestListener. Puedes hacerlo mediante programación usando WebApplicationInitializer. Como alternativa, añade la siguiente declaración a web.xml:

<web-app>
	...
	<listener>
		<listener-class>
			org.springframework.web.context.request.RequestContextListener
		</listener-class>
	</listener>
	...
</web-app>

Como alternativa, si tienes problemas con la configuración del listener, utiliza RequestContextFilter de Spring. El mapeo del filtro depende de la configuración de la aplicación web que lo rodea, por lo que debes adaptarlo según corresponda:

<web-app>
	...
	<filter>
		<filter-name>requestContextFilter</filter-name>
		<filter-class>
			org.springframework.web.filter.RequestContextFilter
		</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>requestContextFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	...
</web-app>

DispatcherServlet, RequestContextListener y RequestContextFilter hacen lo mismo: asocian el objeto de petición HTTP al Thread que atiende la petición. Esto hace que los beans con ámbito request y session estén disponibles en los niveles inferiores de la cadena de llamadas.

Ámbito request

Considera la siguiente configuración XML:

<bean id="loginAction"
      class="com.something.LoginAction"
      scope="request"/>

El contenedor de Spring crea una nueva instancia del bean LoginAction a partir de la definición loginAction para cada petición HTTP. Es decir, el bean loginAction tiene ámbito de petición HTTP.

Puedes modificar el estado interno de la instancia creada cuanto necesites, porque otras instancias creadas a partir de la misma definición no ven esos cambios de estado. Los cambios pertenecen a una petición individual.

Cuando termina el procesamiento de la petición, se descarta el bean con ámbito request.

Al utilizar componentes basados en anotaciones o configuración Java, puedes asignar el ámbito request mediante @RequestScope:

@RequestScope
@Component
public class LoginAction {
	// ...
}

Ámbito session

Considera la siguiente configuración XML:

<bean id="userPreferences"
      class="com.something.UserPreferences"
      scope="session"/>

El contenedor de Spring crea una nueva instancia del bean UserPreferences a partir de la definición userPreferences para la duración de una única Session HTTP. En otras palabras, el bean userPreferences queda efectivamente limitado al nivel de sesión HTTP.

Al igual que con los beans con ámbito request, puedes modificar todo lo que necesites el estado interno de la instancia creada. Otras sesiones HTTP que utilicen instancias creadas a partir de la misma definición userPreferences no ven esos cambios, porque pertenecen a una sesión HTTP individual. Cuando se descarta la sesión HTTP, también se descarta el bean asociado a ella.

Con componentes basados en anotaciones o configuración Java, puedes asignar el ámbito session mediante @SessionScope:

@SessionScope
@Component
public class UserPreferences {
	// ...
}

Ámbito application

Considera la siguiente configuración XML:

<bean id="appPreferences"
      class="com.something.AppPreferences"
      scope="application"/>

El contenedor de Spring crea una única instancia del bean AppPreferences a partir de la definición appPreferences para toda la aplicación web. Es decir, el bean appPreferences tiene ámbito de ServletContext y se almacena como un atributo normal de ServletContext.

Esto es parecido a un bean singleton de Spring, pero se diferencia en dos aspectos importantes: es un singleton por ServletContext, no por ApplicationContext de Spring —puede haber varios en una misma aplicación web—, y se expone realmente como un atributo visible de ServletContext.

Con componentes basados en anotaciones o configuración Java, puedes asignar el ámbito application mediante @ApplicationScope:

@ApplicationScope
@Component
public class AppPreferences {
	// ...
}

Ámbito websocket

El ámbito websocket está asociado al ciclo de vida de una sesión WebSocket y se aplica a aplicaciones STOMP sobre WebSocket.

Beans con ámbito como dependencias

El contenedor IoC de Spring no solo gestiona la creación de objetos, sino también la conexión de colaboradores o dependencias. Si quieres inyectar, por ejemplo, un bean con ámbito de petición HTTP en otro bean de ámbito más duradero, puedes inyectar un proxy AOP en lugar del bean con ámbito.

Debes inyectar un objeto proxy que exponga la misma interfaz pública que el objeto con ámbito, pero que pueda recuperar el objeto de destino real desde el ámbito correspondiente, como una petición HTTP, y delegar en él las llamadas a métodos.

También puedes utilizar <aop:scoped-proxy/> entre beans con ámbito singleton. En ese caso, la referencia pasa por un proxy intermedio serializable que puede volver a obtener el bean singleton de destino durante la deserialización.

Si declaras <aop:scoped-proxy/> en un bean con ámbito prototype, cada llamada a un método del proxy compartido crea una nueva instancia de destino y le reenvía la llamada.

Los proxies con ámbito no son la única forma de acceder de forma segura a beans con ámbitos más cortos. También puedes declarar el punto de inyección —argumento de constructor, de setter o campo autoconectado— como ObjectFactory<MyTargetBean>. Así, puedes llamar a getObject() para recuperar la instancia actual bajo demanda cada vez que la necesites, sin conservarla ni almacenarla por separado.

Como variante ampliada, puedes declarar ObjectProvider<MyTargetBean>, que ofrece varias formas de acceso adicionales, como getIfAvailable() y getIfUnique().

La variante de JSR-330 se denomina Provider y se usa declarando Provider<MyTargetBean> y llamando a get() cada vez que se intente recuperar la instancia.

La configuración del siguiente ejemplo ocupa solo una línea, pero es importante entender tanto el motivo como la forma de utilizarla:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="
		   http://www.springframework.org/schema/beans
		   https://www.springframework.org/schema/beans/spring-beans.xsd
		   http://www.springframework.org/schema/aop
		   https://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- Bean con ámbito de sesión HTTP expuesto como proxy. -->
	<bean id="userPreferences"
		  class="com.something.UserPreferences"
		  scope="session">
		<!-- Indica al contenedor que cree un proxy para el bean circundante. -->
		<aop:scoped-proxy/>
	</bean>

	<!-- Bean singleton al que se inyecta un proxy del bean anterior. -->
	<bean id="userService" class="com.something.SimpleUserService">
		<!-- Referencia al bean userPreferences representado por el proxy. -->
		<property name="userPreferences" ref="userPreferences"/>
	</bean>
</beans>

Para crear un proxy de este tipo, inserta el elemento hijo <aop:scoped-proxy/> en una definición de bean con ámbito.

¿Por qué las definiciones de beans con ámbito request, session y ámbitos personalizados necesitan normalmente el elemento <aop:scoped-proxy/>? Considera la siguiente definición de singleton y compárala con la configuración necesaria para esos ámbitos. La definición de userPreferences siguiente está incompleta:

<bean id="userPreferences"
      class="com.something.UserPreferences"
      scope="session"/>

<bean id="userManager" class="com.something.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>

En el ejemplo anterior, al bean singleton userManager se le inyecta una referencia al bean userPreferences, con ámbito de sesión HTTP. El punto importante es que userManager es un singleton: se crea exactamente una vez por contenedor y sus dependencias, en este caso userPreferences, también se inyectan una sola vez.

Esto significa que userManager opera siempre sobre el mismo objeto userPreferences, el que se inyectó originalmente.

Ese comportamiento no es el deseado cuando inyectas un bean de ámbito más corto en un bean de ámbito más largo. Por ejemplo, al inyectar un bean colaborador con ámbito de sesión HTTP como dependencia de un bean singleton, necesitas un solo objeto userManager y, para la duración de cada sesión HTTP, un objeto userPreferences específico de esa sesión.

Por ello, el contenedor crea un objeto que expone exactamente la misma interfaz pública que la clase UserPreferences, idealmente un objeto que sea una instancia de UserPreferences. Este objeto puede obtener el objeto UserPreferences real desde el mecanismo de ámbito, ya sea la petición HTTP, la sesión u otro. El contenedor inyecta el proxy en userManager, que no sabe que la referencia a UserPreferences es un proxy.

Cuando una instancia de UserManager invoca un método del objeto UserPreferences inyectado, en realidad invoca un método del proxy. El proxy recupera el objeto UserPreferences real de la sesión HTTP y delega en él la llamada al método.

Por tanto, para inyectar beans con ámbito request y session en objetos colaboradores, necesitas esta configuración completa:

<bean id="userPreferences"
      class="com.something.UserPreferences"
      scope="session">
	<aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>

Elegir el tipo de proxy que se crea

De forma predeterminada, cuando el contenedor de Spring crea un proxy para un bean marcado con <aop:scoped-proxy/>, se crea un proxy de clase basado en CGLIB.

Los proxies CGLIB no interceptan métodos privados. Intentar llamar a un método privado de ese proxy no delega en el objeto de destino real con ámbito.

Como alternativa, puedes configurar el contenedor de Spring para crear proxies estándar de interfaces basados en JDK. Para ello, especifica false en el atributo proxy-target-class de <aop:scoped-proxy/>.

Al utilizar proxies de interfaces de JDK, no necesitas bibliotecas adicionales en el classpath de la aplicación. Sin embargo, la clase del bean con ámbito debe implementar al menos una interfaz y todos los colaboradores donde se inyecte ese bean deben referenciarlo a través de una de sus interfaces.

El siguiente ejemplo muestra un proxy basado en interfaz:

<!-- DefaultUserPreferences implementa la interfaz UserPreferences. -->
<bean id="userPreferences"
      class="com.stuff.DefaultUserPreferences"
      scope="session">
	<aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>

Inyectar directamente referencias a request y session

Como alternativa a los ámbitos de fábrica, WebApplicationContext de Spring también permite inyectar HttpServletRequest, HttpServletResponse, HttpSession, WebRequest y, si JSF está presente, FacesContext y ExternalContext en beans gestionados por Spring. Esto se hace simplemente mediante autoconexión por tipo, junto con los puntos de inyección habituales de otros beans.

Por lo general, Spring inyecta proxies para estos objetos de petición y sesión. Esto tiene la ventaja de funcionar también en beans singleton y en beans serializables, de forma similar a los proxies con ámbito para beans con ámbito de fábrica.

Ámbitos personalizados

El mecanismo de ámbitos de beans es extensible. Puedes definir tus propios ámbitos o incluso redefinir ámbitos existentes, aunque esto último se considera una mala práctica. No puedes reemplazar los ámbitos integrados singleton y prototype.

Crear un ámbito personalizado

Para integrar ámbitos personalizados en el contenedor de Spring, debes implementar la interfaz org.springframework.beans.factory.config.Scope.

Como referencia para implementar tus propios ámbitos, consulta las implementaciones de Scope incluidas en Spring Framework y el Javadoc de Scope, que explica en detalle los métodos que debes implementar.

La interfaz Scope tiene cuatro métodos para obtener objetos del ámbito, eliminarlos y permitir su destrucción.

La implementación del ámbito de sesión, por ejemplo, devuelve el bean con ámbito de sesión. Si no existe, devuelve una nueva instancia del bean después de asociarla a la sesión para futuras referencias. El siguiente método devuelve el objeto desde el ámbito subyacente:

Object get(String name, ObjectFactory<?> objectFactory)

La implementación del ámbito de sesión elimina el bean con ámbito de sesión de la sesión subyacente. Debe devolver el objeto, aunque puede devolver null si no se encuentra un objeto con el nombre especificado:

Object remove(String name)

El siguiente método registra una devolución de llamada que el ámbito debe invocar cuando se destruya o cuando se destruya el objeto especificado dentro del ámbito:

void registerDestructionCallback(String name, Runnable destructionCallback)

Consulta el Javadoc o una implementación de ámbito de Spring para más información sobre las devoluciones de llamada de destrucción.

El siguiente método obtiene el identificador de conversación del ámbito subyacente:

String getConversationId()

Este identificador es distinto para cada ámbito. En una implementación con ámbito de sesión, puede ser el identificador de la sesión.

Usar un ámbito personalizado

Después de escribir y probar una o más implementaciones personalizadas de Scope, debes informar al contenedor de Spring sobre los nuevos ámbitos.

El siguiente método es el método central para registrar un nuevo Scope en el contenedor de Spring:

void registerScope(String scopeName, Scope scope);

Este método está declarado en la interfaz ConfigurableBeanFactory, disponible mediante la propiedad BeanFactory en la mayoría de implementaciones concretas de ApplicationContext incluidas con Spring.

El primer argumento de registerScope(..) es el nombre único asociado al ámbito. Algunos ejemplos de nombres utilizados por el propio contenedor de Spring son singleton y prototype. El segundo argumento es una instancia real de la implementación personalizada de Scope que quieres registrar y utilizar.

Supón que escribes una implementación de Scope personalizada y la registras como se muestra a continuación.

El siguiente ejemplo usa SimpleThreadScope, que está incluido en Spring, pero no se registra de forma predeterminada. Las instrucciones serían las mismas para tus propias implementaciones de Scope:

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

Después puedes crear definiciones de bean que cumplan las reglas de ámbito de tu Scope personalizado:

<bean id="..." class="..." scope="thread"/>

Con una implementación personalizada de Scope, no estás limitado al registro programático. También puedes registrar el ámbito de forma declarativa mediante CustomScopeConfigurer:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="
		   http://www.springframework.org/schema/beans
		   https://www.springframework.org/schema/beans/spring-beans.xsd
		   http://www.springframework.org/schema/aop
		   https://www.springframework.org/schema/aop/spring-aop.xsd">

	<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
		<property name="scopes">
			<map>
				<entry key="thread">
					<bean class="org.springframework.context.support.SimpleThreadScope"/>
				</entry>
			</map>
		</property>
	</bean>

	<bean id="thing2" class="x.y.Thing2" scope="thread">
		<property name="name" value="Rick"/>
		<aop:scoped-proxy/>
	</bean>

	<bean id="thing1" class="x.y.Thing1">
		<property name="thing2" ref="thing2"/>
	</bean>
</beans>

Cuando colocas <aop:scoped-proxy/> dentro de una declaración <bean> para una implementación de FactoryBean, el bean con ámbito es la propia fábrica, no el objeto devuelto por getObject().