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.mzph.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;互斥类型表示同一时刻只允许一个执行体进入临界资…