1. 程式人生 > >在Mybatis-spring上基於註解的數據源實現方案

在Mybatis-spring上基於註解的數據源實現方案

操作數 實例 模板 開發工程師 normal 取數 inter -c star

一、遇到的痛點

最近在學習Spring-boot過程中,涉及到操作數據庫。按照DOC引入mybatis-spring-boot-starter,然後按照套路配置application.properties、碼Mapper、dataobject、xxx-mapper.xml的代碼就OK了。這個時候,采用DataSourceAutoConfiguration默認方式實現的,這時單數據源可用了。這種方式,網上有很Blog。
但是,我是測試開發工程師,自動化工程經常要連N個數據源。對於多數據源,網上提供了重寫DataSourceAutoConfiguration的方式。代碼如下:

@Configuration
@MapperScan(basePackages = "com.youzan.springboot.dal.master", sqlSessionTemplateRef  = "masterSST")
public class MasterSouceConfig {

    private String localMapper = "classpath:mapper/*.xml";

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    @Primary
    public DataSource buildDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "masterSSF")
    @Primary
    public SqlSessionFactory buildSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws  Exception {
        SqlSessionFactoryBean bean;
        bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(localMapper));
        return bean.getObject();
    }

    @Bean(name = "masterTM")
    @Primary
    public DataSourceTransactionManager buildTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "masterSST")
    @Primary
    public SqlSessionTemplate buildSqlSessionTemplate(@Qualifier("masterSSF") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

這個方式,確實可用,不足在於,需要根據不同數據源建立不同的package,一旦數據源發生變更,需要更改所在的package。也看過了動態數據源,那也不是我想要的。

二、方案探索

我在思考能不能基於註解來指定數據源呢?
然後開始寫個註解DataSourceRoute。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceRoute {

    String name() default "master";
}

之後,寫了AOP處理器來檢測這個註解,一直無法正確切入。那我在想是不是可以通過重寫mybatis啟動掃描方式實現多數據源呢?然後,閱讀了下mybatis-spring的源碼。org.mybatis.spring.mapper.ClassPathMapperScanner.processBeanDefinitions發現,啟動時,mybatis生成了MapperFactoryBean對象。

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {  
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name ‘" 
                      + holder.getBeanName() + "‘ and ‘" 
                      + definition.getBeanClassName() 
                      + "‘ mapperInterface");
      }

      definition.getConstructorArgumentValues()
           .addGenericArgumentValue(definition.getBeanClassName()); 
      definition.setBeanClass(this.mapperFactoryBean.getClass());
      definition.getPropertyValues()
           .add("addToConfig",this.addToConfig);

然後,我通過Debug看下生成的對象,驗證對代碼的理解。那就朝著創建MapperFactoryBean去就好了。技術分享

三、具體方案實現

3.1 知識儲備

請通過網絡等途徑了解下BeanDefinition、BeanDefinitionRegistryPostProcessor、ApplicationContextAware、BeanFactoryPostProcessor、InitializingBean、MapperFactoryBean、MapperProxyFactory、ClassPathMapperScanner、GenericBeanDefinition。前面這些,在你閱讀mybatis源碼時會看到,請先了解。

3.2 實現內容

  • 實現多數據源的加載
  • Mapper對象掃描加載
  • 生成MapperFactoryBean對象與裝配

下面直接上代碼。

3.2.1 讀取配置文件公共類

@Data
public class Config {

    // dao的package,現在只支持一個包名
    private String daoPath;

    //  *-mapper.xml的目錄信息
    private String mapperPath;

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:56
     * @modify history:
     *
     * @desc:
     *   1、讀取數據庫、DAO初始化需要的一些配置信息
     *
     */
    public Config() {
        InputStream in = 
             this.getClass().getClassLoader()
                 .getResourceAsStream("application-db.properties");
        if (in != null) {
            Properties properties = new Properties();
            try {
                properties.load(in);
            } catch (IOException e) {
                throw new BeanInitializationException("加載屬性配置文件過程失敗。", e);
            }
            daoPath = properties.getProperty("mybatis.dao-path");
            mapperPath = properties.getProperty("mybatis.mapper-locations");
        }
    }
}

3.2.2 實現多數據源的加載

第一步、構造多數據源的DataSource

