Springboot 整合druid+mybatis+jta分佈式事務+多數據源aop註解動態切換 (一篇到位)

前言:

springboot整合多數據源,大家肯定不陌生,方式不一,但是相信大家整合多數據源,如果涉及到事務,都會非常煩惱,多數據源的事務不是衝突就是失效,而如今網上千篇一律的老年博客,想找到真正解決問題的,非常少。所以我決定出來分享下可用的整合方案,而且是從頭到尾的那種。

這一篇我選擇的是以AOP註解的方式去進行數據源的動態切換,順帶整合jta-atomikos把煩人的事務問題解決調,持久層框架用mybatis,數據庫連接池使用druid,這些在我們周圍目前使用比較多,方便大家根據項目實際需求,能在這個腳手架上進行進一步的擴展(能擴展什麼?也可以看看我的springboot專欄,說不定會有額外的收穫)


這篇篇幅可能較長,但是跟着我全部代碼流程走完,你以後就可以把這個作爲多數據源+分佈式事務的腳手架,以後對於多數據源相關的事務問題,對你來說就不是問題。

 

接下來,我們開始整合。

 

先看下項目目錄結構,大致能瞭解到我們這個實戰整合做了些什麼。

 先準備兩個數據源,

創個user表用於後面使用,

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

pom.xml:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--分佈式事務-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <!-- springboot2.1.x版本默認的mysql-connector-java 版本比較高 8.0.x ,需要降低版本-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>
        <!--Druid連接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <!--aop starter-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--整合mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--調試-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

然後是數據源的yml信息,application.yml:

server:
  port: 8077


spring:
  application:
    name: jta-dbsource
  datasource:
    druid:
      mydbone:
        url: jdbc:mysql://localhost:3306/mydbone?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&pinGlobalTxToPhysicalConnection=true&autoReconnect=true
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 初始化時建立物理連接的個數。初始化發生在顯示調用 init 方法,或者第一次 getConnection 時
        initialSize: 5
        # 最小連接池數量
        minIdle: 5
        # 最大連接池數量
        maxActive: 10
        # 獲取連接時最大等待時間,單位毫秒。配置了 maxWait 之後,缺省啓用公平鎖,併發效率會有所下降,如果需要可以通過配置 useUnfairLock 屬性爲 true 使用非公平鎖。
        maxWait: 60000
        # Destroy 線程會檢測連接的間隔時間,如果連接空閒時間大於等於 minEvictableIdleTimeMillis 則關閉物理連接。
        timeBetweenEvictionRunsMillis: 60000
        # 連接保持空閒而不被驅逐的最小時間
        minEvictableIdleTimeMillis: 300000
        # 用來檢測連接是否有效的 sql 因數據庫方言而異, 例如 oracle 應該寫成 SELECT 1 FROM DUAL
        validationQuery: SELECT 1
        # 建議配置爲 true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閒時間大於 timeBetweenEvictionRunsMillis,執行 validationQuery 檢測連接是否有效。
        testWhileIdle: true
        # 申請連接時執行 validationQuery 檢測連接是否有效,做了這個配置會降低性能。
        testOnBorrow: false
        # 歸還連接時執行 validationQuery 檢測連接是否有效,做了這個配置會降低性能。
        testOnReturn: false
        # 是否自動回收超時連接
        removeAbandoned: true
        # 超時時間 (以秒數爲單位)
        remove-abandoned-timeout: 1800
        logAbandoned: true
        pinGlobalTxToPhysicalConnection: true

      mydbtwo:
        url: jdbc:mysql://localhost:3306/mydbtwo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&pinGlobalTxToPhysicalConnection=true&autoReconnect=true
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        initialSize: 6
        minIdle: 6
        maxActive: 10
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        removeAbandoned: true
        remove-abandoned-timeout: 1800
        logAbandoned: true
        pinGlobalTxToPhysicalConnection: true
      # WebStatFilter 用於採集 web-jdbc 關聯監控的數據。
      web-stat-filter:
        # 是否開啓 WebStatFilter 默認是 true
        enabled: true
        # 需要攔截的 url
        url-pattern: /*
        # 排除靜態資源的請求
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
      # Druid 內置提供了一個 StatViewServlet 用於展示 Druid 的統計信息。
      stat-view-servlet:
        #是否啓用 StatViewServlet 默認值 true
        enabled: true
        # 需要攔截的 url
        url-pattern: /druid/*
        # 允許清空統計數據
        reset-enable: true
        login-username: myname
        login-password: mypwd

 

-----接下來就是代碼環節-----

 

大家多注意看註釋,很多關鍵信息都用註釋方式進行了簡明的介紹

 

先創建一個自定義註解,DataSource.java:

import java.lang.annotation.*;

/**
 * @Author : JCccc
 * @CreateTime : 2019/8/28
 * @Description :
 **/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default DataSourceNames.ONE;
}

