mybatis 源碼分析之 解析mapper.xml文件

  • mybatis 是如何解析我們定義的mapper.xml文件?解析後如何存儲?存在哪裏?
  • 想要解析必然先找到mapper.xml文件,mapperLocations  就是通過讀取我們的配置獲取到的xml Resource數組。
  private Resource[] mapperLocations;

mybatis.mapper-locations=classpath:/mapper/*.xml
  • 循環讀取mapper文件,進行解析。
  • 接下來我們看下解析是如何進行的?
  • 首先構建了一個xmlMapperBuilder  對象,該對象包含了mapper.xml文件流,全路徑文件名,Configuration 對象,以及當前對象的sqlFragments .即 sql 片段。
  • 接下來調用XMLMapperBuilder的parse方法進行具體的解析工作。具體方法如下。
      public void parse() {
    
        //這裏首先判斷該資源是否已經加載過,已加載則不處理。
        if (!configuration.isResourceLoaded(resource)) {
    
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    
     // XPathParser 類 parser.evalNode("/mapper")  構造一個XNode 節點對象。
    public XNode evalNode(String expression) {
        return evalNode(document, expression);
      }
    
      public XNode evalNode(Object root, String expression) {
        Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
        if (node == null) {
          return null;
        }
        return new XNode(this, node, variables);
      }
    
    // configurationElement  方法
      private void configurationElement(XNode context) {
        try {
           //  獲取命名空間,可見mapper文件的命名空間不可爲空也不能是空字符串  
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
           // 設置當前命名空間
          builderAssistant.setCurrentNamespace(namespace);
            // 解析cache-ref 相關配置
          cacheRefElement(context.evalNode("cache-ref"));
            // 解析cache配置
          cacheElement(context.evalNode("cache"));
            //  解析mapper 標籤下parameterMap 配置 參數配置 
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // 解析 mapper/resultMap 標籤,放入到configuration  的屬性resultMaps  。key 是由我們當前mapper文件的命名空間和標籤定義裏的id 拼接而來的。這樣不同的命名空間就可以只用相同的id定義,由此命名空間起到了隔離的作用。
    //  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
          resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析sql標籤 放入到  configuration   的屬性sqlFragments 參數裏,sql片段信息
    //   protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
          sqlElement(context.evalNodes("/mapper/sql"));
    // 這個方法是我們解析SQL語句的關鍵地方,這裏包含一些我們的if 等標籤的解析。解析完成後存放在
          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);
        }
      }
    
    // 獲取參數配置後放入到 builderAssistant 對象的
     private void parameterMapElement(List<XNode> list) {
        for (XNode parameterMapNode : list) {
          String id = parameterMapNode.getStringAttribute("id");
          String type = parameterMapNode.getStringAttribute("type");
          Class<?> parameterClass = resolveClass(type);
          List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
          List<ParameterMapping> parameterMappings = new ArrayList<>();
          for (XNode parameterNode : parameterNodes) {
            String property = parameterNode.getStringAttribute("property");
            String javaType = parameterNode.getStringAttribute("javaType");
            String jdbcType = parameterNode.getStringAttribute("jdbcType");
            String resultMap = parameterNode.getStringAttribute("resultMap");
            String mode = parameterNode.getStringAttribute("mode");
            String typeHandler = parameterNode.getStringAttribute("typeHandler");
            Integer numericScale = parameterNode.getIntAttribute("numericScale");
            ParameterMode modeEnum = resolveParameterMode(mode);
            Class<?> javaTypeClass = resolveClass(javaType);
            JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
            Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
            ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
            parameterMappings.add(parameterMapping);
          }
    //看此方法源碼可以知道參數對象放入到configuration 對象的屬性裏,由此可見configuration是一個容器
          builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
        }
      }
    
    // 這裏是添加mapper文件的配置參數以Map的形式放入到configuration 的屬性 parameterMaps  .
    //   protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    
    public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
        id = applyCurrentNamespace(id, false);
        ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
        configuration.addParameterMap(parameterMap);
        return parameterMap;
      }
    
       // 該方法是處理mapper 文件下的 insert|update|delete|select 標籤的主要方法。
      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        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
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
          keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
          keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
              configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
          resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
           
        // 經過一系列處理完畢後。這裏將會構建MappedStatement 對象用於存儲這些解析出來的信息。
        // 
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    
    
      public MappedStatement addMappedStatement(
          String id,
          SqlSource sqlSource,
          StatementType statementType,
          SqlCommandType sqlCommandType,
          Integer fetchSize,
          Integer timeout,
          String parameterMap,
          Class<?> parameterType,
          String resultMap,
          Class<?> resultType,
          ResultSetType resultSetType,
          boolean flushCache,
          boolean useCache,
          boolean resultOrdered,
          KeyGenerator keyGenerator,
          String keyProperty,
          String keyColumn,
          String databaseId,
          LanguageDriver lang,
          String resultSets) {
    
        if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        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 對象的 mappedStatements
    //  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .conflictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
             // mappedStatements 使用的Map實現類是StrictMap ,是由mybatis自定義的,這裏的put 方法可以看下。
        configuration.addMappedStatement(statement);
        return statement;
      }
    
    // 這個put 方法會檢查key是否包含.號,包含則會獲取一個短key,即. 分隔符分割成數組後取最後一個
    // 所以這裏會放入兩個key value 。 key不同,value 相同。
    // 短 key 的存儲這裏會有一個問題,因爲短key會重複,那麼重複的話
     public V put(String key, V value) {
          if (containsKey(key)) {
            throw new IllegalArgumentException(name + " already contains value for " + key
                + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
          }
          if (key.contains(".")) {
            final String shortKey = getShortName(key);
            if (super.get(shortKey) == null) {
              super.put(shortKey, value);
            } else {
                // 短key 重複,則短key對應的value值是短key本身封裝的一個對象。 
            // Ambiguity是一個靜態內部類對象。這裏簡單的封裝而不是使用字符串,可以便於以後的識別。
    
              super.put(shortKey, (V) new Ambiguity(shortKey));
            }
          }
          return super.put(key, value);
        }
            // Configuration 的內部類
      protected static class Ambiguity {
          final private String subject;
    
          public Ambiguity(String subject) {
            this.subject = subject;
          }
    
          public String getSubject() {
            return subject;
          }
        }
    

     

  • 通過以上的源碼跟蹤和分析我們可以得到:
  • 解析mapper.xml文件的結果使用mapper存儲,放在Configuration 生成的對象的屬性裏。
  • 通過對不同mapper下的標籤的解析,分別生成了不同的對象存在自定義的map裏。如參數對象 ParameterMap,映射結果對象:ResultMap  sql 標籤解析對象:XNode key生成策略:protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");。
  • 通過源碼的分析跟蹤: 這裏我們可以學習到  mybatis 的設計概念,比如短key的添加,不同標籤的解析到不同對象。
  • 使用自定義map實現一些自己框架的邏輯。設計模式的靈活運用,比如建造者模式構建複雜對象。比如模板方法模式:抽出公共處理邏輯方法,定義整個處理流程。不同的處理由子類來實現。
  • 比如內部類的使用,內部類的使用一般都是在主類下或者主類的同一個包下面,或者及其子類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章