Mybatis源碼研讀(一)—— XML解析

走進Mybatis

上文中簡單的介紹過了Mybatis的使用毒涧。本篇文章將介紹Mybatis如何解析XML

組件:

  • SqlSessionFactory
    用于生成SQLSession 的工廠類

  • SqlSession
    Mybatis 的核心API,表示和數(shù)據(jù)交互的會話襟诸,用于處理所有的增刪改查功能。

  • Configuration
    Configuration可以說是一個倉庫類了。所有的配置項都集中在這個類里面。在SqlSessionFactoryBuilder構(gòu)建SqlSessionFactory的時候奕剃,會創(chuàng)建XMLConfigBuilder,然后會解析到Configuration類哑姚,再通過這個Configuration類來構(gòu)建SqlSessionFactory祭饭。所有所有的XML文件信息到最后都是通過Configuration來承載,供SQLSessionFactory創(chuàng)建SQLSession叙量。

public class Configuration {
  protected Environment environment;

  protected boolean safeRowBoundsEnabled;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase;
  protected boolean aggressiveLazyLoading;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys;
  protected boolean useColumnLabel = true;
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls;
  protected boolean useActualParamName = true;
  protected boolean returnInstanceForEmptyRow;
  //日志前綴
  protected String logPrefix;
  // 日志接口
  protected Class <? extends Log> logImpl;
  protected Class <? extends VFS> vfsImpl;
  //
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  //數(shù)據(jù)庫類型
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
 //
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  protected Integer defaultStatementTimeout;
  protected Integer defaultFetchSize;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

  protected Properties variables = new Properties();
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

  protected boolean lazyLoadingEnabled = false;
  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 >Issue 300 (google code)</a>
   */
  protected Class<?> configurationFactory;

  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

  protected final Set<String> loadedResources = new HashSet<>();
  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<>();

解析XML的過程,可以理解為就是在填充Configuration的過程九串。

SqlSessionFactoryBuilder().build(inputStream);

一切的解析都是由build開始绞佩。最終會調(diào)用到 XMLConfigBuilder.parseConfiguration方法。

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      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);
    }
  }

在該方法中對每個節(jié)點進行單獨的解析猪钮。

properties

解析Mapper.xml

在解析XML中最重要的是就是解析Mapper.xml了品山,也就是代碼中的: mapperElement(root.evalNode("mappers"))
其最終會調(diào)用到:
XMLMapperBuilder.configurationElement

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      //是否開啟二級緩存 
      cacheElement(context.evalNode("cache"));
      //解析參數(shù)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析結(jié)果集
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析SQL節(jié)點
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析SQL語句
      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);
    }
  }
SQL節(jié)點

SQL節(jié)點可以將將重復的sql提取出來,在使用到的地方使用include醫(yī)用即可烤低,最終達到SQL重用的目的肘交。它的解析很簡單,就是把內(nèi)容放入sqlFragments容器扑馁。id為命名空間+節(jié)點ID

  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      id = builderAssistant.applyCurrentNamespace(id, false);
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
    }
  }
SQL語句

buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
這一段是Mybatis的核心所在涯呻。他會把Mapper.xml里面的SQL語句進行解析凉驻。分為兩種類型。存在動態(tài)標簽的會被解析為動態(tài)SQL, 不存在動態(tài)標簽的則被解析為靜態(tài)SQL复罐。

動態(tài)標簽指的是在SQL里面添加的 <if>涝登、<choose><foreach>效诅、<trim>胀滚、<set> 等的標簽。
例如:

    <select id="selectByIds" resultType="com.xavier.mybatis.User">
        SELECT * FROM TB_USER
        where id in
        <foreach collection="ids" item="id" index="index" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>

在解析SQL的時候乱投,會根據(jù)SQL是否有動態(tài)標簽來確定初始化的SqlSource的類型咽笼。動態(tài)的為DynamicSqlSource, 靜態(tài)的為RawSqlSource .

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