然後是創建 DataSourceNames.java,用於簡單數據源命名:

/**
 * @Author : JCccc
 * @CreateTime : 2019/8/28
 * @Description :
 **/

public interface DataSourceNames {
    String ONE = "ONE";
    String TWO = "TWO";
}

ps:其實這些都是我之前aop切換數據源的時候敲的,大概8月份的時候,這次我相當於在這個基礎上着重解決事務問題

 然後是將自定義註解作爲切點,進行aop方式動態切換邏輯補全,創建DynamicDataSourceAspect.java:

import com.test.jtadbsource.dbConfig.DataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/10
 * @Description :
 **/
@Aspect
@Component
public class DynamicDataSourceAspect {

    protected Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 切點: 所有配置 DataSource 註解的方法
     */
    @Pointcut("@annotation(com.test.jtadbsource.dbAop.DataSource)")
    public void dataSourcePointCut() {}
    
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource ds = null;
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //獲取自定義註解
        ds = method.getAnnotation(DataSource.class);
        if (ds == null) {
            //如果監測到自定義註解不存在,那麼默認切換到數據源 mydbone
            DataSourceContextHolder.setDataSourceKey(DataSourceNames.ONE);
            logger.info("set default datasource is " + DataSourceNames.ONE);
        } else {
            //自定義存在,則按照註解的值去切換數據源
            DataSourceContextHolder.setDataSourceKey(ds.value());
            logger.info("set datasource is " + ds.value());
        }
        return point.proceed();
    }


    @After(value = "dataSourcePointCut()")
    public void afterSwitchDS(JoinPoint point) {
        DataSourceContextHolder.clearDataSourceKey();
        logger.info("clean datasource");
    }
    

}

上面用到的DataSourceContextHolder.java:

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/10
 * @Description :
 **/
public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    // 設置數據源名
    public static void setDataSourceKey(String dbName) {
        contextHolder.set(dbName);
    }

    // 獲取數據源名
    public static String getDataSourceKey() {
        return (contextHolder.get());
    }

    // 清除數據源名
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
}

ok,到這裏,基本的動態切換邊框的東西都完畢了,接下來是比較核心的:

1. DataSourceFactory.java  :
用於 不同的數據源DataSource的信息配置,使用DruidXADataSource創建,支持jta事務;

將不同數據源DataSource分別都關聯上對應的AtomikosDataSourceBean,這樣事務能提取到JTA事務管理器;

重寫數據源會話工廠,爲每個數據源單獨配置一個。

配置重寫的sqlSessionTemplate,將實際使用的不同數據源的sqlsession和spring的事務機制關聯起來。

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.test.jtadbsource.dbAop.DataSourceNames;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/10
 * @Description :多數據源配置
 **/

