Java代碼生成器更新:添加多數(shù)據(jù)源模式支持

引言

哈嘍,小伙伴們,一周不見了,在這段時間,我利用下班的閑暇時間更新了一版代碼生成器,添加了之前呼聲較高的多數(shù)據(jù)源模式,這樣生成的代碼可以實現(xiàn)動態(tài)切換數(shù)據(jù)源的功能,多數(shù)據(jù)源在項目當(dāng)中還算比較常用的,例如主從讀寫分離,多庫操作等都需要在同一個項目中操作多個數(shù)據(jù)庫,本次更新正是解決了這個痛點,生成代碼之后,可以通過注解的方式靈活切換數(shù)據(jù)源,并且支持多庫事務(wù)一致性,下面就讓我們一起看一下具體的實現(xiàn)效果,順便講一下動態(tài)多數(shù)據(jù)源的內(nèi)部原理!

生成器界面調(diào)整

為了實現(xiàn)多數(shù)據(jù)源模式,代碼生成器對界面進(jìn)行了調(diào)整,如下:


界面調(diào)整

主界面添加了選擇數(shù)據(jù)源的功能,并且現(xiàn)在數(shù)據(jù)庫信息需要點擊數(shù)據(jù)源配置來進(jìn)行配置,點擊后會彈出如下窗口:


數(shù)據(jù)源配置

在這里我們可以配置數(shù)據(jù)庫信息,配置完畢后點擊保存,在主界面即可進(jìn)行選擇,在主界面被選擇的數(shù)據(jù)源將在生成的代碼中作為默認(rèn)數(shù)據(jù)源使用。

勾選多數(shù)據(jù)源模式可以生成多數(shù)據(jù)源模式代碼,不勾選則與之前一樣,生成的是常規(guī)單數(shù)據(jù)源項目。

總體跟原來區(qū)別不大,使用多數(shù)據(jù)源模式生成代碼基本步驟如下:

  1. 配置數(shù)據(jù)源保存
  2. 主界面依次選擇數(shù)據(jù)源,配置數(shù)據(jù)項信息
  3. 勾選多數(shù)據(jù)源模式,點擊生成代碼即可

生成代碼展示

代碼展示

雙數(shù)據(jù)源

多數(shù)據(jù)源模式下會在 config 包下生成多數(shù)據(jù)源相關(guān)的配置類及切面,如果大家有個性化需求可以通過修改 DynamicDataSourceAspect 切面來實現(xiàn)動態(tài)切換邏輯,現(xiàn)有切換邏輯基本足夠。

多數(shù)據(jù)源其實還可以通過代碼分包的方式實現(xiàn),這種方式實現(xiàn)起來易于理解:配置多個數(shù)據(jù)源,掃描不同的包,創(chuàng)建屬于自己的 sqlSessionFactory 和 txManager(事務(wù)管理器),在使用的時候可以通過調(diào)用不同包下的 mapper 來實現(xiàn)多數(shù)據(jù)源的效果,但是這種方式的弊端也較為明顯,分包稍有不慎便會出錯,并且如果想要實現(xiàn)不同數(shù)據(jù)源下的事務(wù)一致性也較為麻煩,在同一個 service 方法中操作多個數(shù)據(jù)庫因此受限。

動態(tài)多數(shù)據(jù)源則不會有以上問題,因此代碼生成器選擇了動態(tài)多數(shù)據(jù)源的生成模式,利用 aop 實現(xiàn)數(shù)據(jù)源的動態(tài)切換,并且可以保證多庫操作事務(wù)一致性,后面會詳細(xì)講解。

代碼運行效果

在 idea 中運行生成的代碼,啟動完畢登錄,點擊左側(cè)菜單查詢:


查看

查看后臺日志,發(fā)現(xiàn)會切換不同的數(shù)據(jù)庫執(zhí)行sql:


切換

下面以 springboot 為例,講一下多數(shù)據(jù)源內(nèi)部原理。

動態(tài)多數(shù)據(jù)源內(nèi)部原理及核心代碼

動態(tài)多數(shù)據(jù)源的內(nèi)部原理其實就是 aop,只不過復(fù)雜的是 aop 的實現(xiàn)過程。