/**
 * youzan.com Inc.
 * Copyright (c) 2012-2017 All Rights Reserved.
 *
 * @author: [email protected] 無影
 * @date 17/9/20 下午1:20
 * @desc
 */
@Data
public class DataSourceBuilder {

    /**
     * 存儲實例化後的多數據元對象
     */
    private Map<String, DataSource>  dataSourceMap  = new HashMap<>();

    /**
     * 存儲數據庫別名,在DAO類中,只能使用這些別名
     */
    private List<String>  dataSourceAlias = new ArrayList<>();

    /**
     *
     * 存儲數據源配置信息,按照數據源分組
     */
    private Map<String, Map<String, String>> dataSourceProperties = new HashMap<>();

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午2:10
     * @modify history:
     *
     * @desc:
     * 1、讀取系統classpath環境下,application-db.properties文件的數據庫配置
     * 2、將數據庫配置按照數據源進行分組
     * 3、實例化javax.sql.DataSource對象
     *
     * @return DataSourceBuilder
     *
     */
    public DataSourceBuilder builder() {
        InputStream in = this.getClass().getClassLoader().
                              getResourceAsStream("application-db.properties");
        if (in != null) {
            Properties properties = new Properties();
            try {
                properties.load(in);
            } catch (IOException e) {
                throw new BeanInitializationException("read property file error!", e);
            }
            //結束數據庫配置信息
            Iterator<String> propertyKeys = properties.stringPropertyNames().iterator();
            while (propertyKeys.hasNext()) {
                String key = propertyKeys.next();
                String value = properties.getProperty(key);
                String[] keys = key.split("[.]");
                if (dataSourceProperties.containsKey(keys[0])) {
                    dataSourceProperties.get(keys[0]).put(key, value);
                } else {
                    Map<String, String> innerMap = new HashMap<>();
                    innerMap.put(key, value);
                    dataSourceProperties.put(keys[0], innerMap);
                    dataSourceAlias.add(keys[0]);
                }
            }
            /**
             * 生成數據源
             */
            Iterator<String> DSNames = dataSourceProperties.keySet().iterator();
            while (DSNames.hasNext()) {
                String dsName = DSNames.next();
                Map<String, String> dsconfig = dataSourceProperties.get(dsName);
                DataSource dataSource = org.springframework.boot.autoconfigure.jdbc
                    .DataSourceBuilder.create()
                    .type(MysqlDataSource.class).
                    .driverClassName(dsconfig.get(dsName + ".datasource.driver-class-name")
                    .url(dsconfig.get(dsName + ".datasource.url"))
                    .username(dsconfig.get(dsName + ".datasource.username"))
                    .password(dsconfig.get(dsName + ".datasource.password")).build();
                dataSourceMap.put(dsName, dataSource);  
            }
        }
        return this;
    }
}

第二步、構造SqlSessionFactoryBean對象

@Data
public class SqlSessionFactoryBuilder {

    /**
     * 數據庫與實體對象間映射文件目錄
     */
    private String localMapper = "classpath:mapper/*.xml";

    /**
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午2:28
     * @modify history:
     * @desc:
     * 1、創建一個SqlSessionFactoryBean實例對象
     *
     * @param dbAlias
     * @param dataSource
     * @return
     */
    public SqlSessionFactoryBean builder(String dbAlias, DataSource dataSource)throws Exception{
        SqlSessionFactoryBean bean;
        bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(localMapper));
        bean.afterPropertiesSet();
        return bean;
    }
}

第三步、構造SqlSessionFactoryBean對象

/**
 * youzan.com Inc.
 * Copyright (c) 2012-2017 All Rights Reserved.
 *
 * @author: [email protected] 無影
 * @date 17/9/20 下午2:31
 * @desc
 */
@Data
public class SqlSessionTemplateBuilder {

    /**
     * SqlSessionFactory構建實體
     */
    SqlSessionFactoryBuilder  ssfb = new SqlSessionFactoryBuilder();

    /**
     * 
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午2:31
     * @modify history:
     *  
     * @desc:
     * 1、創建一個SqlSessionFactoryBean實例對象
     *
     * @param dbAlias
     * @param dataSource
     * @return
     */
    public SqlSessionTemplate builder(String dbAlias, DataSource dataSource)throws Exception{
        SqlSessionFactoryBean bean = ssfb.builder(dbAlias,dataSource);
        return new SqlSessionTemplate(bean.getObject());
    }
}

