Spring Session

0
23319

En este tutorial veremos cómo utilizar Spring Session en nuestras aplicaciones web.

Índice de contenidos

1. Introducción

En este tutorial vamos a ver un nuevo módulo de Spring, que es Spring Session. Spring Session ofrece una API y las implementaciones de la gestión de la sesión de un usuario. También proporciona integración transparente con:

  • HTTPSession: permite la sustitución de la HttpSession en un contenedor de aplicaciones de forma neutral. Las características adicionales incluyen :
    • Sesiones en cluster: Permite el soporte de las sesiones en cluster sin estar atado a una solución específica contenedor de aplicaciones
    • Múltiple Sesiones en un navegador: Permite tener varias sesiones en un mismo navegador
    • RESTful APIs: Proporcionar identificadores de sesión en las cabeceras de las API REST
  • WebSocket: proporciona la capacidad de mantener el HttpSession con vida al recibir mensajes WebSocket.

2. Motivación

Las cosas se vuelven más difíciles al escalar porque cada solicitud debe ser asociado con su correspondiente sesión que pueden residir en otro servidor.
Para superar esto, los provedores de servidores han implementado diversos tipos de replicación de sesión entre sus servidores. Alternativamente,
balanceadores de carga se pueden configurar para utilizar sesiones pegajosas. Ambos soluciones de trabajo, pero con la Spring Sesión ha creado otra opción.

Este tutorial va a mostrar cómo utilizar Spring Session, en concreto, la sesión en cluster.

3. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3 SDRAM)
  • Sistema Operativo: Mac OS X Yosemite 10.10.5
  • NVIDIA GeForce GT 750M 2048 MB
  • Eclipse Luna 4.4.2
  • Java 1.8
  • Docker 1.9.0
  • Docker-compose 1.5.0
  • Spring Session 1.0.2.RELEASE
  • Spring Framework 4.1.6.RELEASE
  • Tomcat 8.0.28
  • Redis 3.0.5
  • curl 7.43.0

4. Creación del proyecto

Vamos a crear una proyecto web muy simple que en sesión va a guardar un contador:

  • Con una petición POST aumentamos el contador
  • Con una petición GET recuperamos el contador