mybatis 為我們提供了一個抽象類 AbstractRoutingDataSource,通過繼承此類,重寫 determineCurrentLookupKey 方法可以根據(jù)返回值決定當(dāng)前使用哪個數(shù)據(jù)源,因此我們創(chuàng)建類 DynamicDataSource 繼承 AbstractRoutingDataSource 并重寫 determineCurrentLookupKey 方法:

/**
 * 重寫數(shù)據(jù)源選擇方法(獲取當(dāng)前線程設(shè)置的數(shù)據(jù)源)
 * @author zrx
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    
    }
}

先不忙著實現(xiàn),如果想要正確匹配數(shù)據(jù)源,我們還需要向 DynamicDataSource 類中注冊數(shù)據(jù)源,所以需要先對數(shù)據(jù)源進(jìn)行配置,這里注冊兩個數(shù)據(jù)源 db1(mysql) 和 db2(oracle),我們使用枚舉值 DB1 和 DB2 作為數(shù)據(jù)源 db1 和 db2 的 key:

package mutitest.config.mutidatasource;

/**
 * 數(shù)據(jù)源枚舉
 * @author zrx
 */
public enum DataSourceType {

    /**
    * DB1
    */
    DB1,
    /**
    * DB2
    */
    DB2,

}
package mutitest.config.mutidatasource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 數(shù)據(jù)源配置類
 *
 * @author zrx
 */
@Configuration
public class DynamicDataSourceConfig {

    @Bean(name = "db1")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean(name = "db2")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DataSource dynamicDataSource(@Qualifier(value = "db1") DataSource db1,@Qualifier(value = "db2") DataSource db2) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //設(shè)置默認(rèn)數(shù)據(jù)源
        dynamicDataSource.setDefaultTargetDataSource(db1);
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.DB1, db1);
        dataSourceMap.put(DataSourceType.DB2, db2);
        //向動態(tài)數(shù)據(jù)源中注冊所有數(shù)據(jù)源信息
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager txManager(DataSource dataSource) {
        //返回動態(tài)數(shù)據(jù)源的事務(wù)管理器
        return new DataSourceTransactionManager(dataSource);
    }

}

通過以上配置,我們成功向 DynamicDataSource 中注冊了 db1 和 db2,如何才能獲取當(dāng)前程序運行中的數(shù)據(jù)源呢?這就需要我們用到 ThreadLocal,ThreadLocal 可以向當(dāng)前線程中 set 和 get 值并且不受其他線程影響,而我們服務(wù)器的每一個請求都由一個工作線程來處理(nio 模式也是一個請求一個工作線程處理,只是在接收請求的時候使用了 io 多路復(fù)用),所以可以使用 ThreadLocal 存儲當(dāng)前工作線程的數(shù)據(jù)源,ThreadLocal 在很多開源框架中都有使用,主要用于線程隔離。

創(chuàng)建 DynamicDataSourceHolder 類,存儲當(dāng)前線程中的數(shù)據(jù)源:

package mutitest.config.mutidatasource;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * 數(shù)據(jù)源選擇器
 *
 * @author zrx
 */
public class DynamicDataSourceHolder {
    private static final ThreadLocal<DataSourceType> DATA_SOURCE_HOLDER = new ThreadLocal<>();

    private static final Set<DataSourceType> DATA_SOURCE_TYPES = new HashSet<>();

    static {
        //添加全部枚舉
        DATA_SOURCE_TYPES.addAll(Arrays.asList(DataSourceType.values()));
    }

    public static void setType(DataSourceType dataSourceType) {
        if (dataSourceType == null) {
            throw new NullPointerException();
        }
        DATA_SOURCE_HOLDER.set(dataSourceType);
    }

    public static DataSourceType getType() {
        return DATA_SOURCE_HOLDER.get();
    }

    static void clearType() {
        DATA_SOURCE_HOLDER.remove();
    }

    static boolean containsType(DataSourceType dataSourceType) {
        return DATA_SOURCE_TYPES.contains(dataSourceType);
    }
}

然后,實現(xiàn) determineCurrentLookupKey 方法,一行代碼即可:

@Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getType();
    }

