SpringBoot+Mybatis-Plus實現動態數據源

目錄

    • 一、前言
    • 二、代碼實現
      • 1)工程結構
      • 2)相關依賴
      • 3)數據源攔截切面
      • 4)動態數據源切換
      • 5)核心配置類
      • 6)使用
    • 三、原理分析
      • 1)mapper接口注入流程
      • 2)動態數據源切換執行流程
    • 四、聲明式事務導致切換失效
      • 1)場景復現
      • 2)原因
      • 3)解決方法
    • 五、自調用導致數據源失效
      • 1)場景復現
      • 2)原因
      • 3)解決方法
    • 六、總結

代碼倉庫:

  • https://gitee.com/zhszstudy/dynamic-datasource
  • https://github.com/zhszstudy/dynamic-datasource

一、前言

這段時間剛好有需求,需要在當前的一個模塊中直連其他系統的數據庫,但是當前系統并不支持多數據源,只支持單數據源。這也可以通過新建一個模塊來編寫該需求,但是總感覺不是特別方便,萬一后續又要連接其他數據庫,又要新建一個個模塊。或者可以引入mybatis-plus的多數據源支持依賴,雖說簡單,但總有不妥的地方。因此,在這個需求下,實現了比較輕量的數據源切換組件。

二、代碼實現

1)工程結構

在這里插入圖片描述

2)相關依賴

springboot版本:2.2.7.RELEASE

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.21</version></dependency>
</dependencies>

3)數據源攔截切面

主要用于攔截標注了**@DataSourceType**注解的Bean,并將注解中的值存入數據源上下文中

