今天闲来无时,练习了一下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方法上加上注解后则切换到指定数据源,方法执行完后,则回答原始数据源。