En este tutorial veremos como utilizar la clase AbstractRoutingDataSource que proporciona spring.
Índice de contenidos
- 1. Introducción
- 2. Motivación
- 3. Entorno
- 4. Creación del proyecto
- 5. Ejemplo práctico
- 6. Conclusiones
- 7. Referencias
1. Introducción
En este tutorial vamos a ver como utilizar la clase AbstractRoutingDataSource de spring, que se encuentra el módulo de spring-jdbc, esta clase esta disponible desde la versión 2.0.1 de spring.
¿Qué nos proporciona esta clase?
Pues nos permite cambiar de datasource en tiempo de ejecución.
¿Cómo funciona?
Muy fácil esta clase actúa como intermediario, mientras que el datasource «real» se puede determinar de forma dinámica en tiempo de ejecución en base a una clave de búsqueda.
2. Motivación
Debido a la gran demanda de gente preguntando como hacer para poder cambiar de datasource en tiempo de ejecución. Mucha gente ha creado sus propias soluciones, la gente de spring han sacado una solución para este problema.
Este tutorial vamos hacer un pequeño ejemplo que tenemos 2 bases de datos donde guardamos los equipos de fútbol de cada país, mediante un servicio rest que vamos a obtener todos los equipos pasando como parámetro el código del país
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 Yosomite 10.10.5
- NVIDIA GeForce GT 750M 2048 MB
- Eclipse Luna 4.4.2
- Java 1.8
- Docker 1.9.0
- Spring Framework 4.1.6.RELEASE
- Spring MVC 4.1.6.RELEASE
- Tomcat 8.0.28
- curl 7.43.0
4. Creación del proyecto
Vamos a crear una proyecto web para exponer nuestro servicio rest, para facilitar el ejemplo vamos a utilizar bases de datos en memoria, en concreto H2 y HSQL
Lo primero es crear un proyecto web con maven y añadimos las dependencias de Spring y Spring MVC, 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>springDynamicDataSource</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> <fasterxml.jackson.version>2.6.3</fasterxml.jackson.version> <mybatis.version>3.2.8</mybatis.version> <mybatis.spring.version>1.2.2</mybatis.spring.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> </properties> <build> <finalName>springDynamicDataSource</finalName> <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</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- ======================== Spring ============================ --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- ======================== Mybatis ============================ --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.3.157</version> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.2.9</version> </dependency> <!-- ======================== JSON ============================ --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${fasterxml.jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${fasterxml.jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${fasterxml.jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jdk8</artifactId> <version>${fasterxml.jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${fasterxml.jackson.version}</version> </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> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</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>
Para facilitar el ejemplo solo vamos a ver el código que se refiere a la clase AbstractRoutingDataSource, el resto estará disponible en el zip de descarga.
CountryRoutingDatasource.java
package es.autentia.spring.util; import org.springframework.jdbc.datasource.lookup.*; public class CountryRoutingDatasource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return CountryDbContextHolder.getDbType(); } }
Como vemos solo tenemos que implemetar el método «determineCurrentLookupKey» donde tenemos que devolver la key del map que contiene AbstractRoutingDataSource, en este caso tenemos definido un CountryDbContextHolder que guarda en un ThreadLocal la key que va usar.
Lo siguiente es ver la configuración de la clase CountryRoutingDatasource en fichero de configuración de spring
configuration-spring-datasource.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" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> <!-- Datasource --> <bean id="dataSource" class="es.autentia.spring.util.CountryRoutingDatasource"> <property name="targetDataSources"> <map key-type="es.autentia.spring.model.Country"> <entry key="SPAIN" value-ref="spainDataSource" /> <entry key="GERMANY" value-ref="germanyDataSource" /> </map> </property> <property name="defaultTargetDataSource" ref="spainDataSource" /> </bean> <!-- Datasource embedded Spanish --> <jdbc:embedded-database id="spainDataSource" type="H2"> <jdbc:script location="classpath:bd/schema/schema_spain.sql" /> <jdbc:script location="classpath:bd/data/data_spain.sql" /> </jdbc:embedded-database> <!-- Datasource embedded Germany --> <jdbc:embedded-database id="germanyDataSource" type="HSQL"> <jdbc:script location="classpath:bd/schema/schema_germany.sql" /> <jdbc:script location="classpath:bd/data/data_germany.sql" /> </jdbc:embedded-database> </beans>
Como vemos al definir el bean de dataSource (CountryRoutingDatasource) le pasamos a la propiedad targetDataSources que es un map que contiene todos los datasource, en este caso son 2 spainDataSource y germanyDataSource
Otra propiedad importante pero no obligatoria que es defaultTargetDataSource, que en caso de no obtener una key válida en el método determineCurrentLookupKey entonces usar por defecto ese dataSource
Por último vamos a ver nuestro servicio rest
web.xml
package es.autentia.spring.controller; import java.util.*; import java.util.stream.*; import org.springframework.beans.factory.annotation.*; import org.springframework.web.bind.annotation.*; import es.autentia.spring.model.*; import es.autentia.spring.service.*; import es.autentia.spring.util.*; @RestController @RequestMapping(value = "/team") public class TeamController { private static final String COUNTRY = "country"; private final TeamService teamService; private final Map<String, League> mapLeagues; @Autowired public TeamController(TeamService teamService) { this.teamService = teamService; this.mapLeagues = Stream.of(League.values()).collect(Collectors.toMap(League::getCode, item -> item)); } @RequestMapping(method = RequestMethod.GET) public List<Team> getTeams(@RequestParam(required = false, value = COUNTRY) String code) { CountryDbContextHolder.setDbType(mapLeagues.get(code.toUpperCase())); return teamService.getTeams(); } }
Como vemos obtenemos el código del país por medio de una RequestParam y se lo pasamos a nuestro CountryDbContextHolder la key del país para seleccionar correctamente el datasource de ese país.
5. Ejemplo práctico
Para relizar el ejemplo práctico vamos a utilizar docker para desplegar nuestra aplicación en un contendor con tomcat y java 8.
Lo primero es crear nuestro war con el siguiente comando
mvn clean package
Una vez creado nuestro war, procedemos a ejecutar nuestro war en contenedor de docker
docker run -d -p 8080:8080 --name springDynamicDataSource -v `pwd`/target:/usr/local/tomcat/webapps tomcat:8.0.28-jre8
Para comprobar si ha desplegado correctamente vamos a ver el log del contenedor, con el siguiente comando.
docker logs springDynamicDataSource
Lo primero es ejecutar con el comando curl para obtener los equipos de España
curl `docker-machine ip dev`:8080/springDynamicDataSource/rest/team?country=ES
Donde “dev” es el nombre de la máquina virtual donde se esta ejecutando docker.
Ahora vamos a obtener los equipos de Alemania
curl `docker-machine ip dev`:8080/springDynamicDataSource/rest/team?country=DE
Por último vamos hacer una petición sin pasar el country
curl `docker-machine ip dev`:8080/springDynamicDataSource/rest/team
Como vemos obtenemos los esquipos de España como es lógico al no pasarle ningun código, el CountryRoutingDatasource va usar el datasource por defecto, en caso de no tener definido provocaria un IllegalStateException.
Con esto termina nuestra prueba con la clase AbstractRoutingDataSource.
6. Conclusiones
Como hemos visto spring no proporciona una clase para poder cambiar de dataSource en tiempo de ejecución de forma fácil y rápida. Solo implementado la clase AbstractRoutingDataSource
Espero que el tutorial os haya animado a usar (o al menos probar) esta utilidad que nos ofrece spring.
Puedes descargar el ejemplo de este tutorial desde aquí
Excelente ejemplo, es bueno saber que Spring ofrece soluciones para este tipo de casos, relmente es muy triste ver workarounds que penden de un hilo