1. 程式人生 > 實用技巧 >Mybatis 流程解析 之 配置載入

Mybatis 流程解析 之 配置載入

Mybatis載入配置的流程 我們知道mybatis在ORM框架中具有舉足輕重的地位,接下來幾篇部落格文章將對mybatis的原理和設計理念進行解析,會發現很多很多令人驚喜的設計和想法。 在mybatis的原始碼分析流程中,我大概分成四個模組:1、配置載入;2、對映繫結;3、執行操作,封裝結果;4、外掛開發 這篇文章的首先介紹配置載入模組。 首先,我們提出這樣的一個問題,就是mybatis是怎樣將SQL語句,配置資料來源,配置快取,在mybatis-config.xml檔案配置,我們就可以實現自動注入的呢?這種配置化的程式設計方式,方便了程式設計師的開發, 也遮蔽掉了傳統JDBC的各種設定,究竟是怎樣做的?
問題提出來了,我們需要回答這個問題的時候,首先想到的是,我們從配置檔案中載入的這些東西,應該是有一個地方去儲存他們吧,想到這個,mybatis的核心配置類Configuration 就被設計出來了, 首先看下Configuration主要包含哪些東西。這裡我只是貼了一些Configuration中的成員變數,並加了註釋。至於其他構造方法等,等用到的時候我們在詳細解釋。
/**
 * @author Clinton Begin
 */
public class Configuration {

    protected Environment environment;

    /* 是否啟用行內巢狀語句*
*/ protected boolean safeRowBoundsEnabled; protected boolean safeResultHandlerEnabled = true; /* 是否啟用資料組A_column自動對映到Java類中的駝峰命名的屬性**/ protected boolean mapUnderscoreToCamelCase; /*當物件使用延遲載入時 屬性的載入取決於能被引用到的那些延遲屬性,否則,按需載入(需要的是時候才去載入)**/ protected boolean aggressiveLazyLoading;
/*是否允許單條sql 返回多個數據集 (取決於驅動的相容性) default:true **/ protected boolean multipleResultSetsEnabled = true; /*-允許JDBC 生成主鍵。需要驅動器支援。如果設為了true,這個設定將強制使用被生成的主鍵,有一些驅動器不相容不過仍然可以執行。 default:false**/ protected boolean useGeneratedKeys; /* 使用列標籤代替列名。不同的驅動在這方面會有不同的表現, 具體可參考相關驅動文件或通過測試這兩種不同的模式來觀察所用驅動的結果。**/ protected boolean useColumnLabel = true; /*配置全域性性的cache開關,預設為true**/ protected boolean cacheEnabled = true; protected boolean callSettersOnNulls; protected boolean useActualParamName = true; protected boolean returnInstanceForEmptyRow; /* 日誌列印所有的字首 **/ protected String logPrefix; /* 指定 MyBatis 所用日誌的具體實現,未指定時將自動查詢**/ protected Class<? extends Log> logImpl; protected Class<? extends VFS> vfsImpl; /* 設定本地快取範圍,session:就會有資料的共享,statement:語句範圍,這樣不會有資料的共享**/ protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; /* 設定但JDBC型別為空時,某些驅動程式 要指定值**/ protected JdbcType jdbcTypeForNull = JdbcType.OTHER; /* 設定觸發延遲載入的方法**/ protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString")); /* 設定驅動等待資料響應超時數**/ protected Integer defaultStatementTimeout; /* 設定驅動返回結果數的大小**/ protected Integer defaultFetchSize; /* 執行型別,有simple、resue及batch**/ protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; /*指定 MyBatis 應如何自動對映列到欄位或屬性*/ protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE; protected Properties variables = new Properties(); protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); /*MyBatis每次建立結果物件的新例項時,它都會使用物件工廠(ObjectFactory)去構建POJO*/ protected ObjectFactory objectFactory = new DefaultObjectFactory(); protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); /*延遲載入的全域性開關*/ protected boolean lazyLoadingEnabled = false; /*指定 Mybatis 建立具有延遲載入能力的物件所用到的代理工具*/ protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL protected String databaseId; /** * Configuration factory class. * Used to create Configuration for loading deserialized unread properties. * * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a> */ protected Class<?> configurationFactory; /*外掛集合*/ protected final InterceptorChain interceptorChain = new InterceptorChain(); /*TypeHandler註冊中心*/ protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(); /*TypeAlias註冊中心*/ protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); //------------------------------------------------------------- /*mapper介面的動態代理註冊中心*/ protected final MapperRegistry mapperRegistry = new MapperRegistry(this); /*mapper檔案中增刪改查操作的註冊中心*/ protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection"); /*mapper檔案中配置cache節點的 二級快取*/ protected final Map<String, Cache> caches = new StrictMap<>("Caches collection"); /*mapper檔案中配置的所有resultMap物件 key為名稱空間+ID*/ protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection"); protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection"); /*mapper檔案中配置KeyGenerator的insert和update節點,key為名稱空間+ID*/ protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection"); /*載入到的所有*mapper.xml檔案*/ protected final Set<String> loadedResources = new HashSet<>(); /*mapper檔案中配置的sql元素,key為名稱空間+ID*/ protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>(); protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>(); protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>(); protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>(); /* * A map holds cache-ref relationship. The key is the namespace that * references a cache bound to another namespace and the value is the * namespace which the actual cache is bound to. */ protected final Map<String, String> cacheRefMap = new HashMap<>(); public Configuration(Environment environment) { this(); this.environment = environment; } }

