需求:项目使用了读写分离,或者数据进行了分库处理,我们希望在操作不同的数据库的时候,我们的程序能够动态的切换到相应的数据库,执行相关的操作。
首先,你需要一个能够正常运行的springboot项目,配置mybatis并且能够正常的操作数据库(增删查改)
现在开始实现:
思路:现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了。
虽然思路是有了,但是怎么换呢?
爬了很多的博客查看了下官方的文档,我找到了这样一个类:AbstractRoutingDataSource,它继承于AbstractDataSource而AbstractDataSource又实现了DataSource接口,是一个标准的数据源。
查看AbstractRoutingDataSource类:
/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
//翻译结果如下
/**
* 抽象 {@link javax.sql.DataSource} 路由 {@link #getConnection ()} 的实现
* 根据查找键调用不同的目标数据之一。后者通常是
* (但不一定) 通过某些线程绑定事务上下文来确定。
*
* @author 史塔克 Hoeller
* @since 2.0。1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey ()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
.......
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*/
//翻译如下
/**
*指定目标数据源的映射,查找键为键。
*映射的值可以是相应的{@link javax.sql.DataSource}
*实例或数据源名称字符串(要通过
* {@link #setDataSourceLookup DataSourceLookup})。
*键可以是任意类型的; 这个类实现了
*通用查找过程只。 具体的关键表示将
*由{@link #resolveSpecifiedLookupKey(Object)}和
* {@link #determineCurrentLookupKey()}。
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
......
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
//翻译如下
/**
* 确定当前的查找键。这通常会
* 实现以检查线程绑定的事务上下文。
* <p> 允许任意键。返回的密钥需要
* 与存储的查找密钥类型匹配, 如
* {@link #resolveSpecifiedLookupKey} 方法。
*/
protected abstract Object determineCurrentLookupKey();
}
我这里只是选择性的贴出了部分源码,详细信息大家可以在IDE里直接点进去查看。翻译的很鸡肋,但是可以明白,这个类的基本运作方式,它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey()方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换,这个时候你会又疑问,我没设置数据源,它是怎么切换的?上面我贴出了另外一个核心的方法setTargetDataSources(Map<Object, Object> targetDataSources),它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。
基本的原理已经说完了,接下来去项目里面配置就OK。
1、创建枚举类DataSourceKey列出你所有的数据源名称,当然了,类名你可以按照自己的取名习惯,下面所有的类也是如此。
public enum DataSourceKey {
DB_MASTER,
DB_SLAVE1,
DB_SLAVE2,
DB_OTHER
}
2、创建DynamicDataSourceContextHolder类,这个类是为了解决多线程访问全局变量的问题。
import org.apache.commons.lang3.RandomUtils;
import org.apache.log4j.Logger;
/**
* @author RocLiu [apedad@qq.com]
* @version 1.0
*/
public class DynamicDataSourceContextHolder {
private static final Logger LOG = Logger.getLogger(DynamicDataSourceContextHolder.class);
private static final ThreadLocal<DataSourceKey> currentDatesource = new ThreadLocal<>();
/**
* 清除当前数据源
*/
public static void clear() {
currentDatesource.remove();
}
/**
* 获取当前使用的数据源
*
* @return 当前使用数据源的ID
*/
public static DataSourceKey get() {
return currentDatesource.get();
}
/**
* 设置当前使用的数据源
*
* @param value 需要设置的数据源ID
*/
public static void set(DataSourceKey value) {
currentDatesource.set(value);
}
/**
* 设置从从库读取数据
* 采用简单生成随机数的方式切换不同的从库
*/
public static void setSlave() {
if (RandomUtils.nextInt(0, 2) > 0) {
DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE2);
} else {
DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE1);
}
}
}
3、创建类DynamicRoutingDataSource继承AbstractRoutingDataSource类并且实现determineCurrentLookupKey()方法,设置数据源。
import org.apache.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private static final Logger LOG = Logger.getLogger(DynamicRoutingDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
LOG.info("当前数据源:{}"+ DynamicDataSourceContextHolder.get());
return DynamicDataSourceContextHolder.get();
}
}
4、配置数据源,这一步比较重要,创建配置类DynamicDataSourceConfiguration
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.apedad.example.commons.DataSourceKey;
import com.apedad.example.commons.DynamicRoutingDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@MapperScan(basePackages = "com.apedad.example.dao")
@Configuration
public class DynamicDataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "multiple.datasource.master")//此处的"multiple.datasource.master"需要你在application.properties中配置,详细信息看下面贴出的application.properties文件。
public DataSource dbMaster() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "multiple.datasource.slave1")
public DataSource dbSlave1() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "multiple.datasource.slave2")
public DataSource dbSlave2() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "multiple.datasource.other")
public DataSource dbOther() {
return DruidDataSourceBuilder.create().build();
}
/**
* 核心动态数据源
*
* @return 数据源实例
*/
@Bean
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setDefaultTargetDataSource(dbMaster());
Map<Object, Object> dataSourceMap = new HashMap<>(4);
dataSourceMap.put(DataSourceKey.DB_MASTER, dbMaster());
dataSourceMap.put(DataSourceKey.DB_SLAVE1, dbSlave1());
dataSourceMap.put(DataSourceKey.DB_SLAVE2, dbSlave2());
dataSourceMap.put(DataSourceKey.DB_OTHER, dbOther());
dataSource.setTargetDataSources(dataSourceMap);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
//此处设置为了解决找不到mapper文件的问题
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
/**
* 事务管理
*
* @return 事务管理实例
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
5、为了不影响业务代码而实现数据源切换,我决定使用AOP切换数据源,为了准确的知道哪个地方需要切换哪个数据源,我这里使用自定义注解的方式,如果你又更好的方式也推荐你使用自己的方式。创建自定义注解类:TargetDataSource:
import com.apedad.example.commons.DataSourceKey;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
DataSourceKey dataSourceKey() default DataSourceKey.DB_MASTER;
}
6、编写数据源切换切面类:DynamicDataSourceAspect
import com.apedad.example.annotation.TargetDataSource;
import org.apache.log4j.Logger;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {
private static final Logger LOG = Logger.getLogger(DynamicDataSourceAspect.class);
@Pointcut("execution(* com.apedad.example.service.*.list*(..))")
public void pointCut() {
}
/**
* 执行方法前更换数据源
*
* @param joinPoint 切点
* @param targetDataSource 动态数据源
*/
@Before("@annotation(targetDataSource)")
public void doBefore(JoinPoint joinPoint, TargetDataSource targetDataSource) {
DataSourceKey dataSourceKey = targetDataSource.dataSourceKey();
if (dataSourceKey == DataSourceKey.DB_OTHER) {
LOG.info(String.format("设置数据源为 %s", DataSourceKey.DB_OTHER));
DynamicDataSourceContextHolder.set(DataSourceKey.DB_OTHER);
} else {
LOG.info(String.format("使用默认数据源 %s", DataSourceKey.DB_MASTER));
DynamicDataSourceContextHolder.set(DataSourceKey.DB_MASTER);
}
}
/**
* 执行方法后清除数据源设置
*
* @param joinPoint 切点
* @param targetDataSource 动态数据源
*/
@After("@annotation(targetDataSource)")
public void doAfter(JoinPoint joinPoint, TargetDataSource targetDataSource) {
LOG.info(String.format("当前数据源 %s 执行清理方法", targetDataSource.dataSourceKey()));
DynamicDataSourceContextHolder.clear();
}
@Before(value = "pointCut()")
public void doBeforeWithSlave(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取当前切点方法对象
Method method = methodSignature.getMethod();
if (method.getDeclaringClass().isInterface()) {//判断是否为借口方法
try {
//获取实际类型的方法对象
method = joinPoint.getTarget().getClass()
.getDeclaredMethod(joinPoint.getSignature().getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
LOG.error("方法不存在!", e);
}
}
if (null == method.getAnnotation(TargetDataSource.class)) {
DynamicDataSourceContextHolder.setSlave();
}
}
}
7、在springboot程序运行入口中设置取消自动配置数据源
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SpringBootDynamicDatasourceStartedApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDynamicDatasourceStartedApplication.class, args);
}
}
8、使用,在你需要切换数据源的service方法上加上注解就OK,注意:如果你使用了接口对service层进行分离,那么注解需要添加到你的实现类的相关方法上。示例如下
@Service("userInfoService")
public class UserInfoServiceImpl implements UserInfoService {
private static final Logger LOG = Logger.getLogger(UserInfoServiceImpl.class);
@Resource
private UserInfoMapper userInfoMapper;
@TargetDataSource(dataSourceKey = DataSourceKey.DB_OTHER)
@Override
public List<UserInfo> listAll() {
return userInfoMapper.listAll();
}
//使用此注解来切换到想切换的数据源
@TargetDataSource(dataSourceKey = DataSourceKey.DB_OTHER)
@Override
public int insert(UserInfo userInfo) {
return userInfoMapper.insert(userInfo);
}
}
测试用例这里就不赘述了,详情可以参考下面的源码。
项目源码地址:https://github.com/Apedad/spring-boot-dynamic-datasource-started
最后,有个问题需知,此处事务只能回滚默认数据源操作,如果需要回滚其他数据源的操作请使用分布式事务进行处理
————————————————
转载自「twomr」