多數據源系統接入mybatis-plus, 實現動態數據源、動態事務。
- 2020 年 4 月 15 日
- 筆記
- ,動態事務, mybatisPlus, 動態數據源, 重構和設計模式
目錄:
- 實現思想
- 導入依賴、配置說明
- 代碼實現
- 問題總結
一.實現思想
接手一個舊系統,SpringBoot 使用的是純粹的 mybatis ,既沒有使用規範的代碼生成器,也沒有使用 JPA 或者 mybatis-plus。
想着接入 mybatis-plus,為以後敲代碼省點力氣。普通的接入 mybatis-plus 可以直接參考官方文檔 //mp.baomidou.com/ 。
但我接手的系統是個多數據源系統,本來最優的方法是使用官方的 動態數據源 支持 //mp.baomidou.com/guide/dynamic-datasource.html 。
但我因為亂七八糟的依賴衝突,決定自己實現 動態數據源 的支持。
實現的核心邏輯:使用一個 代理數據源,來管理 其他數據源 的分發請求。(通過AOP分發)
二.導入依賴、配置說明
因為依賴的衝突,我沒有直接使用
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1.tmp</version> </dependency>
而是引入的
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.3.1.tmp</version> </dependency>
數據庫配置文件大致如下
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 ThreadLocal<String> contextHolder = 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<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceEnums.PRIMARY.getValue(), primaryDataSource); targetDataSources.put(DataSourceEnums.SECOND.getValue(), secondDataSource); targetDataSources.put(DataSourceEnums.THIRD.getValue(), thirdDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(thirdDataSource); // 默認使用的數據源 return dynamicDataSource; } @Bean("sqlSessionFactory") public SqlSessionFactory sqlSessionFactory() throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(multipleDataSource(primaryDataSource(), secondDataSource(), thirdDataSource()));
//mybatis-plus yml 配置不生效,要在這裡代碼里配置 MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setJdbcTypeForNull(JdbcType.NULL); //是否使用轉駝峰 configuration.setMapUnderscoreToCamelCase(false); configuration.setCacheEnabled(false); sqlSessionFactory.setConfiguration(configuration); //添加分頁功能 Interceptor[] plugins = {paginationInterceptor()}; sqlSessionFactory.setPlugins(plugins); //掃描 mapper 路徑 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resource = resolver.getResources("classpath:mapper/**/*.xml"); sqlSessionFactory.setMapperLocations(resource); return sqlSessionFactory.getObject(); } /** * @Description : mybatis-plus分頁插件 */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 設置請求的頁面大於最大頁後操作, true調回到首頁,false 繼續請求 默認false paginationInterceptor.setOverflow(true); // 設置最大單頁限制數量,默認 500 條,-1 不受限制 // paginationInterceptor.setLimit(30); return paginationInterceptor; } }
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 就設置好了默認數據源。