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為例,追蹤一下原始碼。
[html] view plain copy
- 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方法上,該方法對資料來源工廠的位元組碼進行查詢。
[html] view plain copy
- 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中才真正進行查詢過程。
[html] view plain copy
- 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的匹配。
[html] view plain copy
- 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物件的構造方法中將會註冊框架中的一些重要引數。
[html] view plain copy
- 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;
- }
[html] view plain copy
- 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的狀態是否正常,正常則直接呼叫原邏輯。
[html] view plain copy
- 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包已經幫你處理好了···
其實整個邏輯還是很簡單的,原始碼揭露一切。
附註:
本文如有錯漏,煩請不吝指正,謝謝!