【mybatis】mybatis數據源源碼剖析(JNDI、POOLED、UNPOOLED)

一、概述

    

二、創建

    mybatis數據源的創建過程稍微有些曲折。

    1. 數據源的創建過程;

    2. mybatis支持哪些數據源,也就是dataSource標籤的type屬性可以寫哪些合法的參數?

    弄清楚這些問題,對mybatis的整個解析流程就清楚了,同理可以應用於任何一個配置上的解析上。

    從SqlSessionFactoryBuilder開始追溯DataSource的創建。SqlSessionFactoryBuilder中9個構造方法,其中字符流4個構造方法一一對應字節流4個構造方法,都是將mybatis-config.xml配置文件解析成Configuration對象,最終導向build(Configuration configuration)進行SqlSessionFactory的構造。


    配置文件的在build(InputStream, env, Properties)構造方法中進行解析,InputStream和Reader方式除了流不一樣之外均相同,本處以InputStream爲例,追蹤一下源碼。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
  // mybatis-config.xml文件的解析對象
  // 在XMLConfigBuilder中封裝了Configuration對象
  // 此時還未真正發生解析,但是將解析的必備條件都準備好了
  XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  // parser.parse()的調用標誌着解析的開始
  // mybatis-config.xml中的配置將會被解析成運行時對象封裝到Configuration中
  return build(parser.parse());
} catch (Exception e) {
  throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
  ErrorContext.instance().reset();
  try {
	inputStream.close();
  } catch (IOException e) {
	// Intentionally ignore. Prefer previous error.
  }
}
}

    在XMLConfigBuilder進一步追蹤,疑問最終保留在其父類BaseBuilder的resolveClass方法上,該方法對數據源工廠的字節碼進行查找。

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
	// mybatis-config.xml的根節點就是configuration
	// 配置文件的解析入口
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      settingsElement(root.evalNode("settings"));
	  // environment節點包含了事務和連接池節點
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  
  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
		// 如果調用的build沒有傳入environment的id
		// 那麼就採用默認的environment,即environments標籤配置的default="environment_id"
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
		  // 數據源工廠解析
		  // 這裏是重點,數據源工廠的查找
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
		  // 工廠模式,生成相應的數據源
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
  
  private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
	  // dataSource標籤的屬性type
      String type = context.getStringAttribute("type");
	  // 解析dataSource標籤下的子標籤<property name="" value="">
	  // 實際上就是數據源的配置信息,url、driver、username、password等
      Properties props = context.getChildrenAsProperties();
	  // resolveClass:到XMLConfigBuilder的父類BaseBuilder中進行工廠Class對象的查找
	  // 這裏是重點
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

    在父類中並沒有窺探到重點,轉到其實例屬性typeAliasRegistry中才真正進行查找過程。

protected Class<?> resolveClass(String alias) {
    if (alias == null) return null;
    try {
	  // 做了一下檢查,轉
      return resolveAlias(alias);
    } catch (Exception e) {
      throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
  }
  protected Class<?> resolveAlias(String alias) {
	// BaseBuilder中的實例屬性
	// 實例屬性:protected final TypeAliasRegistry typeAliasRegistry;
    return typeAliasRegistry.resolveAlias(alias);
  }<span style="font-family: SimSun; background-color: rgb(255, 255, 255);">  </span>

    typeAliasRegistry中實際上是在一個Map中進行KV的匹配。

public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) return null;
      String key = string.toLowerCase(Locale.ENGLISH); // issue #748
      Class<T> value;
      if (TYPE_ALIASES.containsKey(key)) {
		// private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
		// TYPE_ALIASES是一個實例屬性,類型是一個Map
        value = (Class<T>) TYPE_ALIASES.get(key);
      } else {
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }

    那麼問題就來了,工廠類什麼時候被註冊到這個map中的?

    實際上在SqlSessionFactoryBuilder的build(InputStream, env, Propeerties)方法中調用parse解析配置文件之前,我們忽略了一段重要的代碼。


    查看創建XMLConfigBuilder的過程,根據繼承中初始化的規則,將會在父類BaseBuilder構造方法中創建Configuration對象,而Configuration對象的構造方法中將會註冊框架中的一些重要參數。

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
	// 轉
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
  
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    // 轉調父類構造方法
	// 同時最終要的是直接new Configuration()傳入父類
	// Configuration中的屬性TypeAliasRegistry將會註冊數據源工廠
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
public abstract class BaseBuilder {
  protected final Configuration configuration;
  protected final TypeAliasRegistry typeAliasRegistry;
  protected final TypeHandlerRegistry typeHandlerRegistry;

  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
	// typeAliasRegistry來自於Configuration
	// 也就是合理解釋了剛纔通過typeAliasRegistry來找數據源工廠
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }

    至此,數據源創建結束。接下來就看看怎麼用。


