在項目中遇到需要使用多數據源的情況,解決辦法是,使用註解,切面攔截來注入不同的dataSource。實現代碼在底部。
基本思路:在dao的方法前加上@TargetDataSource(ORDER_DATA_SOURCE)註解來表明使用的哪個數據源。
問題:事務開啓一般是在service中開啓,事務開啓後會導致數據源切換失敗,數據源切換需要在事務開啓前執行。
解決:
1、@EnableAspectJAutoProxy(exposeProxy = true)
2、數據源切入點@Pointcut增加service切入點,其次service中需要開啓事務的方法寫兩個,
3、方法1加@TargetDataSource(ORDER_DATA_SOURCE),方法2加@Transactional,
4、控制器調用方法1。方法1(使用代理方式調用,而不是直接調用)調用方法2。
總結:控制器->方法1(切換數據源,使用代理方式調用方法2)->方法2(開啓事務,執行多個dao操作)
代理調用示例:
public class TestService{
@TargetDataSource(ORDER_DATA_SOURCE)
public void method1() {
((TestService)AopContext.currentProxy()).method2();
}
@Transactional
public void method2() {
//dao操作
}
}
動態切換數據源示例:
/**
* 目標數據源註解,註解在方法上指定數據源的名稱
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {
String value();//此處接收的是數據源的名稱
}
/**
* 動態數據源持有者,負責利用ThreadLocal存取數據源名稱
*/
public class DynamicDataSourceHolder {
/**
* 本地線程共享對象
*/
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSource(String name) {
THREAD_LOCAL.set(name);
}
public static String getDataSource() {
return THREAD_LOCAL.get();
}
public static void removeDataSource() {
THREAD_LOCAL.remove();
}
}
/**
* 目標數據源註解,註解在方法上指定數據源的名稱
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {
String value();//此處接收的是數據源的名稱
}
@Component
@Data
@ConfigurationProperties(prefix = "spring")
public class DBProperties {
private DataSource datasource;
private DataSource orderDatasource ;
}
@Configuration
@EnableScheduling
@Slf4j
public class DataSourceConfig {
@Autowired
private DBProperties properties;
@Bean(name = "dataSource")
public DataSource dataSource() {
//按照目標數據源名稱和目標數據源對象的映射存放在Map中
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("datasource", properties.getDatasource());
targetDataSources.put("orderDatasource", properties.getOrderDatasource());
//採用是想AbstractRoutingDataSource的對象包裝多數據源
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
//設置默認的數據源,當拿不到數據源時,使用此配置
dataSource.setDefaultTargetDataSource(properties.getDatasource());
return dataSource;
}
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
public static final String ORDER_DATA_SOURCE = "orderDatasource";
}
@Component
@Aspect
@Slf4j
public class DataSourceAspect {
private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class);
//切換放在mapper接口的方法上,所以這裏要配置AOP切面的切入點
//增加api 控制器切入點,是因爲動態數據源切換需要在事務開啓前執行,故需要在service前切換
@Pointcut("execution( * com.lifeccp.besra.repository..*.*(..)) || execution( * com.lifeccp.besra.service..*.*(..))")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String method = joinPoint.getSignature().getName();
Class claz = target.getClass() ;
Class<?>[] clazz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
try {
Method m = null ;
if(clazz.length <= 0){
m = claz.getMethod(method,parameterTypes) ;
}else{
m = clazz[0].getMethod(method, parameterTypes);
}
//如果方法上存在切換數據源的註解,則根據註解內容進行數據源切換
if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource data = m.getAnnotation(TargetDataSource.class);
String dataSourceName = data.value();
DynamicDataSourceHolder.putDataSource(dataSourceName);
LOG.info("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
} else {
LOG.info("switch datasource fail,use default");
}
} catch (Exception e) {
LOG.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
}
}
//執行完切面後,將線程共享中的數據源名稱清空
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint){
DynamicDataSourceHolder.removeDataSource();
}
}