基於註解的springboot+mybatis的多數據源組件的實現代碼

通常業務開發中,我們會使用到多個數據源,比如,部分數據存在mysql實例中,部分數據是在oracle數據庫中,那這時候,項目基於springboot和mybatis,其實隻需要配置兩個數據源即可,隻需要按照

dataSource -SqlSessionFactory – SqlSessionTemplate配置好就可以瞭。

如下代碼,首先我們配置一個主數據源,通過@Primary註解標識為一個默認數據源,通過配置文件中的spring.datasource作為數據源配置,生成SqlSessionFactoryBean,最終,配置一個SqlSessionTemplate。

@Configuration
@MapperScan(basePackages = "com.xxx.mysql.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {

    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druid() {
        return new DruidDataSource();
    }

    @Bean(name = "primarySqlSessionFactory")
    @Primary
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        return bean.getObject();
    }

    @Bean("primarySqlSessionTemplate")
    @Primary
    public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }
}

然後,按照相同的流程配置一個基於oracle的數據源,通過註解配置basePackages掃描對應的包,實現特定的包下的mapper接口,使用特定的數據源。

@Configuration
@MapperScan(basePackages = "com.nbclass.oracle.mapper", sqlSessionFactoryRef = "oracleSqlSessionFactory")
public class OracleDataSourceConfig {

    @Bean(name = "oracleDataSource")
    @ConfigurationProperties(prefix = "spring.secondary")
    public DataSource oracleDruid(){
        return new DruidDataSource();
    }

    @Bean(name = "oracleSqlSessionFactory")
    public SqlSessionFactory oracleSqlSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:oracle/mapper/*.xml"));
        return bean.getObject();
    }

    @Bean("oracleSqlSessionTemplate")
    public SqlSessionTemplate oracleSqlSessionTemplate(@Qualifier("oracleSqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }
}

這樣,就實現瞭一個工程下使用多個數據源的功能,對於這種實現方式,其實也足夠簡單瞭,但是如果我們的數據庫實例有很多,並且每個實例都主從配置,那這裡維護起來難免會導致包名過多,不夠靈活。

現在考慮實現一種對業務侵入足夠小,並且能夠在mapper方法粒度上去支持指定數據源的方案,那自然而然想到瞭可以通過註解來實現,首先,自定義一個註解@DBKey:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DBKey {

    String DEFAULT = "default"; // 默認數據庫節點

    String value() default DEFAULT;
}

思路和上面基於springboot原生的配置的類似,首先定義一個默認的數據庫節點,當mapper接口方法/類沒有指定任何註解的時候,默認走這個節點,註解支持傳入value參數表示選擇的數據源節點名稱。至於註解的實現邏輯,可以通過反射來獲取mapper接口方法/類的註解值,然後指定特定的數據源。

那在什麼時候執行這個操作獲取呢?可以考慮使用spring AOP織入mapper層,在切入點執行具體mapper方法之前,將對應的數據源配置放入threaLocal中,有瞭這個邏輯,立即動手實現:

首先,定義一個db配置的上下文對象。維護所有的數據源key實例,以及當前線程使用的數據源key:

public class DBContextHolder {

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

    //在app啟動時就加載全部數據源,不需要考慮並發
    private static Set<String> allDBKeys = new HashSet<>();

    public static String getDBKey() {
        return DB_KEY_CONTEXT.get();
    }

    public static void setDBKey(String dbKey) {
        //key必須在配置中
        if (containKey(dbKey)) {
            DB_KEY_CONTEXT.set(dbKey);
        } else {
            throw new KeyNotFoundException("datasource[" + dbKey + "] not found!");
        }
    }

    public static void addDBKey(String dbKey) {
        allDBKeys.add(dbKey);
    }

    public static boolean containKey(String dbKey) {
        return allDBKeys.contains(dbKey);
    }

    public static void clear() {
        DB_KEY_CONTEXT.remove();
    }
}

然後,定義切點,在切點before方法中,根據當前mapper接口的@@DBKey註解來選取對應的數據源key:

@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DSAdvice implements BeforeAdvice {

    @Pointcut("execution(* com.xxx..*.repository.*.*(..))")
    public void daoMethod() {
    }

    @Before("daoMethod()")
    public void beforeDao(JoinPoint point) {
        try {
            innerBefore(point, false);
        } catch (Exception e) {
            logger.error("DefaultDSAdviceException",
                    "Failed to set database key,please resolve it as soon as possible!", e);
        }
    }

    /**
     * @param isClass 攔截類還是接口
     */
    public void innerBefore(JoinPoint point, boolean isClass) {
        String methodName = point.getSignature().getName();

        Class<?> clazz = getClass(point, isClass);
        //使用默認數據源
        String dbKey = DBKey.DEFAULT;
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        Method method = null;
        try {
            method = clazz.getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
        }
        //方法上存在註解,使用方法定義的datasource
        if (method.isAnnotationPresent(DBKey.class)) {
            DBKey key = method.getAnnotation(DBKey.class);
            dbKey = key.value();
        } else {
            //方法上不存在註解,使用類上定義的註解
            clazz = method.getDeclaringClass();
            if (clazz.isAnnotationPresent(DBKey.class)) {
                DBKey key = clazz.getAnnotation(DBKey.class);
                dbKey = key.value();
            }
        }
        DBContextHolder.setDBKey(dbKey);
    }