我們來看一下parseDynamicTags方法:

    MixedSqlNode rootSqlNode = parseDynamicTags(context);

如其名,他是解析SQL語句的戚炫,并且返回MixedSqlNode
MixedSqlNode 的結(jié)構(gòu)如下剑刑。它包含了一個SqlNode的List

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;
}

接著看它解析過程:

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
        //如果為靜態(tài)屬性,則添加 __StaticTextSqlNode__
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        //獲取節(jié)點的名字
        String nodeName = child.getNode().getNodeName();
        //根據(jù)節(jié)點的名字獲取節(jié)點處理器
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //節(jié)點處理器處理節(jié)點
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

可以看到嘹悼,這就是對子節(jié)點的處理叛甫。

  1. 判斷節(jié)點的內(nèi)容是不是純文本。如果是純文本就構(gòu)建靜態(tài)對象StaticTextSqlNode
  2. 如果是動態(tài)標簽杨伙,則獲取節(jié)點類型其监,然后交給對應(yīng)的類型處理。

為每一個動態(tài)標簽綁定不同的處理器在:
XMLScriptBuilder.initNodeHandlerMap

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

我們分析一下foreach的Handler:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String collection = nodeToHandle.getStringAttribute("collection");
      String item = nodeToHandle.getStringAttribute("item");
      String index = nodeToHandle.getStringAttribute("index");
      String open = nodeToHandle.getStringAttribute("open");
      String close = nodeToHandle.getStringAttribute("close");
      String separator = nodeToHandle.getStringAttribute("separator");
      ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
      targetContents.add(forEachSqlNode);
    }
  }

ForEachHandler的處理方式限匣,

  1. 遞歸調(diào)用parseDynamicTags(),繼續(xù)處理自己的節(jié)點
  2. 獲取自己定義的各種參數(shù): collection, item , index, open ,close ……
  3. 構(gòu)建ForEachSqlNode對象抖苦,然后把它加入到 targetContents

其他類型的處理器大致如此。

所以米死,我們最后解析獲取到的MixedSqlNode對象就是一棵以樹锌历,其中的contents是這棵樹的第一級子節(jié)點。

在初始化好了SqlSource之后峦筒,Mybatis會將SqlSource連同入?yún)⒕课鳎鰠ⅲ鹊纫幌盗袇?shù)封裝為一個MappedStatement物喷,然后注冊到configuration這個大管家類中:

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);

在大管家中的mappedStatements 屬性是一個StrictMap<MappedStatement> 卤材,所以存入的時候,key 為唯一識別這個Statement的東西峦失,在mybatis中是由 namespace+mappper中節(jié)點的id扇丛,在我們案例中就是:com.xavier.mybatis.UserMapper.selectUser

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尉辑,隨后出現(xiàn)的幾起案子帆精,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卓练,死亡現(xiàn)場離奇詭異隘蝎,居然都是意外死亡,警方通過查閱死者的電腦和手機昆庇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門末贾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人整吆,你說我怎么就攤上這事拱撵。” “怎么了表蝙?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵拴测,是天一觀的道長。 經(jīng)常有香客問我府蛇,道長集索,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任汇跨,我火速辦了婚禮务荆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘穷遂。我一直安慰自己,他們只是感情好蚪黑,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著忌穿,像睡著了一般抒寂。 火紅的嫁衣襯著肌膚如雪掠剑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天朴译,我揣著相機與錄音,去河邊找鬼动分。 笑死,一個胖子當著我的面吹牛澜公,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼迹辐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了明吩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤印荔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后详羡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡实柠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年水泉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窒盐。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡草则,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟹漓,到底是詐尸還是另有隱情炕横,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布牧牢,位于F島的核電站看锉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏塔鳍。R本人自食惡果不足惜伯铣,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望轮纫。 院中可真熱鬧腔寡,春花似錦、人聲如沸掌唾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糯彬。三九已至凭语,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撩扒,已是汗流浹背似扔。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炒辉。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓豪墅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黔寇。 傳聞我的和親對象是個殘疾皇子偶器,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354