/*** @author zhou22* @desc 數據源選擇切面* @Date 2025-02-19 10:25:15*/
@Aspect
public class DynamicDataSourceAspect {@Pointcut("@annotation(com.zhou.annotation.DataSourceType)")public void pointCut() {}@Around("pointCut() && @annotation(dataSourceType)")public Object selectDataSource(ProceedingJoinPoint joinPoint, DataSourceType dataSourceType) throws Throwable {if (!StringUtils.isBlank(dataSourceType.dataSourceName())) {// 將要切換的數據源名稱存入上下文中DataSourceContextHolder.setDatasource(dataSourceType.dataSourceName());}try {return joinPoint.proceed();} finally {DataSourceContextHolder.clearDatasource();}}}

4)動態數據源切換

①自定義數據源

繼承了AbstractRoutingDataSource ,主要在獲取數據庫連接時,會根據這里的值,去選擇要切換的數據源

/*** @author zhou22* @desc 動態數據源獲取* @Date 2025-02-19 10:33:19*/
public class DynamicDataSource extends AbstractRoutingDataSource {@Value("${dynamic.jdbc.datasource.default}")private String defaultDataSource;@Overrideprotected Object determineCurrentLookupKey() {// 從數據源上下文獲取要切換的數據源String datasource = DataSourceContextHolder.getDatasource();// 如果沒有配置注解,則選擇默認數據源return datasource != null ? datasource : defaultDataSource;}
}

②數據源上下文

用于存儲線程執行此次CRUD操作要切換的數據源名稱

/*** @author zhou22* @desc 數據源上下文* @Date 2025-02-19 10:14:10*/
public class DataSourceContextHolder {private static final ThreadLocal<String> dataSourceName = new ThreadLocal<>();public static String getDatasource() {return dataSourceName.get();}public static void setDatasource(String datasource) {dataSourceName.set(datasource);}public static void clearDatasource() {dataSourceName.remove();}}

5)核心配置類

項目啟動時會根據spring.factories文件的配置信息,來加載這個配置類,主要用于裝配實現動態數據源的相關bean,以及讀取配置文件中的配置

/*** @author zhou22* @desc 動態數據源切換配置* @Date 2025-02-19 10:37:08*/
@Configuration
public class DynamicDataSourceAutoConfig implements EnvironmentAware {// 數據源分組private final Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();// 默認數據源名稱private String defaultDataSourceName;private Environment environment;/*** 讀取配置文件,**見下面①分析**** @param environment*/@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;// 獲取默認數據源名稱this.defaultDataSourceName = PropertyUtil.convertToTarget(environment, DynamicDataSourceConstants.PREFIX + DynamicDataSourceConstants.DEFAULT_DATA_SOURCE, String.class);// 獲取數據源名稱列表String dataSources = PropertyUtil.convertToTarget(environment, DynamicDataSourceConstants.PREFIX + DynamicDataSourceConstants.DATA_SOURCE_LIST, String.class);for (String dataSource : dataSources.split(COMMA)) {// 挨個獲取數據源配置Map<String, Object> dataSourceProperties = PropertyUtil.convertToTarget(environment, DynamicDataSourceConstants.PREFIX + dataSource, Map.class);dataSourceMap.put(dataSource, dataSourceProperties);}}// 配置數據源切面@Beanpublic DynamicDataSourceAspect dynamicDataSourceAspect() {return new DynamicDataSourceAspect();}// 配置自定義數據源:動態數據源核心實現@Bean("dynamicDataSource")public DataSource dataSource() {DynamicDataSource dynamicDataSource = new DynamicDataSource();Map<Object, Object> targetDataSources = new HashMap<>();// 將讀取的數據源配置信息,依次轉為真實的數據源對象for (Map.Entry<String, Map<String, Object>> entry : dataSourceMap.entrySet()) {DataSource dataSource = createDataSource(entry.getValue());targetDataSources.put(entry.getKey(), dataSource);}// 設置配置的所有數據源,后續會根據這個map來實現數據源切換dynamicDataSource.setTargetDataSources(targetDataSources);// 設置默認數據源dynamicDataSource.setDefaultTargetDataSource(targetDataSources.get(defaultDataSourceName));return dynamicDataSource;}/*** 創建數據源** @param dataSourcePropertyMap* @return*/private DataSource createDataSource(Map<String, Object> dataSourcePropertyMap) {DataSourceProperties dataSourceProperties = new DataSourceProperties();dataSourceProperties.setUrl(dataSourcePropertyMap.get(DynamicDataSourceConstants.URL).toString());dataSourceProperties.setUsername(dataSourcePropertyMap.get(DynamicDataSourceConstants.USERNAME).toString());dataSourceProperties.setPassword(dataSourcePropertyMap.get(DynamicDataSourceConstants.PASSWORD).toString());dataSourceProperties.setDriverClassName(dataSourcePropertyMap.get(DynamicDataSourceConstants.DRIVER_CLASS_NAME).toString());String typeClassName = dataSourcePropertyMap.get(DynamicDataSourceConstants.TYPE_CLASS_NAME).toString();try {// 創建數據源DataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type((Class<DataSource>) Class.forName(typeClassName)).build();// 獲取連接池配置,支持多種連接池配置的關鍵實現Map<String, Object> poolProperties = (Map<String, Object>) (dataSourcePropertyMap.containsKey(DynamicDataSourceConstants.POOL_KEY) ? dataSourcePropertyMap.get(DynamicDataSourceConstants.POOL_KEY) : Collections.emptyMap());// 反射設置連接池配置信息MetaObject metaObject = SystemMetaObject.forObject(dataSource);for (Map.Entry<String, Object> poolProperty : poolProperties.entrySet()) {String key = MapKeyConvertUtils.middleLineToCamelHump(poolProperty.getKey());if (metaObject.hasSetter(key)) {metaObject.setValue(key, poolProperty.getValue());}}return dataSource;} catch (ClassNotFoundException e) {throw new IllegalStateException("數據源連接池配置失效,無法找到類:" + typeClassName);}}/*** SqlSession工廠配置,**見下面②分析**** @param dynamicDataSource* @return* @throws Exception*/@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {// 將配置映射到配置類MybatisScannerProperties mybatisScannerProperties = PropertyUtil.convertToTarget(environment, MybatisScannerConstants.PREFIX, MybatisScannerProperties.class);// 配置mybatis-plus掃描MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();// 指定數據源為配置的動態數據源factory.setDataSource(dynamicDataSource);factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mybatisScannerProperties.getMapperLocations()));factory.setTypeAliasesPackage(mybatisScannerProperties.getTypeAliasesPackage());MybatisConfiguration configuration = new MybatisConfiguration();// 是否開啟數據庫字段下劃線命名到Java屬性駝峰命名的自動映射configuration.setMapUnderscoreToCamelCase(mybatisScannerProperties.getMapUnderscoreToCamelCase());// 日志輸出類配置configuration.setLogImpl((Class<? extends Log>) Class.forName(mybatisScannerProperties.getLogImpl()));factory.setConfiguration(configuration);return factory.getObject();}@Beanpublic DataSourceTransactionManager transactionManager(DataSource dynamicDataSource) {// 指定數據源為配置的動態數據源return new DataSourceTransactionManager(dynamicDataSource);}/*** spring事務管理配置** @param transactionManager* @return*/@Beanpublic TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {TransactionTemplate transactionTemplate = new TransactionTemplate();transactionTemplate.setTransactionManager(transactionManager);transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");return  transactionTemplate;}/*** druid監控頁面配置-帳號密碼配置,見下面③分析** @return servlet registration bean*/@ConditionalOnProperty(prefix = DruidMonitorConstants.STAT_PREFIX, name = "enabled", havingValue = "true")@Beanpublic ServletRegistrationBean druidStatViewServlet() {// 將配置映射到配置類DruidMonitorProperties.StatViewServlet statViewServlet = PropertyUtil.convertToTarget(environment, DruidMonitorConstants.STAT_PREFIX, DruidMonitorProperties.StatViewServlet.class);// druid監控帳號密碼配置ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), statViewServlet.getUrlPattern());servletRegistrationBean.addInitParameter(DruidMonitorConstants.LOGIN_USERNAME, statViewServlet.getLoginUsername());servletRegistrationBean.addInitParameter(DruidMonitorConstants.LOGIN_PASSWORD, statViewServlet.getLoginPassword());servletRegistrationBean.addInitParameter(DruidMonitorConstants.RESET_ENABLE, String.valueOf(statViewServlet.isResetEnable()));return servletRegistrationBean;}/*** druid監控頁面配置-允許頁面正常瀏覽,見下面③分析** @return filter registration bean*/@ConditionalOnProperty(prefix = DruidMonitorConstants.WEB_PREFIX, name = "enabled", havingValue = "true")@Beanpublic FilterRegistrationBean druidWebStataFilter() {DruidMonitorProperties.WebStatFilter webStatFilter = PropertyUtil.convertToTarget(environment, DruidMonitorConstants.WEB_PREFIX, DruidMonitorProperties.WebStatFilter.class);FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());// 添加過濾規則.filterRegistrationBean.addUrlPatterns(webStatFilter.getUrlPattern());// 排除不需要統計的URL請求filterRegistrationBean.addInitParameter(DruidMonitorConstants.EXCLUSIONS, webStatFilter.getExclusions());return filterRegistrationBean;}}

解釋:

①setEnvironment方法

因為配置類實現了EnvironmentAware 接口,所以在配置類實例化之后,初始化之前,會執行重寫了該接口的setEnvironment方法,此時就可以拿到Environment對象信息,它里面包含了配置文件的配置信息,通過SpringBoot的Binder類,可以很輕松將配置信息映射到具體的實體類,使用的工具類如下:

/*** @author zhou22* @desc 屬性操作工具類* @Date 2025-02-19 10:38:14*/
public class PropertyUtil {/*** 如果沒有配置的信息,則拋出異常** @param environment* @param name* @param clz* @param <T>* @return*/public static <T> T convertToTarget(Environment environment, String name, Class<T> clz) {try {return Binder.get(environment).bind(name, clz).get();} catch (NoSuchElementException e) {throw new RuntimeException(e.getMessage(), e);}}/*** 如果沒有配置信息,返回一個空的對象** @param environment* @param name* @param clz* @param <T>* @return*/public static <T> T convertToTargetIfAbsent(Environment environment, String name, Class<T> clz) {return Binder.get(environment).bindOrCreate(name, clz);}
}

②為什么要配置這個bean

為了取代項目中原有的數據源配置,我直接把需要引入動態數據源的模塊中,原有的數據源配置刪掉了,也就是spring.datasource.jdbc 前綴的配置,然后啟動項目就會報錯,找不到url的信息。為了解決這個問題,在啟動類加上了如下的配置,排除框架原先的數據源自動裝配:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})

加上這些配置后,啟動項目,又報了mapper文件找不到的錯誤,因為我把自動裝配類給排除了,自然有一些bean沒有裝配到,為了解決這些問題,需要讓mybatis-plus掃描到這些mapper文件

③為什么要配置這兩個bean

因為原先模塊是支持druid監控配置的,因為我把DruidDataSourceAutoConfigure這個自動裝配類排除掉了,所以無法根據原先的druid監控配置來加載bean,為了實現druid監控,因此創建了這兩個bean,根據配置文件的信息來決定是否加載

6)使用

