需求:项目使用了读写分离,或者数据进行了分库处理,我们希望在操作不同的数据库的时候,我们的程序能够动态的切换到相应的数据库,执行相关的操作。
首先,你需要一个能够正常运行的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); } }
测试用例这里就不赘述了,详情可以参考下面的源码。
项目源码地址:github.com/Apedad/spri…
最后,有个问题需知,此处事务只能回滚默认数据源操作,如果需要回滚其他数据源的操作请使用分布式事务进行处理。