@Configuration
@MapperScan(basePackages = DataSourceFactory.BASE_PACKAGES, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DataSourceFactory {

    static final String BASE_PACKAGES = "com.test.jtadbsource.mapper";

    private static final String MAPPER_LOCATION = "classpath:mybatis/mapper/*.xml";

    /***
     * 創建 DruidXADataSource mydbone 用@ConfigurationProperties 自動配置屬性
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.mydbone")
    public DataSource druidDataSourceOne() {
        return new DruidXADataSource();
    }

    /***
     * 創建 DruidXADataSource mydbtwo
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.mydbtwo")
    public DataSource druidDataSourceTwo() {

        return new DruidXADataSource();
    }

    /**
     * 創建支持 XA 事務的 Atomikos 數據源 mydbone
     */
    @Bean
    public DataSource dataSourceOne(DataSource druidDataSourceOne) {
        AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
        sourceBean.setXaDataSource((DruidXADataSource) druidDataSourceOne);
        // 必須爲數據源指定唯一標識
        sourceBean.setPoolSize(5);
        sourceBean.setTestQuery("SELECT 1");
        sourceBean.setUniqueResourceName("mydbone");
        return sourceBean;
    }

    /**
     * 創建支持 XA 事務的 Atomikos 數據源 mydbtwo
     */
    @Bean
    public DataSource dataSourceTwo(DataSource druidDataSourceTwo) {
        AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
        sourceBean.setXaDataSource((DruidXADataSource) druidDataSourceTwo);
        sourceBean.setPoolSize(5);
        sourceBean.setTestQuery("SELECT 1");
        sourceBean.setUniqueResourceName("mydbtwo");
        return sourceBean;
    }


    /**
     * @param dataSourceOne 數據源 mydbone
     * @return 數據源 mydbone 的會話工廠
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryOne(DataSource dataSourceOne)
            throws Exception {
        return createSqlSessionFactory(dataSourceOne);
    }


    /**
     * @param dataSourceTwo 數據源 mydbtwo
     * @return 數據源 mydbtwo 的會話工廠
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryTwo(DataSource dataSourceTwo)
            throws Exception {
        return createSqlSessionFactory(dataSourceTwo);
    }


    /***
     * sqlSessionTemplate 與 Spring 事務管理一起使用,以確保使用的實際 SqlSession 是與當前 Spring 事務關聯的,
     * 此外它還管理會話生命週期,包括根據 Spring 事務配置根據需要關閉,提交或回滾會話
     * @param sqlSessionFactoryOne 數據源 mydbone
     * @param sqlSessionFactoryTwo 數據源 mydbtwo
     */
    @Bean
    public CustomSqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactoryOne, SqlSessionFactory sqlSessionFactoryTwo) {
        Map<Object, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put(DataSourceNames.ONE, sqlSessionFactoryOne);
        sqlSessionFactoryMap.put(DataSourceNames.TWO, sqlSessionFactoryTwo);
        CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(sqlSessionFactoryOne);
        customSqlSessionTemplate.setTargetSqlSessionFactories(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }

    /***
     * 自定義會話工廠
     * @param dataSource 數據源
     * @return :自定義的會話工廠
     */
    private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //配置駝峯命名
        configuration.setMapUnderscoreToCamelCase(true);
        //配置sql日誌
        configuration.setLogImpl(StdOutImpl.class);
        factoryBean.setConfiguration(configuration);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        //配置讀取mapper.xml路徑
        factoryBean.setMapperLocations(resolver.getResources(MAPPER_LOCATION));
        return factoryBean.getObject();
    }
    
}

 上面用到的自定義CustomSqlSessionTemplate (重寫SqlSessionTemplate):

import static java.lang.reflect.Proxy.newProxyInstance;
import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable;
import static org.mybatis.spring.SqlSessionUtils.closeSqlSession;
import static org.mybatis.spring.SqlSessionUtils.getSqlSession;
import static org.mybatis.spring.SqlSessionUtils.isSqlSessionTransactional;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.MyBatisExceptionTranslator;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.util.Assert;


public class CustomSqlSessionTemplate extends SqlSessionTemplate {
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;
    private final PersistenceExceptionTranslator exceptionTranslator;
    private Map<Object, SqlSessionFactory> targetSqlSessionFactories;
    private SqlSessionFactory defaultTargetSqlSessionFactory;