最后一步,我們要實現(xiàn)數(shù)據(jù)源的動態(tài)切換,則需要自己實現(xiàn)一個數(shù)據(jù)源動態(tài)切面,改變當(dāng)前線程中的數(shù)據(jù)源,我們可以使用注解來輔助實現(xiàn),在切面中通過掃描方法上的注解來得知具體切換到哪個數(shù)據(jù)源。

創(chuàng)建 DBType 注解:

package mutitest.config.mutidatasource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 多數(shù)據(jù)源注解
* @author zrx
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DBType {
DataSourceType value() default DataSourceType.DB1;
}

創(chuàng)建數(shù)據(jù)源動態(tài)切面 DynamicDataSourceAspect:

package mutitest.config.mutidatasource;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 動態(tài)數(shù)據(jù)源切面(order 必須要設(shè)置,否則事務(wù)的切面會優(yōu)先執(zhí)行,數(shù)據(jù)源已經(jīng)設(shè)置完了,再設(shè)置就無效了)
 * @author zrx
 */
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    @Before("@annotation(dbType)")
    public void changeDataSourceType(JoinPoint joinPoint, DBType dbType) {
        DataSourceType curType = dbType.value();
        //判斷注解類型
        if (!DynamicDataSourceHolder.containsType(curType)) {
            logger.info("指定數(shù)據(jù)源[{}]不存在,使用默認(rèn)數(shù)據(jù)源-> {}", dbType.value(), joinPoint.getSignature());
        } else {
            logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
            // 切換當(dāng)前線程的數(shù)據(jù)源
            DynamicDataSourceHolder.setType(dbType.value());
        }

    }

    @After("@annotation(dbType)")
    public void restoreDataSource(JoinPoint joinPoint, DBType dbType) {
        logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
        //方法執(zhí)行完,清空,防止內(nèi)存泄漏
        DynamicDataSourceHolder.clearType();
    }

}

在數(shù)據(jù)源切面上需要添加 @Order 注解,值取1,這是因為之前我們配置了動態(tài)數(shù)據(jù)源事務(wù),spring 會因此生成事務(wù)代理并且會優(yōu)先于切面執(zhí)行,事務(wù)代理一旦生成,數(shù)據(jù)源便被固定,這樣我們在切面中切換數(shù)據(jù)源就會無效,所以切面邏輯需要在事務(wù)代理之前執(zhí)行才可生效。

至此,動態(tài)多數(shù)據(jù)源基本實現(xiàn)完畢!

事務(wù)一致性問題

使用動態(tài)多數(shù)據(jù)源的同時,也要注意保證事務(wù)一致性,大家可能遇到這種情況,傳統(tǒng)單數(shù)據(jù)源應(yīng)用中,同一個 service ,在沒有開啟事務(wù)的方法里調(diào)用開啟事務(wù)的方法會導(dǎo)致事務(wù)失效,這是因為 spring 只會對相同的 service 代理一次,否則如果在沒有開啟事務(wù)的方法中再次開啟自身代理會導(dǎo)致循環(huán)依賴問題出現(xiàn),類似 “無限套娃”:自己代理的方法調(diào)用自己代理的另一個方法,并且另一個方法還需要自己的代理。解決此類問題的方法很簡單,讓調(diào)用方開啟事務(wù)即可,多數(shù)據(jù)源模式中同樣適用。

除此之外,多數(shù)據(jù)源模式中還存在如下場景:serviceA 中的 A 和 B 方法都開啟了事務(wù),但操作的是不同的數(shù)據(jù)庫(ip不同),這個時候 A 調(diào)用 B,使用的是 A 的代理,對 B 不適用,便會報錯,對此我們可以把 B 方法移入另一個 serviceB 中,在 serviceA 中注入 serviceB ,在 A 方法中使用 serviceB 調(diào)用 B 方法,這樣執(zhí)行到 B 方法的時候使用的便是 serviceB 的代理,看起來沒有問題,但還有一點遺漏,那就是事務(wù)的傳播行為。