三、詳解

1. Mybatis datasource結構


2. mybatis JNDI

    mybatis JNDI之前已經剖析過源碼,此處不再進行剖析,原文鏈接:點擊打開鏈接

3. mybatis UNPOOLED

    mybatis UNPOOLED數據源創建的思想,先通過默認構造方法創建數據源工廠(此時UNPOOLED dataSource隨之創建),將mybatis-config.xml中數據源的配置信息通過setProperties傳給工廠,然後通過工廠getDataSource。回顧一下這一段源碼。



    最終是利用簡單的反射通過默認無參的構造方法實例化了數據源工廠,此時在數據源工廠中也實例化了UNPOOLED數據源對象。

    resolveClass(type)這句話,從configuration中拿到UNPOOLED對應的value,即org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory.class,然後通過無參構造器實例化工廠對象,在工廠的無參構造中也直接實例化了dataSource對象,即org.apache.ibatis.datasource.unpooled.UnpooledDataSource,然後調用setProperties方法,把配置文件中配置的參數(SqlSessionFactoryBuilder.builder中如果傳入Properties也會被putAll,同key則覆蓋value)進行dataSource設置。

    配置數據源,最重要的是connection的獲取和管理,通過UNPOOLED方式來配置數據源,實際上和直接是用JDBC沒有太多區別,操作的都是原生的、沒有任何修飾的connection。


4. POOLED

    POOLED工廠直接繼承UNPOOLED工廠,只是在POOLED工廠的默認構造中實例化org.apache.ibatis.datasource.pooled.PooledDataSource覆蓋了UNPOOLED中實例化的dataSource對象。其他的一模一樣,緊接着調用setProperties方法等。

    直接關注連接對象,通過POOLED,因爲連接池需要存放連接對象,因此連接對象的close方法需要進行改寫,連接池的狀態也需要進行管理(PoolState封裝了連接池的狀態)。


    至於獲取連接,會care PoolState中空閒鏈表中是否還有可用的connection,有則直接返回,沒有則看是否已經到達配置的最大連接數,沒有到達則new一個新的,這部分源碼較長但是邏輯簡單,就不貼出來了。直接看connection的代理部分吧。PooledConnection直接實現了JDK動態代理中的InvocationHandler,其invoke方法中重寫了close方法,將連接對象還回池中,當非close方法的時候,會檢查一下connection的狀態是否正常,正常則直接調用原邏輯。

class PooledConnection implements InvocationHandler {
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }
}

5. Mybatis集成其他數據源

    參看:點擊打開鏈接

    其實和第一節息息相關,直接使用mybatis的時候,在配置標籤<dataSource type="POOLED">,然後再Mybatis初始化的時候從Configuration中得到的POOLED=org.apache.ibatis.datasource.pooled.PooledDataSourceFactory.class,初始化這個對象就是直接通過POOLED得到class,然後newInstance通過默認構造方法直接實例化工廠對象,然後通過實例化出來的factory.setProperties(prop)把dataSource標籤中的配置參數一一注入工廠對象。

    源碼思想很簡單,這裏就不再貼出來了,因此要在純mybatis環境下要繼承其他數據源,例如C3P0或DBCP,只需要將其datasource繼承org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory即可,因爲setProperties方法在UnpooledDataSourceFactory中定義,並且默認POOLED也是繼承於此。

    Mybatis是否支持自定義的數據源,關鍵在於兩點,一是該數據源是否有默認構造函數(無參構造函數),二是可以通過get/set方式來進行數據源配置。滿足以上兩點,就足夠了。mybatis內置的數據源實在是弱得看不下去,所以集成其他數據源的時候,無論是傳統的JNDI/DBCP/C3P0還是當下牛逼閃閃的HikariCP也罷,都可以集成到mybatis中使用!當然,如果你使用Spring來處理數據源,那麼這裏就可以不用考慮了,Spring-mybatis的jar包已經幫你處理好了···


    其實整個邏輯還是很簡單的,源碼揭露一切。



附註:

    本文如有錯漏,煩請不吝指正,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章