Lo primero es crear un proyecto web con maven y añadimos las dependencias de Spring y Spring Session, como puede verse a continuación.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>autentia</groupId>
	<artifactId>spring-session</artifactId>
	<version>1.0.0</version>
	<packaging>war</packaging>

	<properties>
		<java.version>1.8</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<spring.version>4.1.6.RELEASE</spring.version>
		<spring.session.version>1.0.2.RELEASE</spring.session.version>
		<slf4j.version>1.7.12</slf4j.version>
		<log4j.version>2.2</log4j.version>
		<junit.version>4.12</junit.version>
		<hamcrest.version>1.3</hamcrest.version>
		<mockito.version>1.10.19</mockito.version>
		<redis.port>6379</redis.port>
		<redis.host>localhost</redis.host>
	</properties>

	<profiles>
		<profile>
			<id>desarrollo</id>
			<activation>
				<activeByDefault>true</activeByDefault>
			</activation>
		</profile>
		<profile>
			<id>docker</id>
			<properties>
				<redis.port>6379</redis.port>
				<redis.host>redis</redis.host>
			</properties>
		</profile>
	</profiles>

	<build>
		<finalName>springSession</finalName>
		<resources>
			<resource>
				<directory>src/main/resources</directory>
				<filtering>true</filtering>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.3</version>
				<configuration>
					<webResources>
						<resource>
							<directory>src/main/webapp</directory>
							<includes>
								<include>WEB-INF/web.xml</include>
							</includes>
						</resource>
					</webResources>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-release-plugin</artifactId>
				<version>2.4</version>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.5.1</version>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>

		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>javax.servlet.jsp-api</artifactId>
			<version>2.2.1</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.0.1</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>

		<!-- ======================== Spring Session ============================ -->

		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session-data-redis</artifactId>
			<version>${spring.session.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
			<exclusions>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!-- ======================== Logging ============================ -->

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-slf4j-impl</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<!-- ====================== Tests ====================== -->

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-library</artifactId>
			<version>${hamcrest.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-all</artifactId>
			<version>${mockito.version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>

</project>
	

A continuación es crear nuestro servlet que implemente los métodos GET y POST, para recuperar y guardar nuestro contador en sesión.

ContadorServlet.java
package es.autentia.spring.session;

import java.io.*;
import java.util.*;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(urlPatterns = { "/contadorServlet" })
public class ContadorServlet extends HttpServlet {

	private static final long serialVersionUID = -3450969163801147075L;

	protected static final String CONTADOR = "contador";

	private static final Logger LOG = LoggerFactory.getLogger(ContadorServlet.class);

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {

		final Integer contador = getContador(request);
		LOG.debug("Se envia como valor de contador {} en la session {} ",
		 		contador, request.getSession().getId());
		try (ServletOutputStream out = response.getOutputStream()) {
			out.println(contador);
		} catch (IOException e) {
			LOG.error("Error read outputStram cause {}",e.getMessage());
		}
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {

		final int value = getContador(request).intValue() + 1;
		request.getSession().setAttribute(CONTADOR, value);
		LOG.debug("Se guarda como valor de contador {} en la session {} ",
		 		value, request.getSession().getId());
	}

	private Integer getContador(HttpServletRequest request) {
		return Optional.ofNullable(request.getSession())
				.map(session -> (Integer)session.getAttribute(CONTADOR)).orElse(0);
	}
}
	

Por último es la configuración de Spring y Spring session en nuestra aplicación web.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

	<display-name>spring-session</display-name>

	<!-- Spring -->

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
	            classpath*:spring-configuracion/*.xml
	        </param-value>
	</context-param>

	<listener>
	    <listener-class>
	        org.springframework.web.context.ContextLoaderListener
	    </listener-class>
	</listener>

	<!-- Spring Session -->

	<filter>
	    <filter-name>springSessionRepositoryFilter</filter-name>
	    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
	    <filter-name>springSessionRepositoryFilter</filter-name>
	    <url-pattern>/*</url-pattern>
	</filter-mapping>

</web-app>
	

Como vemos solo tenemos que añadir un filtro en nuestro web.xml, para terminar la configuración de Spring Session vamos activarlo por vía xml y además creamos la conexión de redis para que Spring Session guarde la sesión.

web.xml
<?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:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.1.xsd">

	<context:property-placeholder location="classpath*:redis/redis.properties" ignore-unresolvable="true" order="1"/>
	<context:annotation-config />

	<bean
		class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
	</bean>


	<bean
		class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
		p:port="${spring.redis.port}" p:hostName="${spring.redis.host}" />

</beans>
	

Como vemos tenemos un fichero propiedades donde tenemos los parámetros de configuración de conexión redis:

redis.properties
spring.redis.port=${redis.port}
spring.redis.host=${redis.host}
	

Con esto ya tenemos creado nuesto proyecto web con Spring Session.

5. Ejemplo práctico

Para realizar el ejemplo práctico vamos a utilizar docker para desplegar nuestra aplicación en un contenedor y así poder escalar y comprobar que la Session es la misma teniendo varias instancias de nuestra aplicación web.

Para ello vamos a usar docker-compose para tener todas la configuración de nuestros contenedores en un único fichero.

docker-compose.yml
bbdd:
  image: redis
  expose:
   - 6379

app:
  image: tomcat:8.0.28-jre8
  links:
   - bbdd:redis
  ports:
   - 8080
  volumes:
   - ./webapps:/usr/local/tomcat/webapps

Como vemos tenemos definidos dos contenedores:

  • Un contenedor llamado bbdd que contiene una instalación de redis con la versión 3.0.5 y expone el puerto 6379 en la red interna de docker.
  • Un contenedor llamado app que contiene un tomcat con la version 8.0.28, expone el puerto 8080 en la red interna de docker, hacia fuera docker le asignara una aleatoria, un enlace con el contenedor bbdd y volumes para que tomcat lea nuestro war.

Todas las imágenes son oficiales proporciona docker hub.

Una vez creado nuestro war y nuestro docker-compose.yml procedemos a ejecutar nuestro docker-compose para instanaciar nuestro contendores con el siguiente comando

docker-compose up

Como vemos sale por pantalla los logs de nuestros contenedores

springSession_imagen1.
Hacer click en la imagen para agrandar.

Abrimos otra pestaña para obtener la Ip de la máquina donde se esta ejecutando docker y el puerto que tiene asignado tomcat

docker-machine ip dev

Donde “dev” es el nombre de la máquina virtual donde se esta ejecutando docker y por último para saber el puerto ejecutamos el comando

docker ps
Haz click en la imagen para agrandar.
Haz click en la imagen para agrandar.

Como podemos ver en este caso es el puerto 32770 con esta información podemos ya hacer la pruebas

Lo primero es ejecutar con el comando curl una petición GET a nuestro servlet para obtener nuestra sessionId

curl -i $(docker-machine ip dev):32770/springSession/contadorServlet

Como vemos nuestro contador tiene una valor cero y nuestra sessionId es 6f20cb24-cb49-4647-84a1-b0da57ac86eb, a continuación vamos a aumentar nuestro contrado con el siguiente comando

curl -i -X POST -H "Cookie: SESSION=6f20cb24-cb49-4647-84a1-b0da57ac86eb" $(docker-machine ip dev):32770/springSession/contadorServlet

Ahora volvemos a consultar el valor del contador con el siguiente comando

curl -i -H "Cookie: SESSION=6f20cb24-cb49-4647-84a1-b0da57ac86eb"  $(docker-machine ip dev):32770/springSession/contadorServlet

Por último vamos escalar la aplicacion app para tener dos instancia de nuestra aplicacion web, con el siguiente comando

docker-compose scale app=2

Ejecutamos el comando docker ps para verificar que tenemos dos contenedores de app

Haz click en la imagen para agrandar.
Haz click en la imagen para agrandar.

Para finalizar nuestra prueba vamos a realizar una peticón GET al segundo contenedor que tiene el puerto 32771

curl -i -H "Cookie: SESSION=6f20cb24-cb49-4647-84a1-b0da57ac86eb"  $(docker-machine ip dev):32771/springSession/contadorServlet

Si todo esta bien, al ejecutar el comando veremos que nuestro contador tiene valor 1.

Con esto termina nuestra prueba con Spring Session.

6. Conclusiones

Como hemos visto, se puede integrar de una manera fácil y rápida Spring Session en nuestra aplicaciones y poder escalar nuestra aplicaciones sin necesidad de instalar un sistema de replicación de sesion en nuestro sevidores.

La idea detrás de Spring sesión es bastante sencillo :

  • Crear un nuevo filtro de servlet
  • Añadir el filtro a la cadena de filtros de nuestro servlet
  • Conecte el filtro a la conexión Redis

Espero que el tutorial os haya animado a usar (o al menos probar) esta nuevo proyecto de Spring.

7. Referencias