從設計程式碼上,我們不難發現,Configuration 物件 是真的牛啊,在這裡回答一個mybatis關於二級快取的知識點,二級快取的生命週期是什麼時候?回答:是mybatis的整個生命週期,因為如果二級快取的生命週期不是mybatis的生命週期,

那麼我們在操作二級快取的時候,我們就沒有必要將二級快取的物件設定在Configuration物件中,當然二級快取除了生命週期是整個mybatis的生命週期之外,他本身的作用於是namaspace範圍的,這個我們後面會詳細講,這裡暫時先說個結論。

從配置檔案中的設定的各種屬性我們已經找到了儲存的地方,那麼mybatis是在什麼時候,將這些資料怎麼放進去的呢?也就是解決when和how的問題,

接下來就看下繼承於BaseBuilder的三個好兒子了,分別是XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder。

我們首先看下大兒子XMLConfigBuilder 幹了些什麼事

private void parseConfiguration(XNode root) {
        try {
            //issue #117 read properties first
            //解析<properties>節點
            propertiesElement(root.evalNode("properties"));
            //解析<settings>節點
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            //解析<typeAliases>節點
            typeAliasesElement(root.evalNode("typeAliases"));
            //解析<plugins>節點
            pluginElement(root.evalNode("plugins"));
            //解析<objectFactory>節點
            objectFactoryElement(root.evalNode("objectFactory"));
            //解析<objectWrapperFactory>節點
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            //解析<reflectorFactory>節點
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);//將settings填充到configuration
            // read it after objectFactory and objectWrapperFactory issue #631
            //解析<environments>節點
            environmentsElement(root.evalNode("environments"));
            //解析<databaseIdProvider>節點
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            //解析<typeHandlers>節點
            typeHandlerElement(root.evalNode("typeHandlers"));
            //解析<mappers>節點
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

程式碼中,我們可以看到,XMLConfigBuilder 大兒子主要解析的是mybatis-config.xml中的一級節點,他主要就是解析了之後,將資料add到Configuration物件中,問題,XMLConfigBuilder大兒子是怎麼拿到Configuration物件的呢?

後來我們發現,這件事讓XMLConfigBuilder大兒子的爸爸BaseBuilder給做了,在SqlSessionFactoryBuilder buildSqlSessionFactory的時候,我們可以看到他會通過大兒子的例項化方法將大兒子例項化出來,在大兒子的例項化方法中又

會 執行他老爸的 BaseBuilder方法,所以,這裡解決了三個兒子為什麼會持有 Configuration 物件的問題。

大兒子只負責一級標籤的解析,又把其他任務扔給了老二XMLMapperBuilder ,從老二的名字可以看出,老二的主要職責是解析 mapper檔案的,主要核心方法是parse()->configurationElement()

 private void configurationElement(XNode context) {
        try {
            //獲取mapper節點的namespace屬性
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            //設定builderAssistant的namespace屬性
            builderAssistant.setCurrentNamespace(namespace);
            //解析cache-ref節點
            cacheRefElement(context.evalNode("cache-ref"));
            //重點分析 :解析cache節點----------------1-------------------
            cacheElement(context.evalNode("cache"));
            //解析parameterMap節點(已廢棄)
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            //重點分析 :解析resultMap節點(基於資料結果去理解)----------------2-------------------
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            //解析sql節點
            sqlElement(context.evalNodes("/mapper/sql"));
            //重點分析 :解析select、insert、update、delete節點 ----------------3-------------------
            buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }

其中我們需要重點去了解的方法是cacheRefElement、resultMapElements、buildStatementFromContext 這三個方法。

cacheRefElement 方法主要是應對二級快取而存在的方法。

private void cacheElement(XNode context) throws Exception {
        if (context != null) {
            //獲取cache節點的type屬性,預設為PERPETUAL
            String type = context.getStringAttribute("type", "PERPETUAL");
            //根據type對應的cache介面的實現
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            //讀取eviction屬性,既快取的淘汰策略,預設LRU
            String eviction = context.getStringAttribute("eviction", "LRU");
            //根據eviction屬性,找到裝飾器
            Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
            //讀取flushInterval屬性,既快取的重新整理週期
            Long flushInterval = context.getLongAttribute("flushInterval");
            //讀取size屬性,既快取的容量大小
            Integer size = context.getIntAttribute("size");
            //讀取readOnly屬性,既快取的是否只讀
            boolean readWrite = !context.getBooleanAttribute("readOnly", false);
            //讀取blocking屬性,既快取的是否阻塞
            boolean blocking = context.getBooleanAttribute("blocking", false);
            Properties props = context.getChildrenAsProperties();
            //通過builderAssistant建立快取物件,並新增至configuration
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }


//通過builderAssistant建立快取物件,並新增至configuration
  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    //經典的建造起模式,建立一個cache物件
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    //將快取新增至configuration,注意二級快取以名稱空間為單位進行劃分
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

resultMapElements 主要作用是為了解析mapper檔案中的ResultMap標籤,這裡面就是具體的解析出來返回的Result結果,具體可以去參照程式碼

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
        ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
        //獲取resultmap節點的id屬性
        String id = resultMapNode.getStringAttribute("id",
                resultMapNode.getValueBasedIdentifier());
        //獲取resultmap節點的type屬性
        String type = resultMapNode.getStringAttribute("type",
                resultMapNode.getStringAttribute("ofType",
                        resultMapNode.getStringAttribute("resultType",
                                resultMapNode.getStringAttribute("javaType"))));
        //獲取resultmap節點的extends屬性,描述繼承關係
        String extend = resultMapNode.getStringAttribute("extends");
        //獲取resultmap節點的autoMapping屬性,是否開啟自動對映
        Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
        //從別名註冊中心獲取entity的class物件
        Class<?> typeClass = resolveClass(type);
        Discriminator discriminator = null;
        //記錄子節點中的對映結果集合
        List<ResultMapping> resultMappings = new ArrayList<>();
        resultMappings.addAll(additionalResultMappings);
        //從xml檔案中獲取當前resultmap中的所有子節點,並開始遍歷
        List<XNode> resultChildren = resultMapNode.getChildren();
        for (XNode resultChild : resultChildren) {
            if ("constructor".equals(resultChild.getName())) {//處理<constructor>節點
                processConstructorElement(resultChild, typeClass, resultMappings);
            } else if ("discriminator".equals(resultChild.getName())) {//處理<discriminator>節點
                discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
            } else {//處理<id> <result> <association> <collection>節點
                List<ResultFlag> flags = new ArrayList<>();
                if ("id".equals(resultChild.getName())) {
                    flags.add(ResultFlag.ID);//如果是id節點,向flags中新增元素
                }
                //建立ResultMapping物件並加入resultMappings集合中
                resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
            }
        }
        //例項化resultMap解析器
        ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
        try {
            //通過resultMap解析器例項化resultMap並將其註冊到configuration物件
            return resultMapResolver.resolve();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteResultMap(resultMapResolver);
            throw e;
        }
    }

buildStatementFromContext 方法中主要是牽扯到了BaseBuilder的第三個兒子,即XMLStatementBuilder,這第三個兒子做的事,就是解析SQL語句的select、update、delete、insert語句,也就是BaseBuilder和XMLConfigBuilder、XMLMapperBuilder將準備工作都做好了,現在該小兒子去分析具體的SQL語句了。

public void parseStatementNode() {
        //獲取sql節點的id
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");

        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
        /*獲取sql節點的各種屬性*/
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultType = context.getStringAttribute("resultType");
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);

        Class<?> resultTypeClass = resolveClass(resultType);
        String resultSetType = context.getStringAttribute("resultSetType");
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);


        //根據sql節點的名稱獲取SqlCommandType(INSERT, UPDATE, DELETE, SELECT)
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

        // Include Fragments before parsing
        //在解析sql語句之前先解析<include>節點
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());

        // Parse selectKey after includes and remove them.
        //在解析sql語句之前,處理<selectKey>子節點,並在xml節點中刪除
        processSelectKeyNodes(id, parameterTypeClass, langDriver);

        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        //解析sql語句是解析mapper.xml的核心,例項化sqlSource,使用sqlSource封裝sql語句
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        String resultSets = context.getStringAttribute("resultSets");//獲取resultSets屬性
        String keyProperty = context.getStringAttribute("keyProperty");//獲取主鍵資訊keyProperty
        String keyColumn = context.getStringAttribute("keyColumn");///獲取主鍵資訊keyColumn

        //根據<selectKey>獲取對應的SelectKeyGenerator的id
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);


        //獲取keyGenerator物件,如果是insert型別的sql語句,會使用KeyGenerator介面獲取資料庫生產的id;
        if (configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
            keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                    configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                    ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }

        //通過builderAssistant例項化MappedStatement,並註冊至configuration物件
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }

我們可以看到,二兒子和三兒子都是使用builderAssistant物件將自身解析出來的資料來放到Configuration物件中,這樣設計的目的是什麼?為什麼大兒子沒有使用builderAssistant去進行將資料載入到Configuration物件中呢?原因可能是大

兒子解析過程相對簡單,並沒有那麼複雜,向Configuration中新增的時候,也不會很複雜,而兒子和小兒子就不一樣了,一個需要處理快取,mapper中的ResultMap,一個需要處理具體的增刪改查的語句,其中牽扯到的點也是比較多,所以二兒子和小

兒子就找了個祕書,讓祕書完成他們不關心的事。

小兒子將 mapper檔案中的 namespace + 語句id 作為key value值MappedStatement物件 儲存在Configuration的mappedStatements變數中。

至此,mybatis的初始化載入資料的流程到此告一段落,第一階段中,mybatis將我們需要用到的所有資料全數儲存到Configuration物件中了,為下一階段的對映繫結打下了堅實的基礎。

具體流程圖可以參考我畫的一個,有點拙劣,請見諒。