3.2.3 Mapper對象掃描加載

/**
 *
 * youzan.com Inc.
 * Copyright (c) 2012-2017 All Rights Reserved.
 *
 * @author: [email protected] 無影
 * @date 17/9/20 下午3:29
 * @desc
 *   1、掃描指定package路徑下的類文件列表
 */
public class ClassScanner {



    /**
     * 掃描的包路徑
     */
    String scanpPackage ;

    /**
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:49
     * @modify history:
     *
     * @desc:
     *    1、掃描指定package下的所有*DAO文件,並轉換成Class<?>
     *
     * @return Map<String, Class<?>>
     *     key:為DAO的alais,例如 AppInfoDao,key則為appInfoDao。
     *     value: Class類型的類信息,非實例化的
     *
     * @throws Exception
     */
    public Map<String, Class<?>> scan() throws Exception{

        Config config =  new Config();
        scanpPackage = config.getDaoPath();

        Map<String,Class<?>>  classMap = new HashMap<>();

        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        String packagePath = scanpPackage.replace(".", "/");

        URL url = loader.getResource(packagePath);
        List<String> fileNames = null;
        if (url != null) {
            String type = url.getProtocol();
            if ("file".equals(type)) {
                fileNames = getClassNameByFile(url.getPath(), null, true);
            }
        }
        for (String classPath : fileNames) {
            classMap.putAll(this.getClassByPath(classPath));
        }
        return classMap;
    }

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:51
     * @modify history:
     *
     * @desc:
     *   1、讀取package下的所有類文件
     *
     * @param filePath
     * @param className
     * @param childPackage
     * @return
     */
    private static List<String> getClassNameByFile(String filePath, List<String> className, boolean childPackage) {
        List<String> myClassName = new ArrayList<String>();
        File file = new File(filePath);
        File[] childFiles = file.listFiles();
        for (File childFile : childFiles) {
            if (childFile.isDirectory()) {
                if (childPackage) {
                    myClassName.addAll(getClassNameByFile(childFile.getPath(), myClassName, childPackage));
                }
            } else {
                String childFilePath = childFile.getPath();
                if (childFilePath.endsWith(".class")) {
                    childFilePath = childFilePath.substring(childFilePath.indexOf("\\classes") + 9,
                        childFilePath.lastIndexOf("."));
                    childFilePath = childFilePath.replace("\\", ".");
                    myClassName.add(childFilePath);
                }
            }
        }

        return myClassName;
    }

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:52
     * @modify history:
     *
     * @desc:
     *   1、將DAO的標準文件,轉成 DAO Class
     *
     * @param classPath
     * @return
     * @throws Exception
     */
    public Map<String, Class<?>> getClassByPath(String classPath) 
     throws Exception{
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Map<String, Class<?>> classMap = new HashMap<>();
        classMap.put(this.getClassAlias(classPath),loader.loadClass(this.getFullClassName(classPath)));
        return classMap;
    }

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:53
     * @modify history:
     *
     * @desc:
     *   1、將DAO的標準文件,轉成java標準的類名稱
     *
     * @param classPath
     * @return
     * @throws Exception
     */
    private String getFullClassName(String classPath) 
      throws Exception{
        int comIndex = classPath.indexOf("com");
        classPath = classPath.substring(comIndex);
        classPath = classPath.replaceAll("\\/", ".");
        return classPath;
    }

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:54
     * @modify history:
     *
     * @desc:
     *   1、根據類地址,獲取類的Alais,即根據名稱,按照駝峰規則,生成可作為變量的名稱
     *
     * @param classPath
     * @return
     * @throws Exception
     */
    private String getClassAlias(String classPath) 
     throws Exception{
        String  split = "\\/";
        String[] classTmp = classPath.split(split);
        String className = classTmp[classTmp.length-1];
        return this.toLowerFisrtChar(className);
    }

    /**
     *
     * @author:  [email protected] 無影
     * @date: 17/9/20 下午6:55
     * @modify history:
     *
     * @desc:
     *  1、將字符串的第一個字母轉小寫
     *
     * @param className
     * @return
     */
    private String toLowerFisrtChar(String className){
        String  fisrtChar = className.substring(0,1);
        fisrtChar = fisrtChar.toLowerCase();
        return fisrtChar+className.substring(1);
    }
}