    /**
     * 通過Map傳入
     * @param targetSqlSessionFactories
     */
    public void setTargetSqlSessionFactories(Map<Object, SqlSessionFactory> targetSqlSessionFactories) {
        this.targetSqlSessionFactories = targetSqlSessionFactories;
    }
    public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory) {
        this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory;
    }
    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }
    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
        this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration()
                .getEnvironment().getDataSource(), true));
    }
    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                                    PersistenceExceptionTranslator exceptionTranslator) {
        super(sqlSessionFactory, executorType, exceptionTranslator);
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
                SqlSessionFactory.class.getClassLoader(),
                new Class[] { SqlSession.class },
                new SqlSessionInterceptor());
        this.defaultTargetSqlSessionFactory = sqlSessionFactory;
    }
    //通過DataSourceContextHolder獲取當前的會話工廠
    @Override
    public SqlSessionFactory getSqlSessionFactory() {
        String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactories.get(dataSourceKey);
        if (targetSqlSessionFactory != null) {
            return targetSqlSessionFactory;
        } else if (defaultTargetSqlSessionFactory != null) {
            return defaultTargetSqlSessionFactory;
        } else {
            Assert.notNull(targetSqlSessionFactories, "Property 'targetSqlSessionFactories' or 'defaultTargetSqlSessionFactory' are required");
            Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactories' are required");
        }
        return this.sqlSessionFactory;
    }


    @Override
    public Configuration getConfiguration() {
        return this.getSqlSessionFactory().getConfiguration();
    }
    public ExecutorType getExecutorType() {
        return this.executorType;
    }
    public PersistenceExceptionTranslator getPersistenceExceptionTranslator() {
        return this.exceptionTranslator;
    }
    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement) {
        return this.sqlSessionProxy.<T> selectOne(statement);
    }
    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.<T> selectOne(statement, parameter);
    }
    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
        return this.sqlSessionProxy.<K, V> selectMap(statement, mapKey);
    }
    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey);
    }
    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey, rowBounds);
    }
    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement) {
        return this.sqlSessionProxy.<E> selectList(statement);
    }
    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter) {
        return this.sqlSessionProxy.<E> selectList(statement, parameter);
    }
    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        return this.sqlSessionProxy.<E> selectList(statement, parameter, rowBounds);
    }
    /**
     * {@inheritDoc}
     */
    public void select(String statement, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, handler);
    }
    /**
     * {@inheritDoc}
     */
    public void select(String statement, Object parameter, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, parameter, handler);
    }
    /**
     * {@inheritDoc}
     */
    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        this.sqlSessionProxy.select(statement, parameter, rowBounds, handler);
    }
    /**
     * {@inheritDoc}
     */
    public int insert(String statement) {
        return this.sqlSessionProxy.insert(statement);
    }
    /**
     * {@inheritDoc}
     */
    public int insert(String statement, Object parameter) {
        return this.sqlSessionProxy.insert(statement, parameter);
    }
    /**
     * {@inheritDoc}
     */
    public int update(String statement) {
        return this.sqlSessionProxy.update(statement);
    }
    /**
     * {@inheritDoc}
     */
    public int update(String statement, Object parameter) {
        return this.sqlSessionProxy.update(statement, parameter);
    }
    /**
     * {@inheritDoc}
     */
    public int delete(String statement) {
        return this.sqlSessionProxy.delete(statement);
    }
    /**
     * {@inheritDoc}
     */
    public int delete(String statement, Object parameter) {
        return this.sqlSessionProxy.delete(statement, parameter);
    }
    /**
     * {@inheritDoc}
     */
    public <T> T getMapper(Class<T> type) {
        return getConfiguration().getMapper(type, this);
    }
    /**
     * {@inheritDoc}
     */
    public void commit() {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }
    /**
     * {@inheritDoc}
     */
    public void commit(boolean force) {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }
    /**
     * {@inheritDoc}
     */
    public void rollback() {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }
    /**
     * {@inheritDoc}
     */
    public void rollback(boolean force) {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }
    /**
     * {@inheritDoc}
     */
    public void close() {
        throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession");
    }
    /**
     * {@inheritDoc}
     */
    public void clearCache() {
        this.sqlSessionProxy.clearCache();
    }
    /**
     * {@inheritDoc}
     */
    public Connection getConnection() {
        return this.sqlSessionProxy.getConnection();
    }
    /**
     * {@inheritDoc}
     * @since 1.0.2
     */
    public List<BatchResult> flushStatements() {
        return this.sqlSessionProxy.flushStatements();
    }
    /**
     * Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
     * unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to
     * the {@code PersistenceExceptionTranslator}.
     */
    private class SqlSessionInterceptor implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final SqlSession sqlSession = getSqlSession(
                    CustomSqlSessionTemplate.this.getSqlSessionFactory(),
                    CustomSqlSessionTemplate.this.executorType,
                    CustomSqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, CustomSqlSessionTemplate.this.getSqlSessionFactory())) {
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                Throwable unwrapped = unwrapThrowable(t);
                if (CustomSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    Throwable translated = CustomSqlSessionTemplate.this.exceptionTranslator
                            .translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            } finally {
                closeSqlSession(sqlSession, CustomSqlSessionTemplate.this.getSqlSessionFactory());
            }
        }
    }
}