配置示例:

dynamic:jdbc:datasource:default: masterlist: master,slavemaster:url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTCusername: your_usernamepassword: your_passworddriver-class-name: com.mysql.cj.jdbc.Drivertype-class-name: com.alibaba.druid.pool.DruidDataSourcepool: max-active: 10initial-size: 1max-wait: 30000min-idle: 1time-between-eviction-runs-millis: 30000min-evictable-idle-time-millis: 150000validation-query: select 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: falsepool-prepared-statements: truemax-open-prepared-statements: 20filters: stat, walltestConnectionOnCheckout: falsetestConnectionOnCheckin: trueidleConnectionTestPeriod: 3600slave:url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTCusername: your_usernamepassword: your_passworddriver-class-name: com.mysql.cj.jdbc.Drivertype-class-name: com.alibaba.druid.pool.DruidDataSourcepool: max-active: 20initial-size: 1max-wait: 3000min-idle: 1time-between-eviction-runs-millis: 6000min-evictable-idle-time-millis: 3000validation-query: select 'x'test-while-idle: truetest-on-borrow: falsetest-on-return: falsepool-prepared-statements: truemax-open-prepared-statements: 20filters: stat, walltestConnectionOnCheckout: falsetestConnectionOnCheckin: trueidleConnectionTestPeriod: 360
druid:monitor:web-stat-filter:# 是否開啟配置enabled: trueurl-pattern: /*exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"stat-view-servlet:url-pattern: /druid/*reset-enable: false# 是否開啟配置enabled: truelogin-username: adminlogin-password: admin# mybatis 配置
mybatis:scanner:mapperLocations: classpath:mapper/*Mapper.xml# 實體類別名配置typeAliasesPackage: com.ikun.entitymapUnderscoreToCamelCase: truelogImpl: org.apache.ibatis.logging.stdout.StdOutImpl

方法或類中加入自定義的數據源注解,值為配置文件中數據源名稱:

比如下面的代碼,會切換至slave這個數據源來執行CRUD,如果沒有配置這個注解,默認用的是master數據源

@DataSourceType(dataSourceName = "slave")
@Override
public List<Student> queryStudentFromSlave() {return baseMapper.selectList(null);
}

訪問:http://localhost:8348/druid/sql.html,可以看到監控頁面:
在這里插入圖片描述

三、原理分析

以下流程圖的流程,可以自己打斷點看看

1)mapper接口注入流程

忽略引入mybatis-plus(只做增強,不做修改,加多了一層),以原有mybatis的邏輯來分析:

在這里插入圖片描述

2)動態數據源切換執行流程

忽略引入mybatis-plus(只做增強,不做修改,加多了一層),以原有mybatis的邏輯來分析:
在這里插入圖片描述
動態數據源切換關鍵邏輯,主要通過集成抽象父類AbstractRoutingDataSource ,重寫determineCurrentLookupKey方法實現,代碼如下:


public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {// 在DynamicDataSourceAutoConfig配置中,設置的數據源對象@Nullableprivate Map<Object, Object> targetDataSources;@Nullableprivate Object defaultTargetDataSource;private boolean lenientFallback = true;private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();// 存儲所有配置的數據源對象@Nullableprivate Map<Object, DataSource> resolvedDataSources;// 默認數據源對象@Nullableprivate DataSource resolvedDefaultDataSource;public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}@Overridepublic void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");}this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());// 將targetDataSources轉成數據源對象,存進resolvedDataSources中this.targetDataSources.forEach((key, value) -> {Object lookupKey = resolveSpecifiedLookupKey(key);DataSource dataSource = resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource\);}}protected Object resolveSpecifiedLookupKey(Object lookupKey) {return lookupKey;}// 將targetDataSources的值轉DataSourceprotected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {if (dataSource instanceof DataSource) {return (DataSource) dataSource;}else if (dataSource instanceof String) {return this.dataSourceLookup.getDataSource((String) dataSource);}else {throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);}}// 獲取數據庫連接@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return determineTargetDataSource().getConnection(username, password);}// 關鍵方法protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");// 獲取數據源名稱Object lookupKey = determineCurrentLookupKey();// 根據數據源名稱去resolvedDataSources中查找數據源DataSource dataSource = this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");}return dataSource;}@Nullable// 子類DynamicDataSource實現了該方法,返回當前線程要切換的數據源名稱protected abstract Object determineCurrentLookupKey();}

四、聲明式事務導致切換失效

1)場景復現

如下的代碼標注了事務注解,也就是開啟了聲明式事務,在測試時發現數據源切換失效,一直返回了默認數據源的數據

@DataSourceType(dataSourceName = "slave")
@Transactional(rollbackFor = Exception.class)
@Override
public List<Student> queryStudentFromSlave() {return baseMapper.selectList(null);
}

2)原因

這里面就不畫流程圖,簡單說一下,當方法上標注@Transactional注解之后,會為當前類生成一個代理對象,具體事務處理邏輯由TransactionInterceptor 攔截器來實現,當調用上述的queryStudentFromSlave方法時,在這個方法執行之前,會先由TransactionInterceptor 開啟事務,然后才執行queryStudentFromSlave方法。

這似乎沒什么問題,但是調試斷點時,發現TransactionInterceptor 的邏輯先于DynamicDataSourceAspect 實現,并且TransactionInterceptor 在開啟事務時,會提前去獲取一個數據庫連接對象,也就是如下方法:

在這里插入圖片描述

其中獲取數據源的方法如下:

org.springframework.jdbc.datasource.DataSourceTransactionManager

protected DataSource obtainDataSource() {DataSource dataSource = getDataSource();Assert.state(dataSource != null, "No DataSource set");return dataSource;
}
// 這個dataSource就是DynamicDataSource,通過spring注入
public DataSource getDataSource() {return this.dataSource;
}

此時,還沒走切面邏輯,所以返回的是默認數據源對象,然后會將數據源對象保存到一個線程上下文中:
在這里插入圖片描述

接著會走到切面的處理邏輯,設置要切換的數據源名稱,當切面執行完之后,接著走我們的CRUD方法,也就是前面分析的流程圖,最終會通過DataSourceUtils來獲取數據庫連接對象:

在這里插入圖片描述

問題就出現在這里,由于開啟聲明式事務時,提前創建了一個數據庫連接對象存入上下文中,導致動態數據源失效,因為即使后續經過了切面處理,設置了要切換的數據源名稱,在DataSourceUtils 獲取數據庫連接對象時,優先從上下文中獲取!

3)解決方法

既然事務攔截器(TransactionInterceptor)執行比動態數據源切面(DynamicDataSourceAspect )先執行,那我控制動態數據源切面先于事務攔截器執行不就好了嗎,于是在自動配置類加了Order注解來讓動態數據源切面優先執行:

@Bean
// 值越小,優先執行,Ordered.HIGHEST_PRECEDENCE的值為Integer.MIN_VALUE
@Order(Ordered.HIGHEST_PRECEDENCE)
public DynamicDataSourceAspect dynamicDataSourceAspect() {return new DynamicDataSourceAspect();
}

想法很美好,現實很骨感,重新加載依賴,啟動項目,發現還是沒有效果

**查閱相關資料:**https://www.jb51.net/article/139418.htm

發現TransactionInterceptorDynamicDataSourceAspect 是由不同的代理方式生成的:

  • DynamicDataSourceAspect 這種通過@Aspect注解標注的類是通過AnnotationAwareAspectJAutoProxyCreator進行代理的
  • TransactionInterceptor 是BeanNameAutoProxyCreator方式進行代理的

BeanNameAutoProxyCreator攔截優先級高于AnnotationAwareAspectJAutoProxyCreator,order注解只對同一類型的AOP攔截方式起作用

既然這種方式不行的話,那只能采用編程式事務來解決這個問題了,見下面的解決方法:

引入多數據源的模塊,不使用聲明式事務,改用編程式事務,示例如下:

@Service
public class UserService {@Autowiredprivate PlatformTransactionManager transactionManager;@Autowiredprivate UserRepository userRepository;public void createUser(User user) {// 定義事務屬性(如傳播行為、隔離級別等)TransactionDefinition definition = new DefaultTransactionDefinition();TransactionStatus status = transactionManager.getTransaction(definition);try {userRepository.save(user);// 其他數據庫操作transactionManager.commit(status); // 提交事務} catch (Exception e) {transactionManager.rollback(status); // 回滾事務throw e;}}
}

為了減少重復的事務處理代碼,可以在動態數據源切面中,加入上述的編程式事務處理。

五、自調用導致數據源失效

1)場景復現

@DataSourceType(dataSourceName = "slave")
//    @Transactional(rollbackFor = Exception.class)
@Override
public List<Student> queryStudentFromSlave() {return baseMapper.selectList(null);
}@Override
public List<Student> queryStudentWithSelf() {return queryStudentFromSlave();
}

當調用queryStudentWithSelf()方法時,會導致數據源切換失效

2)原因

出現這個問題的原因在于**@DataSourceType(dataSourceName = “slave”)是基于動態代理實現切面效果的,在本類方法調用注解方法時,這個this的引用為普通對象,所以沒有走切面的處理流程,只獲取了默認的數據源,這個失效原理和@Transactional**注解自調用失效一樣(除此之外還要注意注解標注的方法,修飾符不能帶有private、final)

3)解決方法

①獲取代理對象

通過獲取代理對象的方式來解決,方法如下:

1.啟動類開啟代理暴露:

@EnableAspectJAutoProxy(exposeProxy = true)

2.獲取代理對象執行方法:

@DataSourceType(dataSourceName = "slave")
//    @Transactional(rollbackFor = Exception.class)
@Override
public List<Student> queryStudentFromSlave() {return baseMapper.selectList(null);
}@Override
public List<Student> queryStudentWithSelf() {StudentService studentService = (StudentService) AopContext.currentProxy();return studentService.queryStudentFromSlave();
}

②將被自調用的方法抽到其他Service類中,然后在本類注入該bean,再調用方法即可

這個方法就不寫代碼了。。

六、總結

這次學習雖然耗費了一周零零散散的時間,在完成需求的基礎上,追究原理,也通過畫圖加深了理解,不得不感嘆這些框架太靈活了,留這么多東西可以讓我們自定義擴展。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/70499.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/70499.shtml
英文地址,請注明出處:http://en.pswp.cn/web/70499.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

玩轉 Java 與 Python 交互,JEP 庫來助力

文章目錄 玩轉 Java 與 Python 交互&#xff0c;JEP 庫來助力一、背景介紹二、JEP 庫是什么&#xff1f;三、如何安裝 JEP 庫&#xff1f;四、JEP 庫的簡單使用方法五、JEP 庫的實際應用場景場景 1&#xff1a;數據處理場景 2&#xff1a;機器學習場景 3&#xff1a;科學計算場…

Qt常用控件之日歷QCalendarWidget

日歷QCalendarWidget QCalendarWidget 是一個日歷控件。 QCalendarWidget屬性 屬性說明selectDate當前選中日期。minimumDate最小日期。maximumDate最大日期。firstDayOfWeek設置每周的第一天是周幾&#xff08;影響日歷的第一列是周幾&#xff09;。gridVisible是否顯示日歷…

三數之和:經典問題的多種優化策略

三數之和&#xff1a;經典問題的多種優化策略 大家好&#xff0c;我是Echo_Wish。今天我們來聊一個經典的算法問題——三數之和&#xff08;3Sum&#xff09;。它是許多面試和算法競賽中常見的問題之一&#xff0c;也常常考察我們對算法優化的理解和技巧。我們不僅要解決問題&…

Go 語言中的協程

概念 Go語言中的協程&#xff08;Goroutine&#xff09;是一種由Go運行時管理的輕量級線程。它是Go語言并發模型的核心&#xff0c;旨在通過簡單、易用的方式支持高并發的程序設計。 創建協程 協程的創建非常簡單&#xff0c;只需要使用go關鍵字&#xff0c;后面跟著一個函數…

JAVA最新版本詳細安裝教程(附安裝包)

目錄 文章自述 一、JAVA下載 二、JAVA安裝 1.首先在D盤創建【java/jdk-23】文件夾 2.把下載的壓縮包移動到【jdk-23】文件夾內&#xff0c;右鍵點擊【解壓到當前文件夾】 3.如圖解壓會有【jdk-23.0.1】文件 4.右鍵桌面此電腦&#xff0c;點擊【屬性】 5.下滑滾動條&…

基于javaweb的SpringBoot個人博客系統設計和實現(源碼+文檔+部署講解)

技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬蟲、數據可視化、小程序、安卓app、大數據、物聯網、機器學習等設計與開發。 主要內容&#xff1a;免費功能設計、開題報告、任務書、中期檢查PPT、系統功能實現、代碼編寫、論文編寫和輔導、論…

三、linux字符驅動詳解

在上一節完成NFS開發環境的搭建后&#xff0c;本節將探討Linux字符設備驅動的開發。字符設備驅動作為Linux內核的重要組成部分&#xff0c;主要負責管理與字符設備&#xff08;如串口、鍵盤等&#xff09;的交互&#xff0c;并為用戶空間程序提供統一的讀寫操作接口。 驅動代碼…

Python爬蟲處理網頁中的動態內容

文章目錄 前言一、Python環境搭建1.Python安裝2.選擇Python開發環境 二、Python爬蟲處理網頁中的動態內容1. 使用 Selenium 庫2. 使用 Pyppeteer 庫3. 分析 API 請求 前言 在網頁中&#xff0c;動態內容通常是指那些通過 JavaScript 在頁面加載后動態生成或更新的內容&#xf…

重學SpringBoot3-Spring Retry實踐

更多SpringBoot3內容請關注我的專欄&#xff1a;《SpringBoot3》 期待您的點贊??收藏評論 重學SpringBoot3-Spring Retry實踐 1. 簡介2. 環境準備3. 使用方式 3.1 注解方式 基礎使用自定義重試策略失敗恢復機制重試和失敗恢復效果注意事項 3.2 編程式使用3.3 監聽重試過程 監…

vue3中解決組件間 css 層級問題最佳實踐(Teleport的使用)

定義&#xff1a; <Teleport> 是 Vue 3 中引入的一個內置組件&#xff0c;用于將組件的內容渲染到 DOM 中的指定位置&#xff0c;而不受組件層級結構的限制。這在處理模態框、通知、下拉菜單等需要脫離當前組件層級的情況下非常有用。 通俗來說&#xff0c;Teleport的功…

密度提升30%!Intel 18A工藝正式開放代工

快科技2月23日消息&#xff0c;Intel官方網站悄然更新了對于18A(1.8nm級)工藝節點的描述&#xff0c;稱已經做好了迎接客戶項目的準備&#xff0c;將在今年上半年開始流片&#xff0c;有需求的客戶可以隨時聯系。 Intel宣稱&#xff0c;這是在北美地區率先量產的2nm以下工藝節…

docker中常用的命令

一、服務命令 systemctl start docker.service 啟動docker服務 systemctl stop docker.service 關閉docker服務 systemctl enable docker.service 設置docker服務開機啟動 systemctl disable docker.service .禁止docker服務開機自啟動 二、鏡像命令 d…

架構師論文《智慧醫療系統中的數據集成與共享》

智慧醫療系統中的數據集成與共享 摘要 隨著醫療信息化的發展&#xff0c;如何實現跨系統、跨機構的數據集成與共享成為智慧醫療建設的核心問題。2019年&#xff0c;我所在的醫療科技公司承接了某省衛生健康委員會主導的“區域醫療信息化平臺”項目。該平臺旨在整合區域內三甲醫…

請求go構建緩存,go clean -cache

go clean -cache go 構建時會產生很多緩存&#xff0c; 一般是目錄&#xff1a;/Users/xxx/Library/Caches/go-build 此目錄README&#xff1a; This directory holds cached build artifacts from the Go build system. Run "go clean -cache" if the directory …

mybatis從接口直接跳到xml的插件

在使用 MyBatis(包括 MyBatis-Plus)時,如果你希望從接口方法直接跳轉到對應的 XML 映射文件中的 SQL 語句定義,可以借助一些開發工具或插件來實現這一功能。以下是幾種常見的方法和插件推薦: 方法一:使用 IDE 內置功能 IntelliJ IDEA IntelliJ IDEA 提供了對 MyBatis …

計算機視覺行業洞察--影像行業系列第一期

計算機視覺行業產業鏈的上下游構成相對清晰&#xff0c;從基礎技術研發到具體應用場景的多個環節相對成熟。 以下是我結合VisionChina經歷和行業龍頭企業對計算機視覺行業產業鏈上下游的拆解總結。 上下游總結 上游產業鏈分為軟硬件兩類&#xff0c;視覺的硬件主要指芯片、…

Spring事務原理 二

在上一篇博文《Spring事務原理 一》中&#xff0c;我們熟悉了Spring聲明式事務的AOP原理&#xff0c;以及事務執行的大體流程。 本文中&#xff0c;介紹了Spring事務的核心組件、傳播行為的源碼實現。下一篇中&#xff0c;我們將結合案例&#xff0c;來講解實戰中有關事務的易…

邏輯函數的神經網絡實現

1.單層感知器實現基本邏輯函數 先給大家拋出一道例題 &#xff08;一&#xff09;種類 a.OR函數 目標&#xff1a;當至少一個輸入為1時&#xff0c;輸出1&#xff1b;否則輸出0。 權重設置&#xff1a; 輸入權重&#xff1a;所有 wi1&#xff08;i1,2,...,m&#xff09;。…

SF-HCI-SAP問題收集1

最近在做HCI的集成&#xff0c;是S4的環境&#xff0c;發現很多東西都跑不通&#xff0c;今天開始收集一下錯誤點 如果下圖沖從0001變成0010&#xff0c;sfiom_rprq_osi表就會存數據&#xff0c;系統檢查到此表就會報錯&#xff0c;這個選項的作用就是自定義信息類型也能更新&a…

(面試經典問題之分布式鎖)分布式鎖的基本原理、作用以及實現

一、什么是分布式鎖 分布式鎖指的是在分布式場景中實現互斥類型的鎖。 分布式是什么意思&#xff1f;分布式表示運行的節點可能在不同的機器或不同的網段中&#xff0c;節點間通信通過socket。互斥類型是什么意思&#xff1f;互斥類型表示同一時刻只允許一個執行體進入臨界資…