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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
<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
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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
1 |
mvn clean package |
Una vez creado nuestro war, procedemos a ejecutar nuestro war en contenedor de docker
1 |
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.
1 |
docker logs springDynamicDataSource |
Lo primero es ejecutar con el comando curl para obtener los equipos de España
1 |
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
1 |
curl `docker-machine ip dev`:8080/springDynamicDataSource/rest/team?country=DE |
Por último vamos hacer una petición sin pasar el country
1 |
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