1. 程式人生 > >Spring Boot 多資料來源動態切換開啟事務後,資料來源切換失敗

Spring Boot 多資料來源動態切換開啟事務後,資料來源切換失敗

在專案中遇到需要使用多資料來源的情況,解決辦法是,使用註解,切面攔截來注入不同的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();
    }
}