然後是xat分佈式事務管理器,XATransactionManagerConfig.java:

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/10
 * @Description :JTA 事務配置
 **/

@Configuration
@EnableTransactionManagement
public class XATransactionManagerConfig {

    @Bean
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean
    public TransactionManager atomikosTransactionManager() {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(true);
        return userTransactionManager;
    }

    @Bean
    public PlatformTransactionManager transactionManager(UserTransaction userTransaction,
                                                         TransactionManager transactionManager) {
        return new JtaTransactionManager(userTransaction, transactionManager);
    }
}

然後,在啓動類上,去除掉自動加載的數據源配置類,

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class JtadbsourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(JtadbsourceApplication.class, args);
    }

}

 

到這裏,aop註解方式整合多數據源+分佈式事務jta已經完畢了!

 

接下來就是使用測試環節,包括單數據源數據插入&事務回滾,多數據源切換插入&事務回滾:

首先創建實體類,User.java:

import lombok.Data;
import lombok.ToString;

/**
 * @Author : JCccc
 * @CreateTime : 2019/10/22
 * @Description :
 **/

@Data
@ToString
public class User {
    private Integer id;
    private String username;
    private Integer age;


}

然後是UserMapper.java:

import com.test.jtadbsource.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/10
 * @Description :
 **/
@Mapper
@Repository
public interface UserMapper {
     
     int insert(User user);
}

ps:這裏爲什麼要加@Repository,其實不加也行。原因可以看我這篇:(https://blog.csdn.net/qq_35387940/article/details/103402092

然後是創建一個TestJtaservice.java:

import com.test.jtadbsource.pojo.User;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/9
 * @Description :
 **/
public interface TestJtaService {
    
    void testInsertUser(User user);
    void testInsertUser2(User user);
}

然後是TestJtaServiceImpl.java , 這裏將會通過我們開始創建的自定義註解來標識,哪些service使用哪些數據源:

import com.test.jtadbsource.dbAop.DataSource;
import com.test.jtadbsource.dbAop.DataSourceNames;
import com.test.jtadbsource.mapper.UserMapper;
import com.test.jtadbsource.pojo.User;
import com.test.jtadbsource.service.TestJtaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author : JCccc
 * @CreateTime : 2019/12/9
 * @Description :
 **/
@Service
public class TestJtaServiceImpl implements TestJtaService {

    @Autowired
    UserMapper userMapper;


    public void testInsertUser(User user){
        int insertNum = userMapper.insert(user);
        System.out.println("插入成功,條數:"+insertNum);
    }

    @DataSource(DataSourceNames.TWO)
    public void testInsertUser2(User user){
        int insertNum = userMapper.insert(user);
        System.out.println("插入成功,條數:"+insertNum);
    }

}

最後,我們寫個接口,先來測試下數據源方面,操作不同數據是否正常:

調用下該接口,

數據正常插入:

那麼我們直接測試下單數據源的事務回滾, 

不使用手動回滾,這樣測試下其實也行:


 調用下接口,事務回滾正常:

 

接下來是兩個數據源數據同時插入:

調用下接口,數據正常插入:

 

然後是不同數據源事務一起回滾:

調用下接口:

 

 

 

該篇就到此結束,
若對你有幫助,給我留個言即可;
若有不對之處,歡迎指出交流。

發佈了150 篇原創文章 · 獲贊 209 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章