我們都知道,Spring 中默認(rèn)的事務(wù)傳播行為是 required:如果需要開啟事務(wù),則開啟事務(wù),如果已經(jīng)開啟事務(wù),則加入當(dāng)前事務(wù)。上文中,執(zhí)行 B 方法的時候雖然使用的是 serviceB 的代理,但是由于其事務(wù)傳播行為是 required,A 方法執(zhí)行的時候已經(jīng)開啟了事務(wù),所以導(dǎo)致 B 方法加入到了 A 方法的事務(wù)中,但 A 和 B 屬于兩個不同的數(shù)據(jù)庫,使用相同的事務(wù)管理器必然會出現(xiàn)問題。為了解決此問題,我們可以把事務(wù)傳播行為改為 required_new:如果需要開啟事務(wù),則開啟事務(wù),并且總是開啟新的事務(wù)。這樣執(zhí)行 B 方法的時候會開啟新的事務(wù),使用的便是 B 所在數(shù)據(jù)庫的事務(wù)管理器,B 方法也就可以正常執(zhí)行了,并且如果 B 出現(xiàn)異常,如果 A 不主動捕獲,則 A,B 都會回滾。

也許有人會問,單數(shù)據(jù)源模式下使用 required 為什么不會有上述問題呢,因為單數(shù)據(jù)源模式下使用的是同一個數(shù)據(jù)庫,在事務(wù)執(zhí)行過程中,當(dāng)前事務(wù)是共享且通用的,所以沒問題。除此之外,使用 required 不必頻繁重開事務(wù),也一定程度上提升了系統(tǒng)性能,多數(shù)據(jù)源模式下由于不同數(shù)據(jù)庫之間事務(wù)是完全隔離的,所以才需要使用 required_new 重開事務(wù),當(dāng)然,也需要根據(jù)業(yè)務(wù)具體場景具體分析,這里討論的只是較為通用的情況。

代碼生成器多數(shù)據(jù)源模式下使用的事務(wù)傳播行為正是 required_new,全局配置類如下:

package mutitest.config;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;

/**
 * 全局事務(wù)支持
 *
 * @author zrx
 *
 */
@Aspect
@Configuration
public class TransactionAdviceConfig {

    private static final String AOP_POINTCUT_EXPRESSION = "execution(* mutitest.service.impl.*.*(..))";

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public TransactionInterceptor txAdvice() {

        DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
        txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

        DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
        txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        txAttr_REQUIRED_READONLY.setReadOnly(true);

        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        //可以根據(jù)業(yè)務(wù)需要自行添加需要被事務(wù)代理的方法
        source.addTransactionalMethod("add*", txAttr_REQUIRED);
        source.addTransactionalMethod("delete*", txAttr_REQUIRED);
        source.addTransactionalMethod("update*", txAttr_REQUIRED);
        source.addTransactionalMethod("select*", txAttr_REQUIRED_READONLY);
        source.addTransactionalMethod("likeSelect*", txAttr_REQUIRED_READONLY);
        return new TransactionInterceptor(transactionManager, source);
    }

    @Bean
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

到此為止,我們才算實現(xiàn)了一個完整的動態(tài)多數(shù)據(jù)源功能,可見是有許多技術(shù)細(xì)節(jié)潛藏在里面的,朋友們可以使用代碼生成器生成多數(shù)據(jù)源模式下的代碼自行運行體會。

結(jié)語

本文到這里就結(jié)束了,寫這個多數(shù)據(jù)源生成功能其實也算花了一番心思,正著寫代碼容易,反過來生成是真不容易,并且由于最開始做的時候沒有考慮到多數(shù)據(jù)源的情況,導(dǎo)致最開始的設(shè)計全都是針對單個數(shù)據(jù)庫的,這次強(qiáng)行在外面包了一層,總歸是實現(xiàn)了,在這個過程中,順便也復(fù)習(xí)了一下 Spring 的循環(huán)依賴,Bean 加載周期等老生常談的問題,也算有所收獲。作為開發(fā)人員,我們要多關(guān)注一些功能底層的東西,而不是簡單的 api 調(diào)用,這樣才能不斷突破瓶頸,取得成長。碼字不易,各位看官可以點贊,在看,星標(biāo)關(guān)注哦,我們下次再見!

代碼生成器鏈接

關(guān)注公眾號 螺旋編程極客 獲取代碼生成器最新動態(tài),同時第一時間解鎖更多精彩內(nèi)容!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容