业务背景
一个方法里,对A数据源需要进行查询,B数据源进行插入(切面插入访问数据,日志)。
详细业务是查询业务数据,同时主数据库记录访问日志。
第一步依赖先行
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.16</version></dependency><dependency><groupId>com.dameng</groupId><artifactId>Dm8JdbcDriver18</artifactId><version>8.1.1.49</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.16</version></dependency>
配置类
package com.zhuao.config;import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.google.common.collect.Maps;
import com.zhuao.common.DynamicDataSource;
import com.zhuao.constant.DataSourceTypeConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.Map;/*** @Description: 数据源配置* @author: Be.insighted* @create: 2024/7/15 11:35* @since 1.0.0*/
@Slf4j
@Configuration
public class DataSourceConfig {/*** 主数据源* @return*/@Bean(initMethod = "init", destroyMethod = "close")@ConfigurationProperties("dm.datasource")public DruidDataSource masterDataSource() {log.info("masterDataSource druid data-source init...");return DruidDataSourceBuilder.create().build();}/*** 从数据源* @return*/@Bean(initMethod = "init", destroyMethod = "close")@ConfigurationProperties("mysql.datasource")public DruidDataSource slaveDataSource() {log.info("slaveDataSource druid data-source init...");return DruidDataSourceBuilder.create().build();}/*** 动态数据源* @param masterDataSource masterDataSource* @param slaveDataSource slaveDataSource* @return {@link DynamicDataSource}*/@Bean@Primarypublic DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {Map<String, DataSource> pool = Maps.newHashMapWithExpectedSize(16);pool.put(DataSourceTypeConstant.MASTER, masterDataSource);pool.put(DataSourceTypeConstant.FIRST_FOLLOWER, slaveDataSource);return new DynamicDataSource(pool, masterDataSource);}
}
枚举数据库类型
public interface DataSourceTypeConstant {/*** 主数据源*/String MASTER = "MASTER";/*** 第1从数据源*/String FIRST_FOLLOWER = "FIRST_FOLLOWER";/*** 第2从数据源*/String SECOND_FOLLOWER = "SECOND_FOLLOWER";
}
切换数据源
package com.zhuao.common;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.util.Map;
import java.util.Stack;/*** @Description:* @author: lBe.insighted* @create: 2024/7/15 15:41* @since 1.0.0*/public class DynamicDataSource extends AbstractRoutingDataSource {private static final ThreadLocal<Stack<String>> DATA_SOURCE_KEY = new InheritableThreadLocal<>();public static void setDataSourceKey(String dataSource) {Stack<String> stack = DATA_SOURCE_KEY.get();if (stack == null) {stack = new Stack<>();DATA_SOURCE_KEY.set(stack);}stack.push(dataSource);}public static void cleanDataSourceKey() {Stack<String> stack = DATA_SOURCE_KEY.get();if (stack != null) {stack.pop();if (stack.isEmpty()) {DATA_SOURCE_KEY.remove();}}}/*** 构造** @param targetDataSources*/@SuppressWarnings({"rawtypes", "unchecked"})public DynamicDataSource(Map<String, DataSource> targetDataSources, DataSource defaultDataSource) {super.setTargetDataSources((Map) targetDataSources);super.setDefaultTargetDataSource(defaultDataSource);}/*** determineCurrentLookupKey** @return* @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()*/@Overrideprotected Object determineCurrentLookupKey() {Stack<String> stack = DATA_SOURCE_KEY.get();if (stack != null) {return stack.peek();}return null;}
}
切面处理
注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DynamicDatasourceAnno {String value();
}
切面逻辑
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.zhuao.annotation.DynamicDatasourceAnno;
import com.zhuao.common.DynamicDataSource;
import com.zhuao.constant.DataSourceTypeConstant;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;/*** @Description:* @author: be.insighted* @create: 2024/7/15 15:42* @since 1.0.0*/@Component
@Aspect
@Slf4j
public class TypeSwitchDatasourceAspectJ {@Pointcut("@annotation(com.zhuao.annotation.DynamicDatasourceAnno)")public void pointcut() {}@Before(value="pointcut() && @annotation(dynamicDatasourceAnno)", argNames="dynamicDatasourceAnno")public void before(DynamicDatasourceAnno dynamicDatasourceAnno) {String dataSource = dynamicDatasourceAnno.value();if (StringUtils.isNotBlank(dataSource)) {log.info("从主数据源->切换到->从数据源({})", dataSource);DynamicDataSource.setDataSourceKey(dataSource);}}@After("pointcut()")public void after() {DynamicDataSource.cleanDataSourceKey();log.info("恢复主数据源");DynamicDataSource.setDataSourceKey(DataSourceTypeConstant.MASTER);}}
使用
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zhuao.annotation.DynamicDatasourceAnno;
import com.zhuao.constant.DataSourceTypeConstant;
import com.zhuao.entity.CtiTSheetRecord;
import com.zhuao.req.CtiTeleRecordReq;/*** @Description: 通话记录* @author: Be.insighted* @create: 2024/7/9 10:50* @since 1.0.0*/public interface CtiTSheetRecordService extends IService<CtiTSheetRecord> {@DynamicDatasourceAnno(DataSourceTypeConstant.FIRST_FOLLOWER)IPage<CtiTSheetRecord> query(Page<?> page, CtiTeleRecordReq req);
}
这样还不能完全实现数据源切换,服务无法启动
accountServiceImpl (field private com.******** ***Mapper)↓accountDao↓(inner bean)#12942633↓org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
┌─────┐
| dataSource defined in class path resource [com/******************/DataSourcesConfig.class]
↑ ↓
| databusinessDataSource defined in class path resource [com/*********/DataSourcesConfig.class]
↑ ↓
| org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
└─────┘
druid循环依赖
/*** Specify the map of target DataSources, with the lookup key as key.* The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>The key can be of arbitrary type; this class implements the* generic lookup process only. The concrete key representation will* be handled by {@link #resolveSpecifiedLookupKey(Object)} and* {@link #determineCurrentLookupKey()}.*/public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}/*** Specify the default target DataSource, if any.* <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>This DataSource will be used as target if none of the keyed* {@link #setTargetDataSources targetDataSources} match the* {@link #determineCurrentLookupKey()} current lookup key.*/public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}
解决druid循环依赖问题
1.启动时排除DataSourceAutoConfiguration
2.用import以全路径的形式注入bean
/*** * 解决druid循环依赖问题* 1.启动时排除DataSourceAutoConfiguration* 2.用import以全路径的形式注入bean* */
@EnableAspectJAutoProxy
@Import(DataSourcesConfig.class)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BusinessApplication {public static void main(String[] args) {SpringApplication.run(BusinessApplication.class, args);}}