目录:
- 实现思想
- 导入依赖、配置说明
- 代码实现
- 问题总结
一.实现思想
接手一个旧系统,SpringBoot 使用的是纯粹的 mybatis ,既没有使用规范的代码生成器,也没有使用 JPA 或者 mybatis-plus。
想着接入 mybatis-plus,为以后敲代码省点力气。普通的接入 mybatis-plus 可以直接参考官方文档 https://mp.baomidou.com/ 。
但我接手的系统是个多数据源系统,本来最优的方法是使用官方的 动态数据源 支持 https://mp.baomidou.com/guide/dynamic-datasource.html 。
但我因为乱七八糟的依赖冲突,决定自己实现 动态数据源 的支持。
实现的核心逻辑:使用一个 代理数据源,来管理 其他数据源 的分发请求。(通过AOP分发)
二.导入依赖、配置说明
因为依赖的冲突,我没有直接使用
com.baomidou mybatis-plus-boot-starter 3.3.1.tmp
而是引入的
com.baomidou mybatis-plus 3.3.1.tmp
数据库配置文件大致如下
spring: datasource: #数据库配置 primary: #数据库1 driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql:// username: root password: 3 type: com.alibaba.druid.pool.DruidDataSource second: #数据库2 driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql:// username: root password: 3 type: com.alibaba.druid.pool.DruidDataSource third: #数据库3 driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql:// username: root password: 3 type: com.alibaba.druid.pool.DruidDataSource
MybatisPlus 配置大致如下(mybatis 的配置可以删除)
mybatis-plus: # 扫描 mapper.xml mapper-locations: classpath:mapper/*.xml #也可以不配置,在代码中设置 # configuration: # map-underscore-to-camel-case: false
三.代码实现
1.我们先新建 数据源的枚举
public enum DataSourceEnums { PRIMARY("primaryDataSource"), SECOND("secondDataSource"), THIRD("thirdDataSource"); private String value; DataSourceEnums(String value){this.value=value;} public String getValue() { return value; } }
2.用来标记数据源的 注解(在哪里使用哪个数据源)。
/** * @author zhaww * @date 2020/4/14 * @Description .自定义 - 区分数据源的注解 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyDataSource { DataSourceEnums value() default DataSourceEnums.PRIMARY; }
3.动态数据源管理器,继承 AbstractRoutingDataSource
/** * @author zhaww * @date 2020/4/10 * @Description .动态数据源管理器 */public class DataSourceContextHolder extends AbstractRoutingDataSource { private static final ThreadLocalcontextHolder = new InheritableThreadLocal<>(); /** * 重写这个方法,这里返回使用的数据源 key 值 * @return */ @Override protected Object determineCurrentLookupKey() { // log.info("动态切换数据源:" + DataSourceContextHolder.getDataSource()); return contextHolder.get(); } /** * 设置数据源 * @param db */ public static void setDataSource(String db){ contextHolder.set(db); } /** * 取得当前数据源 * @return */ public static String getDataSource(){ return contextHolder.get(); } /** * 清除上下文数据 */ public static void clear(){ contextHolder.remove(); } }
4. mybatis-plus 的配置类
/** * @author zhaww * @date 2020/4/10 * @Description . */ //@EnableTransactionManagement //开启事务 @Configuration @MapperScan(value = {"com.zydd.admin.dao"}) //扫描Mapper 层的类 public class MybatisPlusConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondDataSource") @ConfigurationProperties(prefix = "spring.datasource.second") public DataSource secondDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "thirdDataSource") @ConfigurationProperties(prefix = "spring.datasource.third") public DataSource thirdDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "multipleTransactionManager") @Primary public DataSourceTransactionManager multipleTransactionManager(@Qualifier("multipleDataSource") DataSource dataSource) { // return new MyDataSourceTransactionManager(dataSource); return new DataSourceTransactionManager(dataSource); } /** * 动态数据源配置 * * @return */ @Bean(name = "multipleDataSource") @Primary public DataSource multipleDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource, @Qualifier("secondDataSource") DataSource secondDataSource, @Qualifier("thirdDataSource") DataSource thirdDataSource) { DataSourceContextHolder dynamicDataSource = new DataSourceContextHolder(); Map
5.使用AOP来实现数据源的动态设置。
/** * @author zhaww * @date 2020/4/9 * @Description .AOP通用日志记录、动态数据源分发 */ @Aspect @Component @Slf4j @Order(-100) public class AOP { /** * Controller层路径 */ @Pointcut("within(com.zydd.admin.controller..*)") public void controllerPointcut() { } /** * Service层路径 */ @Pointcut("within(com.zydd.admin.service..*)") public void servicePointcut() { } @Around("servicePointcut()") public Object doServiceLogging(ProceedingJoinPoint joinPoint) throws Throwable { changeDataSource(joinPoint); //检查数据源 } /** * Mapper层拦截,动态切换 mybatisPlus 数据源 */ @Before("execution(* com.zydd.admin.dao.primary..*(..))") public void doAdmin(){ log.info("选择数据源---" + DataSourceEnums.PRIMARY.getValue()); DataSourceContextHolder.setDataSource(DataSourceEnums.PRIMARY.getValue()); } /** * Mapper层拦截,动态切换 mybatisPlus 数据源 */ @Before("execution(* com.zydd.admin.dao.second..*(..))") public void doZYDD(){ log.info("选择数据源---" + DataSourceEnums.SECOND.getValue()); DataSourceContextHolder.setDataSource(DataSourceEnums.SECOND.getValue()); } /** * Mapper层拦截,动态切换 mybatisPlus 数据源 */ @Before("execution(* com.zydd.admin.dao.third..*(..))") public void doDW(){ log.info("选择数据源---" + DataSourceEnums.THIRD.getValue()); DataSourceContextHolder.setDataSource(DataSourceEnums.THIRD.getValue()); } /** * 通过注解 变更数据源 * @param joinPoint */ private void changeDataSource(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); MyDataSource myDataSource = null; //优先判断方法上的注解 if (method.isAnnotationPresent(MyDataSource.class)) { myDataSource = method.getAnnotation(MyDataSource.class); DataSourceContextHolder.setDataSource(myDataSource.value().getValue()); } else if (method.getDeclaringClass().isAnnotationPresent(MyDataSource.class)) { //其次判断类上的注解 myDataSource = method.getDeclaringClass().getAnnotation(MyDataSource.class); DataSourceContextHolder.setDataSource(myDataSource.value().getValue()); } log.info("注解方式选择数据源---" + myDataSource.value().getValue()); } }
注意:我们不但默认通过 Mapper 的路径来切换数据源,还通过 Service 方法层来切换数据源。
因为如果 service 有事务的话,进入service方法的时候,DataSourceTransactionManager 就设置好了默认数据源,就算通过Mapper层重新设置数据源,
DataSourceTransactionManager 的默认数据源还是没有变。
所以在 事务管理器 设置默认数据源之前,就切换数据源,实现动态事务+动态数据源。
6.实际使用,只要 MyDataSource 注解就ok了。也可以在 ServiceImpl 类上加注解。
@Override @MyDataSource(DataSourceEnums.THIRD) @Transactional public void test() { DwUserMPEntity test2 = new DwUserMPEntity(); test2.setUuid("test"); test2.setNickname("test"); test2.setPhone("test"); dwUserDao.insert(test2); // throw new RuntimeException("lala"); } @Override @MyDataSource(DataSourceEnums.PRIMARY) @Transactional public void test1() { SysRoleMPEntity test = new SysRoleMPEntity(); test.setRole("test"); test.setDescription("test"); sysRoleDao.insert(test); // throw new RuntimeException("lala"); }
五.问题总结
1.配置文件里 mybatis-plus的配置不生效:因为我们在 SqlSessionFactory 里重新写了 MybatisConfiguration 。
2.启用事务的话,动态数据源不生效:因为 service 有事务的话,在进入service方法时,DataSourceTransactionManager 就设置好了默认数据源。