    private Class<?> getClass(JoinPoint point, boolean isClass) {
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();

        Class<?> clazz = target.getClass();
        if (!isClass) {
            Class<?>[] clazzList = target.getClass().getInterfaces();

            if (clazzList == null || clazzList.length == 0) {
                throw new MutiDBException("找不到mapper class,methodName =" + methodName);
            }
            clazz = clazzList[0];
        }

        return clazz;
    }
}

既然在執行mapper之前,該mapper接口最終使用的數據源已經被放入threadLocal中,那麼,隻需要重寫新的路由數據源接口邏輯即可:

public class RoutingDatasource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        String dbKey = DBContextHolder.getDBKey();
        return dbKey;
    }

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        for (Object key : targetDataSources.keySet()) {
            DBContextHolder.addDBKey(String.valueOf(key));
        }
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
}

另外,我們在服務啟動,配置mybatis的時候,將所有的db配置加載:

@Bean
    @ConditionalOnMissingBean(DataSource.class)
    @Autowired
    public DataSource dataSource(MybatisProperties mybatisProperties) {
        Map<Object, Object> dsMap = new HashMap<>(mybatisProperties.getNodes().size());
        for (String nodeName : mybatisProperties.getNodes().keySet()) {
            dsMap.put(nodeName, buildDataSource(nodeName, mybatisProperties));
            DBContextHolder.addDBKey(nodeName);
        }
        RoutingDatasource dataSource = new RoutingDatasource();
        dataSource.setTargetDataSources(dsMap);
        if (null == dsMap.get(DBKey.DEFAULT)) {
            throw new RuntimeException(
                    String.format("Default DataSource [%s] not exists", DBKey.DEFAULT));
        }
        dataSource.setDefaultTargetDataSource(dsMap.get(DBKey.DEFAULT));
        return dataSource;
    }



@ConfigurationProperties(prefix = "mybatis")
@Data
public class MybatisProperties {

    private Map<String, String> params;

    private Map<String, Object> nodes;

    /**
     * mapper文件路徑:多個location以,分隔
     */
    private String mapperLocations = "classpath*:com/iqiyi/xiu/**/mapper/*.xml";

    /**
     * Mapper類所在的base package
     */
    private String basePackage = "com.iqiyi.xiu.**.repository";

    /**
     * mybatis配置文件路徑
     */
    private String configLocation = "classpath:mybatis-config.xml";
}

那threadLocal中的key什麼時候進行銷毀呢,其實可以自定義一個基於mybatis的攔截器,在攔截器中主動調DBContextHolder.clear()方法銷毀這個key。具體代碼就不貼瞭。這樣一來,我們就完成瞭一個基於註解的支持多數據源切換的中間件。

那有沒有可以優化的點呢?其實,可以發現,在獲取mapper接口/所在類的註解的時候,使用瞭反射來獲取的,那我們知道一般反射調用是比較耗性能的,所以可以考慮在這裡加個本地緩存來優化下性能:

private final static Map<String, String> METHOD_CACHE = new ConcurrentHashMap<>();
//....
public void innerBefore(JoinPoint point, boolean isClass) {
        String methodName = point.getSignature().getName();

        Class<?> clazz = getClass(point, isClass);
        //key為類名+方法名
        String keyString = clazz.toString() + methodName;
        //使用默認數據源
        String dbKey = DBKey.DEFAULT;
        //如果緩存中已經有這個mapper方法對應的數據源的key,那直接設置
        if (METHOD_CACHE.containsKey(keyString)) {
            dbKey = METHOD_CACHE.get(keyString);
        } else {
            Class<?>[] parameterTypes =
                    ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
            Method method = null;

            try {
                method = clazz.getMethod(methodName, parameterTypes);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
            }
             //方法上存在註解,使用方法定義的datasource
            if (method.isAnnotationPresent(DBKey.class)) {
                DBKey key = method.getAnnotation(DBKey.class);
                dbKey = key.value();
            } else {
                clazz = method.getDeclaringClass();
                //使用類上定義的註解
                if (clazz.isAnnotationPresent(DBKey.class)) {
                    DBKey key = clazz.getAnnotation(DBKey.class);
                    dbKey = key.value();
                }
            }
           //先放本地緩存
            METHOD_CACHE.put(keyString, dbKey);
        }
        DBContextHolder.setDBKey(dbKey);
    }

這樣一來,隻有在第一次調用這個mapper接口的時候,才會走反射調用的邏輯去獲取對應的數據源,後續,都會走本地緩存,提升瞭性能。

到此這篇關於基於註解的springboot+mybatis的多數據源組件的實現代碼的文章就介紹到這瞭,更多相關springboot mybatis多數據源組件內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀:

    None Found