目录
- 一、前言
- 二、代码实现
- 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
发现TransactionInterceptor
和DynamicDataSourceAspect
是由不同的代理方式生成的:
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,再调用方法即可
这个方法就不写代码了。。
六、总结
这次学习虽然耗费了一周零零散散的时间,在完成需求的基础上,追究原理,也通过画图加深了理解,不得不感叹这些框架太灵活了,留这么多东西可以让我们自定义扩展。