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;

    @Override
    protected 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
     */
    @Override
    public 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);
        }
    }

		// 配置数据源切面
    @Bean
    public 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
     */
    @Bean
    public 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();
    }

		
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dynamicDataSource) {
        // 指定数据源为配置的动态数据源
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    /**
     * spring事务管理配置
     *
     * @param transactionManager
     * @return
     */
    @Bean
    public 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")
    @Bean
    public 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")
    @Bean
    public 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: master
      list: master,slave
      master:
        url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
        username: your_username
        password: your_password
        driver-class-name: com.mysql.cj.jdbc.Driver
        type-class-name: com.alibaba.druid.pool.DruidDataSource
        pool: 
          max-active: 10
          initial-size: 1
          max-wait: 30000
          min-idle: 1
          time-between-eviction-runs-millis: 30000
          min-evictable-idle-time-millis: 150000
          validation-query: select 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-open-prepared-statements: 20
          filters: stat, wall
          testConnectionOnCheckout: false
          testConnectionOnCheckin: true
          idleConnectionTestPeriod: 3600
      slave:
        url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
        username: your_username
        password: your_password
        driver-class-name: com.mysql.cj.jdbc.Driver
        type-class-name: com.alibaba.druid.pool.DruidDataSource
        pool: 
          max-active: 20
          initial-size: 1
          max-wait: 3000
          min-idle: 1
          time-between-eviction-runs-millis: 6000
          min-evictable-idle-time-millis: 3000
          validation-query: select 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-open-prepared-statements: 20
          filters: stat, wall
          testConnectionOnCheckout: false
          testConnectionOnCheckin: true
          idleConnectionTestPeriod: 360
druid:
  monitor:
    web-stat-filter:
      # 是否开启配置
      enabled: true
      url-pattern: /*
      exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
    stat-view-servlet:
      url-pattern: /druid/*
      reset-enable: false
      # 是否开启配置
      enabled: true
      login-username: admin
      login-password: admin

# mybatis 配置
mybatis:
  scanner:
    mapperLocations: classpath:mapper/*Mapper.xml
    # 实体类别名配置
    typeAliasesPackage: com.ikun.entity
    mapUnderscoreToCamelCase: true
    logImpl: 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配置中,设置的数据源对象
	@Nullable
	private Map<Object, Object> targetDataSources;

	@Nullable
	private Object defaultTargetDataSource;

	private boolean lenientFallback = true;

	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

	// 存储所有配置的数据源对象
	@Nullable
	private Map<Object, DataSource> resolvedDataSources;

	// 默认数据源对象
	@Nullable
	private DataSource resolvedDefaultDataSource;

	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}

	public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
		this.defaultTargetDataSource = defaultTargetDataSource;
	}

	@Override
	public 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的值转DataSource
	protected 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);
		}
	}

  // 获取数据库连接
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	@Override
	public 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 {
    @Autowired
    private PlatformTransactionManager transactionManager;
    @Autowired
    private 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,再调用方法即可

这个方法就不写代码了。。

六、总结

这次学习虽然耗费了一周零零散散的时间,在完成需求的基础上,追究原理,也通过画图加深了理解,不得不感叹这些框架太灵活了,留这么多东西可以让我们自定义扩展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/975914.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

进程概念、PCB及进程查看

文章目录 一.进程的概念进程控制块&#xff08;PCB&#xff09; 二.进程查看通过指令查看进程通过proc目录查看进程的cwd和exe获取进程pid和ppid通过fork()创建子进程 一.进程的概念 进程是一个运行起来的程序&#xff0c;而程序是存放在磁盘的&#xff0c;cpu要想执行程序的指…

字节火山引擎 DeepSeek 接入本地使用

文章目录 1. 火山引擎 DeepSeek 初体验2. 本地接入 火山引擎 DeepSeek API3. 新建 API KEY4. 直接使用 1. 火山引擎 DeepSeek 初体验 火山引擎官网 : https://www.volcengine.com/product/ark 火山云默认给每个模型赠送 50 万 tokens 推理免费额度 进来就会看到模型广场&#…

基于javaweb的SpringBoot个人博客系统设计和实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…

《操作系统 - 清华大学》8 -4:进程管理:进程控制结构

深度剖析进程控制块&#xff1a;操作系统进程管理的核心关键 在操作系统的复杂体系中&#xff0c;进程控制块&#xff08;PCB&#xff09;是实现高效进程管理的关键所在。接下来&#xff0c;将从多个维度深入剖析进程控制块&#xff0c;帮助更好地理解其在操作系统中的重要作用…

Jupyter里面的manim编程学习

1.Jupyterlab的使用 因为我之前一直都是使用的vscode进行manim编程的&#xff0c;但是今天看的这个教程使用的是Jupyter&#xff0c;我也很是好奇这个manim在Jupyter这样的交互式下面会生成怎么样的效果&#xff0c;所以今天尝试了jupyter&#xff0c;并且对于两个进行比较和说…

孜然单授权系统V2.0PHP授权系统

孜然单授权V1.0系统&#xff0c;延续了2022年开发的孜然多应用授权系统V2.0 变更&#xff1a;多应用变单系统&#xff0c;去除没用的垃圾代码&#xff0c;从0开发&#xff0c;去除了一些没用的功能 完善了开发文档&#xff0c;之前那套是我写着玩的屎山代码&#xff0c;V1.0将展…

输入菜单关键字,遍历匹配到 menuIds,展开 匹配节点 的所有父节点以及 匹配节点 本身,高亮 匹配节点

菜单检索&#xff0c;名称、地址、权限标志 等 关键字匹配、展开、高亮(全程借助 DeepSeek ) 便捷简洁的企业官网 的后台菜单管理&#xff0c;图示&#xff1a; 改造点&#xff1a; &#xff08;1&#xff09;修改 bootstrapTreeTable 的节点class命名方式为&#xff1a;treeg…

【落羽的落羽 数据结构篇】顺序结构的二叉树——堆

文章目录 一、堆1. 概念与分类2. 结构与性质3. 入堆4. 出堆 二、堆排序三、堆排序的应用——TOP-K问题 一、堆 1. 概念与分类 上一期我们提到&#xff0c;二叉树的实现既可以用顺序结构&#xff0c;也可以用链式结构。本篇我们来学习顺序结构的二叉树&#xff0c;起个新名字—…

数据结构系列一:初识集合框架+复杂度

前言 数据结构——是相互之间存在一种或多种特定关系的数据元素的集合。数据结构是计算机专业的基础课程&#xff0c;但也是一门不太容易学好的课&#xff0c;它当中有很多费脑子的东西&#xff0c;之后在学习时&#xff0c;你若碰到了困惑或不解的地方 都是很正常的反应&…

Python 入门教程(2)搭建环境 | 2.3、VSCode配置Python开发环境

文章目录 一、VSCode配置Python开发环境1、软件安装2、安装Python插件3、配置Python环境4、包管理5、调试程序 前言 Visual Studio Code&#xff08;简称VSCode&#xff09;以其强大的功能和灵活的扩展性&#xff0c;成为了许多开发者的首选。本文将详细介绍如何在VSCode中配置…

VSCode自定义快捷键和添加自定义快捷键按键到状态栏

VSCode自定义快捷键和添加自定义快捷键按键到状态栏 &#x1f4c4;在VSCode中想实现快捷键方式执行与某些指令操作进行绑定&#xff0c;可以通过配置组合式的键盘按键映射来实现&#xff0c;另外一种方式就是将执行某些特定的指令嵌入在面板菜单上&#xff0c;在想要执行的时候…

Linux系统安装MySQL5.7(其他版本类似)避坑指南

1.远程连接 在Linux系统安装好MySQL5.7数据库&#xff0c;不要以为就大功告成了后面还有大坑等着你踩了。宏哥这里介绍一下远程连接遇到的坑以及如何处理。由于征文要求安装环境教学除外宏哥这里就不介绍在Linux系统安装mysql数据库&#xff0c;有需要的可以自己百度一下。但是…

HybridCLR+Adressable+Springboot热更

本文章会手把手教大家如何搭建HybridCLRAdressableSpringboot热更。 创作不易&#xff0c;动动发财的小手点个赞。 安装华佗 首先我们按照官网的快速上手指南搭建一个简易的项目&#xff1a; 快速上手 | HybridCLR 注意在热更的代码里添加程序集。把用到的工具放到程序集里…

C语言(12)--------->for循环

在C语言中&#xff0c;有三大结构&#xff1a;顺序、选择、循环。这些结构可以用于处理生活中各种各样的复杂问题。选择结构通常是用if语句或者switch语句实现&#xff0c;可参考前面的博客&#xff1a; C语言&#xff08;7&#xff09;------------&#xff1e;if语句CSDN C…

react路由总结

目录 一、脚手架基础语法(16~17) 1.1、hello react 1.2、组件样式隔离(样式模块化) 1.3、react插件 二、React Router v5 2.1、react-router-dom相关API 2.1.1、内置组件 2.1.1.1、BrowserRouter 2.1.1.2、HashRouter 2.1.1.3、Route 2.1.1.4、Redirect 2.1.1.5、L…

JAVA最新版本详细安装教程(附安装包)

目录 文章自述 一、JAVA下载 二、JAVA安装 1.首先在D盘创建【java/jdk-23】文件夹 2.把下载的压缩包移动到【jdk-23】文件夹内&#xff0c;右键点击【解压到当前文件夹】 3.如图解压会有【jdk-23.0.1】文件 4.右键桌面此电脑&#xff0c;点击【属性】 5.下滑滚动条&…

拆解微软CEO纳德拉战略蓝图:AI、量子计算、游戏革命如何改写未来规则!

2025年2月19日 知名博主Dwarkesh Patel对话微软CEO萨蒂亚纳德拉 在最新访谈释放重磅信号&#xff1a;AI将掀起工业革命级增长&#xff0c;量子计算突破引爆材料科学革命&#xff0c;游戏引擎进化为世界模拟器。 整个视频梳理出几大核心观点&#xff0c;揭示科技巨头的未来十年…

记录此刻:历时两月,初步实现基于FPGA的NVMe SSD固态硬盘存储控制器设计!

背景 为满足实验室横向项目需求&#xff0c;在2024年12月中下旬导师提出基于FPGA的NVMe SSD控制器研发项目。项目核心目标为&#xff1a;通过PCIe 3.0 x4接口实现单盘3000MB/s的持续读取速率。 实现过程 调研 花了半个月的时间查阅了一些使用FPGA实现NVME SSD控制器的论文、…

Grok 3与GPT-4.5的“智能天花板”争夺战——谁才是大模型时代的算力之王?

2025年2月18日&#xff0c;马斯克旗下 xAI 高调发布新一代大模型Grok 3&#xff0c;号称“地球上最聪明AI”&#xff0c;在数学推理、代码生成等核心能力上碾压 GPT-4o、DeepSeek-V3 等对手。而就在同一天&#xff0c;OpenAI创始人 Sam Altman 暗示 GPT-4.5 即将登场&#xff0…

Window电脑中 Linux 系统配置VMware固定IP【最新详细】

一、为什么需要固定IP 当前我们虚拟机的Linux操作系统&#xff0c;其IP地址是通过DHCP服务获取的&#xff0c;DHCP&#xff1a;动态获取IP地址,即每次重启设备后都会获取一次&#xff0c;可能导致IP地址频繁变更。 原因1&#xff1a;办公电脑IP地址变化无所谓&#xff0c;但是…