Java+Spring+MyBatis实现多数据源的动态切换

在实际的项目开发过程中,我们经常会遇到一个项目需要使用多个数据源的情况,而多数据源又可分为固定多数据源和动态多数据源。

固定多数据源:是指在项目中需要使用多个数据源,但数据源的个数是确定的,不会改变,如我们的项目需要使用订单库和商品库这两个数据源,项目中所有的业务逻辑都只需要操作这两个库。

动态多数据源:是指在项目需要使用多数据源,但是数据源的个数不确定,可能会随着项目的需要动态的新增或删除数据源。

下面我将会对这两种情况分别说明如何通过Java + Spring实现多数据源的动态切换。

多数据源的动态切换都是通过重载Spring中的AbstractRoutingDataSource类来实现的。

一、固定多数据源切换

固定多数据源的动态切换,通过自定义注解实现切换,这样在切换数据源时比较灵活,具体的实现方式如下:

1、配置多数据源

 <!--定义数据源1-->
<bean id="oracledataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1522:neworcl" />
<property name="username" value="emspdadev" />
<property name="password" value="emspdadev" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="0"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="20"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="20"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="1"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000"></property>
</bean>

<!--定义数据源2-->
<bean id="mysqldataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/jbpmdb" />
<property name="username" value="root" />
<property name="password" value="123456" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="0"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="20"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="20"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="1"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000"></property>
</bean>

<!--动态数据源配置-->
<bean id="dataSource" class="com.ssm.datasource.DynamicDataSource">
<!--引入定义好的数据源-->
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="oracle" value-ref="oracledataSource" />
<entry key="mysql" value-ref="mysqldataSource" />
</map>
</property>
<!--定义默认数据源-->
<property name="defaultTargetDataSource" ref="oracledataSource" />
</bean>

<!--spring和mybatis整合-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:mapping/*.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ssm.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

2、定义注解(注解名为DataSource),用于切换数据源,注解的值只能为上述配置中定义的key(对应于上面配置中定义的oracle、mysql)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value();
}

3、根据Sping切面编程,当调用指定的切面类时,解释注解,并根据注解的定义使用对应的数据库

public class DataSourceAspect {

/**
* 定义切面,当调用com.ssm.service下的所有类的所有方法前都会执行beforeInvoke方法
*/
@Pointcut("execution(* com.ssm.service.*.*(..))")
public void pointCut(){};

@Before(value = "pointCut()")
public void beforeInvoke(JoinPoint joinpoint) {
try {
String clazzName = joinpoint.getTarget().getClass().getName();
String methodName = joinpoint.getSignature().getName();
Class targetClazz = Class.forName(clazzName);
Method[] methods = targetClazz.getMethods();
for(Method method : methods) {
if(method.getName().equals(methodName)) {
// 首先查看方法是否使用注解
// 如果使用注解,则获取注解定义的值,并根据注解的值设置访问数据库的key
if(method.isAnnotationPresent(DataSource.class)) {
DataSource dataSource = method.getAnnotation(DataSource.class);
DatasourceHolder.setDataType(dataSource.value());
}
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}

}
}

4、定义动态切换数据源(继承Spring的AbstractRoutingDataSource)

public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 根据DatasourceHolder中DataType的值获取具体的数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DatasourceHolder.getDataType();
}
}

5、数据源切换的使用

@Service
public class IdxServiceImpl implements IIdxSevice {

@Autowired
private IdxMapper idxMapper;

@Override
public List<Idx> listIdxInfo() {
return null;
}

/**
* 根据注解的配置,会访问oracle对应的数据源
*/
@Override
@DataSource("oracle")
public Map<String,Object> getIdxById(int idxId) {
return idxMapper.getIdxById(idxId);
}

/**
* 根据注解的配置,会访问mysql对应的数据源
*/
@Override
@DataSource("mysql")
public Map<String, Object> getJobInfo(int dbId) {
return idxMapper.getJobInfo(dbId);
}
}

通过以上的步骤即实现了数据源的动态切换。

二、动态多数据源切换

对于动态的多数据源,数据源的配置一般不放在配置文件中,因为如果放在配置文件中,每次新增或删除数据源,都需要重启项目,这样的实现方式非常不友好;通常情况向数据源的配置放在数据库中。实现方式如下:

1、配置数据源,这里配置的数据源用于保存其他数据源的配置信息,今后数据的新增、删除、修改均在该数据库中操作,配置如下:

<!--定义数据源-->
<bean id="oracledataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1522:neworcl" />
<property name="username" value="cfgmanage" />
<property name="password" value="cfgmanage" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="0"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="20"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="20"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="1"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000"></property>
</bean>

<!--查询动态配置的数据库连接信息-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="oracledataSource" />
</bean>
<bean id="dbConfigService" class="com.teamsun.datasource.DBConfigService">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>

<!--定义动态数据源-->
<bean id="dataSource" class="com.teamsun.datasource.DynamicDataSource">
<property name="masterDataSource" ref="oracledataSource" />
<property name="dbConfigService" ref="dbConfigService" />
</bean>

<!--spring和mybatis整合-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
<!--<property name="mapperLocations" value="classpath:mapping/*.xml" />-->
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.teamsun.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

2、实现查询数据源配置信息的类