3.2.4 生成MapperFactoryBean對象與裝配

前面獲取了所有DAO類的Map集合,同時實現了多數據源的加載。這裏通過org.mybatis.spring.mapper.MapperFactoryBean把DAO、數據源模板進行綁定,並註入到Spring Bean工程池了。

@Component
public class MapperScanner implements BeanFactoryPostProcessor, InitializingBean {

    /**
     * SqlSessionTemplate集合,按照數據庫Alias分組
     */
    Map<String, SqlSessionTemplate> sstMap = new HashMap<>();

    @Override
    public void afterPropertiesSet() throws Exception {

    }

    public void buildSqlSessionTemplate(Map<String, DataSource> dataSourceMap) throws Exception {
        Iterator<String> dataSourceIter = dataSourceMap.keySet().iterator();
        while (dataSourceIter.hasNext()) {
            String dbAlias = dataSourceIter.next();
            DataSource db = dataSourceMap.get(dbAlias);

            SqlSessionTemplateBuilder sstb = new SqlSessionTemplateBuilder();

            sstMap.put(dbAlias, sstb.builder(dbAlias, db));
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 
      throws BeansException {
        /**
         * 加載所有到DAO類
         */
        ClassScanner classScanner = new ClassScanner();
        Map<String, Class<?>> daoClasses = new HashMap<>();
        try {
            daoClasses = classScanner.scan();
        } catch (Exception e) {
            throw new BeanInstantiationException(this.getClass(), e.getMessage());
        }

        /**
         * 加載多數據源
         */
        DataSourceBuilder dsBuiler = new DataSourceBuilder();
        Map<String, DataSource> dataSourceMap = dsBuiler.builder().getDataSourceMap();
        try {
            this.buildSqlSessionTemplate(dataSourceMap);
        } catch (Exception e) {
            throw new BeanInstantiationException(this.getClass(), e.getMessage());
        }

        /**
         * 生命可執行數據庫DAO代理對象
         */
        try {
            Iterator<String> classIter = daoClasses.keySet().iterator();
            while (classIter.hasNext()) {
                String classAlias = classIter.next();
                Class<?> classBean = daoClasses.get(classAlias);
                /**
                 * 獲取該類上的數據源註解
                 */
                DataSourceRoute annotation = classBean.getAnnotation(DataSourceRoute.class);
                //實例化MapperFactory
                MapperFactoryBean bean = new MapperFactoryBean();
                // 給MapperFactory指定其應該使用的數據庫模
                String  dbAlias = annotation.name();
                bean.setSqlSessionTemplate(sstMap.get(dbAlias));
                // 指定DAO
                bean.setMapperInterface(classBean);
                // 刷新
                bean.afterPropertiesSet();
                // 寫入Spring Bean工廠裏
                beanFactory.registerSingleton(classAlias, bean.getObject());
            }
        } catch (Exception e) {
            throw new BeanInstantiationException(this.getClass(), e.getMessage());
        }
    }
}

3.2.5 應用

這時,我們就可以修改DAO的實現。指定的數據源名稱為配置文件裏數據庫配置信息的第一段名稱,例如:「master.datasource.url=jdbc:mysql://127.0.0.1:3006/testdb」,這時名稱就是master。同時去掉了Spring-boot指導方案中的@Mapper註解。

@DataSourceRoute(name="master")
public interface AppInfoDAO {  
    int delete(Integer id);
    int insert(AppInfoDO appInfoDO);
    int insertSelective(AppInfoDO appInfoDO);
    AppInfoDO select(Integer id);
    int updateByPrimaryKeySelective(AppInfoDO appInfoDO);
    int update(AppInfoDO appInfoDO);
}

修改Spring-boot啟動的入口Application類,排除DataSourceAutoConfiguration的加載。

@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Bootstrap {  
    public static void main(String[] args) {
        SpringApplication.run(Bootstrap.class,args);
    }
}

至此,就可以啟動測試了。
這個方案,只是做個引子,沒有完全按照Spring的標準實現。Spring的標準要求,應該把DataSoure、SqlSessionFactoryBean、SqlSessionTemplate註入Spring工程池裏,並給所有DAO類指定Bean的生命周期等。

在Mybatis-spring上基於註解的數據源實現方案