今天閒來無時,練習了一下Spring mvc +mybaties + druid 搭建多數據源。
下面使用了mysql 和 postgresql 爲例,話不多說,直接看代碼:
配置文件
jdbc.properties
mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://localhost:3306/jazz?characterEncoding=utf-8
mysql.jdbc.username=root
mysql.jdbc.password=root
postgresql.jdbc.driverClassName=org.postgresql.Driver
postgresql.jdbc.url=jdbc:postgresql://localhost:5432/BJC20161122
postgresql.jdbc.username=myerp
postgresql.jdbc.password=godblessme
spring-mvc-applicatonContext.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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd"
>
<!-- 定義受環境影響易變的變量,例如數據庫配置屬性文件,應用程序屬性文件等 -->
<context:property-placeholder location="classpath:*.properties"/>
<!-- component-scan自動搜索@Component , @Controller , @Service , @Repository等標註的類 -->
<context:component-scan base-package="com.**.service,bean" />
<!-- 配置DataSource數據源 -->
<bean id="mysqlDataSourceSpied" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 數據庫連接 -->
<property name="driverClassName" value="${mysql.jdbc.driverClassName}" />
<property name="url" value="${mysql.jdbc.url}" />
<property name="username" value="${mysql.jdbc.username}" />
<property name="password" value="${mysql.jdbc.password}" />
<property name="filters" value="stat"/>
<!-- 連接池設置 -->
<property name="initialSize" value="2" />
<property name="maxActive" value="100" />
<property name="maxWait" value="30000" />
<property name="poolPreparedStatements" value="false" />
<property name="defaultAutoCommit" value="false" />
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x' from dual"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
</bean>
<bean id="mysqlDataSource" class="net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy">
<constructor-arg ref="mysqlDataSourceSpied" />
</bean>
<!-- 配置DataSource數據源 -->
<bean id="postgresqlDataSourceSpied" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 數據庫連接 -->
<property name="driverClassName" value="${postgresql.jdbc.driverClassName}" />
<property name="url" value="${postgresql.jdbc.url}" />
<property name="username" value="${postgresql.jdbc.username}" />
<property name="password" value="${postgresql.jdbc.password}" />
<property name="filters" value="stat"/>
<!-- 連接池設置 -->
<property name="initialSize" value="2" />
<property name="maxActive" value="100" />
<property name="maxWait" value="30000" />
<property name="poolPreparedStatements" value="false" />
<property name="defaultAutoCommit" value="false" />
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
</bean>
<bean id="postgresqlDataSource" class="net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy">
<constructor-arg ref="postgresqlDataSourceSpied" />
</bean>
<bean id="dataSource" class="com.jazz.first.datasource.DynamicDataSource">
<property name="defaultTargetDataSource" ref="mysqlDataSource" />
<property name="targetDataSources">
<map key-type="com.jazz.first.datasource.SourcesEnum">
<entry key="mysql" value-ref="mysqlDataSource"/>
<entry key="postgresql" value-ref="postgresqlDataSource"/>
<!-- 這裏還可以加多個dataSource -->
</map>
</property>
</bean>
<!-- SqlSesstion Factory 定義 ↓ mybatis文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 自動掃描entity目錄,省略Configuration.xml裏手工配置 -->
<property name="mapperLocations" value="classpath*:/com/**/dao/mapper/*Mapper.xml"/>
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageHelper">
<property name="properties">
<value>
pageSizeZero=true
rowBoundsWithCount=true
reasonable=true
offsetAsPageNum=true
params=pageNum=offset;pageSize=limit;
supportMethodsArguments=true
returnPageInfo=check
</value>
</property>
</bean>
</array>
</property>
</bean>
<!-- SqlSession Factory 定義 ↑ -->
<!-- MapperScannerConfigurer定義,用於掃描Mapper DAO類 ↓ -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.**.dao.**"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- MapperScannerConfigurer定義,用於掃描Mapper DAO類 ↑ -->
<!-- 數據庫事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- ↓ ↓ 使用註解事務 ↓ ↓ -->
<!-- <tx:annotation-driven transaction-manager="prjTxManager" proxy-target-class="true"/> -->
<!-- ↑ ↑ 使用註解事務 ↑ ↑ -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="create*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="crud*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="schedule*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="do*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:aspectj-autoproxy proxy-target-class="true" />
<aop:config>
<aop:advisor id="managerTx" advice-ref="txAdvice" pointcut="execution(* *..service..*.*(..))" order="2"/>
</aop:config>
<bean id="dataSourceAspect" class="com.jazz.first.datasource.DataSourceAspect" />
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="1">
<aop:pointcut id="tx" expression="execution(* *..service..*.*(..)) "/>
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>
<!-- 公共dao -->
<context:component-scan base-package="com.**.dao" />
</beans>
注意:由於我使用的註解式事務,和我們的AOP數據源切面有一個順序的關係。數據源切換必須先執行,數據庫事務才能獲取到正確的數據源。所以要明確指定 註解式事務 order=2和 我們AOP數據源切面order=1。
AbstractRoutingDataSource 是spring提供的一個多數據源抽象類。spring會在使用事務的地方來調用此類的determineCurrentLookupKey()
方法來獲取數據源的key值。我們繼承此抽象類並實現此方法:
package com.jazz.first.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return HandleDataSource.getDataSource();
}
}
DataSourceRouter
類中通過HandleDataSource.getDataSource()
獲取數據源的key值。此方法應該和線程綁定。
package com.jazz.first.datasource;
/**
* 線程相關的數據源處理類
* @author
*/
public class HandleDataSource {
// 數據源名稱線程池
private static final ThreadLocal<SourcesEnum> contextHolder = new ThreadLocal<SourcesEnum>();
/**
* 獲取數據源
* @return 數據源名稱
*/
public static void setDataSource(SourcesEnum dbType) {
contextHolder.set(dbType);
}
/**
* 獲取數據源
* @return 數據源名稱
*/
public static SourcesEnum getDataSource() {
return contextHolder.get();
}
/**
* 清空數據源
*/
public static void clearDbType() {
contextHolder.remove();
}
}
對於spring來說,註解即簡單方便且可讀性也高。所以,我們也通過註解在service的方法前指定所用的數據源。我們先定義自己的註解類,其中value爲數據源的key值。
package com.jazz.first.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 數據源註解類
* @author
*
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
SourcesEnum value();
}
通過枚舉來對應不同的數據源,這裏一定得與配置文件中的key相同。
package com.jazz.first.datasource;
public enum SourcesEnum {
mysql,postgresql
}
最後,我們可以通過AOP攔截所有service方法,在方法執行之前獲取方法上的註解:即數據源的key值。
package com.jazz.first.datasource;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 切換數據源(不同方法調用不同數據源)
*/
@Aspect
@Component
@Order(1) //請注意:這裏order一定要小於tx:annotation-driven的order,即先執行DataSourceAspect切面,再執行事務切面,才能獲取到最終的數據源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 切入點 service包及子孫包下的所有類
*/
@Pointcut("execution(* *..service..*.*(..))")
public void aspect() {
}
/**
* 配置前置通知,使用在方法aspect()上註冊的切入點
*/
@Before("aspect()")
public void before(JoinPoint point) {
Class<?> target = point.getTarget().getClass();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod() ;
DataSource dataSource = null ;
//從類初始化
dataSource = this.getDataSource(target, method) ;
//從接口初始化
if(dataSource == null){
for (Class<?> clazz : target.getInterfaces()) {
dataSource = getDataSource(clazz, method);
if(dataSource != null){
break ;//從某個接口中一旦發現註解,不再循環
}
}
}
if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
HandleDataSource.setDataSource(dataSource.value());
}
}
@After("aspect()")
public void after(JoinPoint point) {
//使用完記得清空
HandleDataSource.setDataSource(null);
}
/**
* 獲取方法或類的註解對象DataSource
* @param target 類class
* @param method 方法
* @return DataSource
*/
public DataSource getDataSource(Class<?> target, Method method){
try {
//1.優先方法註解
Class<?>[] types = method.getParameterTypes();
Method m = target.getMethod(method.getName(), types);
if (m != null && m.isAnnotationPresent(DataSource.class)) {
return m.getAnnotation(DataSource.class);
}
//2.其次類註解
if (target.isAnnotationPresent(DataSource.class)) {
return target.getAnnotation(DataSource.class);
}
} catch (Exception e) {
e.printStackTrace();
logger.error(MessageFormat.format("通過註解切換數據源時發生異常[class={0},method={1}]:"
, target.getName(), method.getName()),e) ;
}
return null ;
}
}
這樣多數據源就搭建起來了,我們只需要在每個service方法前使用
@DataSource("數據源key")
註解即可。
總結:此框架弊端顯而易見,就是在一個service方法中只能訪問同一個數據庫,不能進行切換。當然好處事比較靈活,可以配置多個數據源,方式一致,service方法上加上註解後則切換到指定數據源,方法執行完後,則回答原始數據源。