public class DBConfigService {

private JdbcTemplate jdbcTemplate;

/**
* 查询数据库配置信息
* @param dbName 数据库名称
* @return 数据库配置信息
*/
public DBCfg getDBCfg(String dbName) throws Exception {
String querySql = "select\n" +
" t.db_type as \"dbType\",\n" +
" t.db_name as \"dbName\",\n" +
" t.db_comment as \"dbCommment\",\n" +
" t.db_driver as \"driverClass\",\n" +
" t.db_username as \"userName\",\n" +
" t.db_password as \"passworld\",\n" +
" t.db_url as \"jdbcURL\"" +
" from TB_RPT_DBCFG t\n" +
" where t.db_name = '" + dbName + "'";

RowMapper<DBCfg> rowMapper = ParameterizedBeanPropertyRowMapper.newInstance(DBCfg.class);
DBCfg dbCfg = (DBCfg) jdbcTemplate.queryForObject(querySql, rowMapper);
return dbCfg;
}

public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

3、实现动态切换数据源

/**
* <p>动态创建及访问多数据源</p>
*/
public class DynamicDataSource extends AbstractRoutingDataSource{

private DBConfigService dbConfigService;

private DataSource masterDataSource;

private Map<Object, Object> targetDataSource = new HashMap<Object, Object>();

private static final String DEFAULT_DB_NAME = "dataSource"; // 默认数据库名

private static final Logger LOGGER = Logger.getLogger(DynamicDataSource.class);

/**
* 创建并获取数据源
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
// 获取数据源名称
String dbName = (String) determineCurrentLookupKey();

// 获取默认数据源
if(DEFAULT_DB_NAME.equals(dbName)) {
return masterDataSource;
}

// 创建数据源
DataSource dataSource = (DataSource) targetDataSource.get(dbName);
try {
if (dataSource == null) {
dataSource = getDataSourceByName(dbName);
}
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}

/**
* 获取数据库名称,可根据获取的数据库名称查询数据库配置信息,
* 通过配置信息动态创建数据源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
String dbName = DatasourceHolder.getDBName();
if(StringUtils.isEmpty(dbName)) {
dbName = DEFAULT_DB_NAME;
}

DatasourceHolder.remove();
return dbName;
}

@Override
public void afterPropertiesSet() {

}

/**
* 通过数据库的配置信息获取数据源
* @param dbName 数据库名称
* @return
*/
public synchronized DataSource getDataSourceByName(String dbName) throws Exception {

// 创建数据源
BasicDataSource dataSource = createDataSource(dbName);

// 如果创建数据源成功则缓存数据源,避免重复创建相同的数据源
if(dataSource != null) {
targetDataSource.put(dbName, dataSource);
}
return dataSource;
}

/**
* 通过数据库的配置创建数据源
* @param dbName 数据库名称
* @return
*/
public BasicDataSource createDataSource(String dbName) throws Exception {

// 查询动态数据源配置信息
String oriDBName = DatasourceHolder.getDBName();

if(dbConfigService == null) {
System.out.println("创建数据源失败[dbCfgService is null......]");
LOGGER.debug("创建数据源失败[dbCfgService is null......]");
}

// 通过数据库名称查询相关的数据库配置信息
DatasourceHolder.setDBName(DEFAULT_DB_NAME);
DBCfg dbCfg = dbConfigService.getDBCfg(dbName);
DatasourceHolder.setDBName(oriDBName);

String driver = dbCfg.getDriverClass(); // 数据库驱动
String url = dbCfg.getJdbcURL(); // 数据库连接地址
String username = dbCfg.getUserName(); // 数据库用户名
String password = dbCfg.getPassworld(); // 数据库密码

LOGGER.debug("动态连接的数据库为[" + url + "|" + username + "]");

// 创建数据源
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName(driver);
basicDataSource.setUrl(url);
basicDataSource.setUsername(username);
basicDataSource.setPassword(password);
basicDataSource.setTestWhileIdle(true);

return basicDataSource;
}

/**
* 如果修改或删除数据源的配置,则需要同步删除缓存的数据源
* @param dbName
*/
public void removeDataSource(String dbName) {
this.targetDataSource.remove(dbName);
}

public DataSource getMasterDataSource() {
return masterDataSource;
}

public void setMasterDataSource(DataSource masterDataSource) {
this.masterDataSource = masterDataSource;
}

public DBConfigService getDbConfigService() {
return dbConfigService;
}

public void setDbConfigService(DBConfigService dbConfigService) {
this.dbConfigService = dbConfigService;
}
}

4、使用动态切换数据源

public class ShowRptServiceImpl implements IShowRptService {

private static final Logger LOGGER = Logger.getLogger(ShowRptServiceImpl.class);

@Autowired
private DBCfgMapper dbCfgMapper;

@Autowired
private ShowRptInfoMapper showRptInfoMapper;

@Override
public RptResult queryRptInfo(BaseRpt baseRpt, Map<String, String> params) {
// 在调用Mybatis执行数据库之前先选择数据源
DatasourceHolder.setDBName(dbCfg.getDbName());
// 查询报表数据
List<Map<String,Object>> resultList = showRptInfoMapper.queryRptData(querySQL);


// 选择数据源
DatasourceHolder.setDBName(dbCfg.getDbName());
// 查询数据数据量
int totalCount = showRptInfoMapper.queryTotalCount(countSQL);

RptResult rptResult = new RptResult();
return rptResult;
}
}

通过以上步骤即可实现动态多数据源的动态切换

您可能还会对下面的文章感兴趣: