美文网首页
【MyBatis】mapper.xml解析及annotation

【MyBatis】mapper.xml解析及annotation

作者: 半个橙子 | 来源:发表于2018-10-25 11:42 被阅读0次

    项目搭建

    参考之前的一篇文章【MyBatis】基本使用

    XML解析过程分析

    解析入口

    • SqlSessionFactoryBean
      SqlSessionFactoryBean.png
      由于SqlSessionFactoryBean实现了InitializingBean接口,所以Spring加载完SqlSessionFactoryBean实例后会调用afterPropertiesSet方法
      org.mybatis.spring.SqlSessionFactoryBean#afterPropertiesSet
      @Override
      public void afterPropertiesSet() throws Exception {
        notNull(dataSource, "Property 'dataSource' is required");
        notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
                  "Property 'configuration' and 'configLocation' can not specified with together");
    
        this.sqlSessionFactory = buildSqlSessionFactory();
      }
    

    在xml中配置给SqlSessionFactoryBean初始化时会自动将配置拷贝到Configuration中,Configuration其实就是MyBatis的一个配置中心。

      protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    
        Configuration configuration;
    
        XMLConfigBuilder xmlConfigBuilder = null;
        if (this.configuration != null) {
          configuration = this.configuration;
          if (configuration.getVariables() == null) {
            configuration.setVariables(this.configurationProperties);
          } else if (this.configurationProperties != null) {
            configuration.getVariables().putAll(this.configurationProperties);
          }
        } else if (this.configLocation != null) {
          //解析全局配置文件  
          xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
          configuration = xmlConfigBuilder.getConfiguration();
        } else {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
          }
          configuration = new Configuration();
          if (this.configurationProperties != null) {
            configuration.setVariables(this.configurationProperties);
          }
        }
    
       ......
    
        configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    
        if (!isEmpty(this.mapperLocations)) {
          for (Resource mapperLocation : this.mapperLocations) {
            if (mapperLocation == null) {
              continue;
            }
    
            try {
              XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                  configuration, mapperLocation.toString(), configuration.getSqlFragments());
              xmlMapperBuilder.parse();
            } catch (Exception e) {
              throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
              ErrorContext.instance().reset();
            }
    
            if (LOGGER.isDebugEnabled()) {
              LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
            }
          }
        } else {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
          }
        }
    
        return this.sqlSessionFactoryBuilder.build(configuration);
      }
    

    下面详细解释一下其中的几个类

    • Configuration
      MyBatis配置的一个容器,注册了一些默认的属性,以及后面生成的ResultMap、MappedStatement等都会加入这个容器中
    package org.apache.ibatis.session;
    public class Configuration {
    
     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");
      protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
      protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
      protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
      protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
    ......
    
    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    
        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    
        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        typeAliasRegistry.registerAlias("LRU", LruCache.class);
        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    
        typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    
      ......
        typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
        typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
    
        languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
        languageRegistry.register(RawLanguageDriver.class);
      }
    }
    
    • ConfigLocation

    是指全局配置文件的路径,因为这些配置都可以直接在SqlSessionFactoryBean 中配置所以现在一般都不再直接新增一个Mybatis的配置文件

     Configuration configuration;
    
        XMLConfigBuilder xmlConfigBuilder = null;
        if (this.configuration != null) {
          configuration = this.configuration;
          if (configuration.getVariables() == null) {
            configuration.setVariables(this.configurationProperties);
          } else if (this.configurationProperties != null) {
            configuration.getVariables().putAll(this.configurationProperties);
          }
        } else if (this.configLocation != null) {
          xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
          configuration = xmlConfigBuilder.getConfiguration();
        } else {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
          }
          configuration = new Configuration();
          if (this.configurationProperties != null) {
            configuration.setVariables(this.configurationProperties);
          }
        }
    
    • Environment
     if (this.transactionFactory == null) {
          this.transactionFactory = new SpringManagedTransactionFactory();
        }
    
        configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    
    • mapperLocations
      mapper文件的路径

    开始解析mapper.xml

    SqlSessionFactoryBean#buildSqlSessionFactory中遍历mapperLocations,并一个一个的解析mapper文件。

    #org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
        if (!isEmpty(this.mapperLocations)) {
          for (Resource mapperLocation : this.mapperLocations) {
            if (mapperLocation == null) {
              continue;
            }
    
            try {
              XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                  configuration, mapperLocation.toString(), configuration.getSqlFragments());
              xmlMapperBuilder.parse();
            } catch (Exception e) {
              throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
              ErrorContext.instance().reset();
            }
    
            if (LOGGER.isDebugEnabled()) {
              LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
            }
          }
        } else {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
          }
        }
    #org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
      public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          //解析mapper.xml
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          //annotation支持
          bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    

    Mybatis使用SAX解析xml文件,parser.evalNode("/mapper")获取mapper节点的内容。

    <mapper namespace="com.excelib.dao.UserMapper">
    <insert keyProperty="id" parameterType="User" useGeneratedKeys="true" id="insertUser">
    <selectKey order="BEFORE" keyProperty="id" resultType="int">
                select if(max(id) is null ,1,max(id)+2) as newId from user
            </selectKey>
    </insert>
    <select resultType="User" parameterType="integer" id="getUser">
            select * from user where id = #{id}
        </select>
    <select parameterType="integer" id="deleteUser">
            delete from user where id = #{id}
        </select>
    </mapper>
    

    依次解析如下的每一个一级节点,XMLMapperBuilder#configurationElement将Xml配置转换为Java对象。

    cache-ref 
    cache
    /mapper/parameterMap
    /mapper/resultMap
    /mapper/sql
    select|insert|update|delete
    
    #org.apache.ibatis.builder.xml.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"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          //resultMap的解析
          resultMapElements(context.evalNodes("/mapper/resultMap"));
         //sql标签的解析
          sqlElement(context.evalNodes("/mapper/sql"));
         //select 等标签的解析      
    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);
        }
      }
    

    ResultMap节点解析

    重点关注一下resultMap节点的解析,Mybatis的对ResultMap的解析步骤如下

    1. 遍历XML中所有的ResultMap节点
    2. 读取ResultMap节点的属性
    3. 遍历ResultMap的子节点,将数据库的每一列的映射关系解析成一个ResultMapping对象,添加到集合resultMappings中
    4. 将resultMappings封装到ResultMap中,构造ResultMap对象
    5. 以ResultMap.getId()为key,将ResultMap添加到Configuration.resultMaps中
    • 读取<resultMap/>节点数据
      由于一个mapper中可以含有多个resultMap 所以resultMapElements(context.evalNodes("/mapper/resultMap"));context.evalNodes("/mapper/resultMap")的解析结果其实是一个集合,如下:
    [<resultMap type="com.excelib.User" id="userMap">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="sex" property="sex"/>
    </resultMap>]
    
    • 生成ResultMapping
      开始具体的解析过程,读取resultMap 的子节点生成resultMapping对象,将所有的ResultMapping存放到一个List集合resultMappings中。
    #org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElements
    //遍历所有的resultMap节点
    private void resultMapElements(List<XNode> list) throws Exception {
       for (XNode resultMapNode : list) {
         try {
           resultMapElement(resultMapNode);
         } catch (IncompleteElementException e) {
           // ignore, it will be retried
         }
       }
     }
     private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
       return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
     }
    
     private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
       ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
       //读取resultMap节点的属性
       String id = resultMapNode.getStringAttribute("id",
           resultMapNode.getValueBasedIdentifier());
       String type = resultMapNode.getStringAttribute("type",
           resultMapNode.getStringAttribute("ofType",
               resultMapNode.getStringAttribute("resultType",
                   resultMapNode.getStringAttribute("javaType"))));
       String extend = resultMapNode.getStringAttribute("extends");
       Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
       Class<?> typeClass = resolveClass(type);
       Discriminator discriminator = null;
       List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
       resultMappings.addAll(additionalResultMappings);
       List<XNode> resultChildren = resultMapNode.getChildren();
       // 遍历resultMap的子节点,数据库的每一列的映射关系解析成resultMapping对象
       for (XNode resultChild : resultChildren) {
         if ("constructor".equals(resultChild.getName())) {
           processConstructorElement(resultChild, typeClass, resultMappings);
         } else if ("discriminator".equals(resultChild.getName())) {
           discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
         } else {
           List<ResultFlag> flags = new ArrayList<ResultFlag>();
           if ("id".equals(resultChild.getName())) {
             flags.add(ResultFlag.ID);
           }
           //解析resultMapping
           resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
         }
       }
       //将resultMappings封装到ResultMap中
       ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
       try {
         return resultMapResolver.resolve();
       } catch (IncompleteElementException  e) {
         configuration.addIncompleteResultMap(resultMapResolver);
         throw e;
       }
     }
    

    XMLMapperBuilder#buildResultMappingFromContext将每一个<result column="age" property="age"/>解析成一个ResultMapping

      private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
        String property;
        if (flags.contains(ResultFlag.CONSTRUCTOR)) {
          property = context.getStringAttribute("name");
        } else {
          property = context.getStringAttribute("property");
        }
        String column = context.getStringAttribute("column");
        String javaType = context.getStringAttribute("javaType");
        String jdbcType = context.getStringAttribute("jdbcType");
        String nestedSelect = context.getStringAttribute("select");
        String nestedResultMap = context.getStringAttribute("resultMap",
            processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
        String notNullColumn = context.getStringAttribute("notNullColumn");
        String columnPrefix = context.getStringAttribute("columnPrefix");
        String typeHandler = context.getStringAttribute("typeHandler");
        String resultSet = context.getStringAttribute("resultSet");
        String foreignColumn = context.getStringAttribute("foreignColumn");
        boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
        Class<?> javaTypeClass = resolveClass(javaType);
        @SuppressWarnings("unchecked")
        Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
      }
    
    • 生成ResultMap并加入Configuration
      实际上是加入了Configuration.resultMaps这个map中,key默认是xml中的id+当前的namespace
    #org.apache.ibatis.builder.ResultMapResolver#resolve
      public ResultMap resolve() {
        return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
      }
    #org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap
    public ResultMap addResultMap(
          String id,
          Class<?> type,
          String extend,
          Discriminator discriminator,
          List<ResultMapping> resultMappings,
          Boolean autoMapping) {
        ......
      //获取该resultMap 的id,默认是xml中的id+namespace
        id = applyCurrentNamespace(id, false);
      //创建ResultMap对象
        ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
            .discriminator(discriminator)
            .build();
         //将resultMap添加到configuration中
        configuration.addResultMap(resultMap);
        return resultMap;
      }
    
    #org.apache.ibatis.session.Configuration#addResultMap
     protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
     public void addResultMap(ResultMap rm) {
        resultMaps.put(rm.getId(), rm);
        checkLocallyForDiscriminatedNestedResultMaps(rm);
        checkGloballyForDiscriminatedNestedResultMaps(rm);
      }
    

    select|insert|update|delete 标签的解析

    1. 解析select|insert|update|delete等xml标签生成MixedSqlNode;
    2. 封装SqlNode到sqlSource中。SqlSource实际上只是对SqlNode的封装并没有实际生成Sql语句,因为MyBatis的动态Sql是根据查询条件动态拼接生成最终数据库执行的Sql;
    3. 根据解析到的sqlSource,resultMap等配置创建MappedStatement并加入Configuration中,实际上加入了Configuration.mappedStatements这个map中。每一个select|insert|update|delete节点都对应一个MappedStatement。

    使用buildStatementFromContext(context.evalNodes("select|insert|update|delete"));来遍历并解析解析mapper中的每一个select|insert|update|delete节点

    #org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>)`
    private void buildStatementFromContext(List<XNode> list) {
        if (configuration.getDatabaseId() != null) {
          buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
      }
      private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
          }
        }
      }
    
     public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        //匹配databaseId,如果不匹配则不解析该sql
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        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);
        //获取resultType对应的Class
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultSetType = context.getStringAttribute("resultSetType");
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        //节点名称select|insert|update|delete
        String nodeName = context.getNode().getNodeName();
       //sqlCommandType 是指sql类型,如insert,update等
        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());
    
        // Parse selectKey after includes and remove them.
        //解析selectkey标签
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
        
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        //提取sqlSource
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        String resultSets = context.getStringAttribute("resultSets");
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        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;
        }
        //生成MappedStatement并加入configuration中
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    
    • 解析Sql类型
      上面的代码中已经解析出了当前sql的类型select|insert|update|delete
    #org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    
    public enum SqlCommandType {
      UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
    }
    
    • 解析SqlNode生成SqlSource
      SqlSource实际上只是对SqlNode的封装,并没有实际生成Sql语句,因为MyBatis的动态Sql是根据查询条件动态拼接生成最终数据库执行的Sql;
    #org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //XMLLanguageDriver#createSqlSource(Configuration, XNode, java.lang.Class<?>)
      @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
      }
    
    #org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
    public SqlSource parseScriptNode() {
        //解析xml节点封装成MixedSqlNode 
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        //生成sqlSource
        SqlSource sqlSource = null;
        if (isDynamic) {
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
      }
    
    • 解析SqlNode
      将sql中的每一个节点都解析为一个特定的SqlNode,并判断子节点是否是普通的文本节点,如果是动态节点(包含${})则封装成TextSqlNode如果不是封装成StaticTextSqlNode
      如果不是普通文本节点则,则递归解析动态子节点
    #org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
      protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<SqlNode>();
        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 {
              //如果不包含${}则是静态文本节点
              contents.add(new StaticTextSqlNode(data));
            }
          } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
            String nodeName = child.getNode().getNodeName();
            //寻找NodeHandler 
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
              throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            //处理子节点
            handler.handleNode(child, contents);
            isDynamic = true;
          }
        }
        return new MixedSqlNode(contents);
      }
    

    上面的解析过程有一点复杂,我们用下面的sql来看一下它的解析过程

    <update id="updateUser" parameterType="com.excelib.User">
            update user
            <set>
                <if test="name!=null and name !=''">
                    name = #{name},
                </if>
                <if test="age!=null and age !=''">
                    age = #{age}
                </if>
            </set>
            where id = #{id}
        </update>
    

    parseDynamicTags方法中NodeList children = node.getNode().getChildNodes();其实拿到的是上面sql的3个子节点的内容

    update user
    <set><if test="name!=null and name !=''"> name = #{name},</if><if test="age!=null and age !=''">age = #{age}</if></set>
    where id = #{id}

    依次遍历三个子节点,根据判定条件封装成不同的sqlNode对象


    SqlNode.png

    sqlNode解析步骤

    1. update user
      判断child.getNode().getNodeType() == Node.TEXT_NODE成立,即为文本节点;然后调用textSqlNode.isDynamic()去判断该节点是否是动态的,这条语句中不包含${}所以不是动态的(GenericTokenParser("${", "}", handler)),最终会调用contents.add(new StaticTextSqlNode(data));

    2. <set>
      child.getNode().getNodeType() == Node.ELEMENT_NODE这个判断是成立,即是它动态节点;然后根据nodeName从nodeHandlerMap查找nodeHandler并调用 handler.handleNode(child, contents);进行动态节点解析。

    NodeHandler.png
    //org.apache.ibatis.scripting.xmltags.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());
      }
    

    这里的nodeName就是set,所以会查找到SetHandler来解析,看一下它的解析方法

    //org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.SetHandler#handleNode
    @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
          MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
          SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
          targetContents.add(set);
        }
    

    这里又递归调用了parseDynamicTags()解析set节点的子节点,并封装成MixedSqlNode返回。可以预期在这次调用parseDynamicTags()中会有2个if子节点,然后每个if节点又会调用IfHandler.handleNode解析子元素,if的子节点只有文本节点所以解析终止返回一个文本节点,最终一层一层返回。

    //org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.IfHandler#handleNode
     @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
          MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
           //读取if节点的test属性
          String test = nodeToHandle.getStringAttribute("test");
          IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
          targetContents.add(ifSqlNode);
        }
    
    1. where
      也判断child.getNode().getNodeType() == Node.TEXT_NODE成立,即为文本节点,会调用contents.add(new StaticTextSqlNode(data));最终会都会加入contents并返回new MixedSqlNode(contents);

    所以最终解析结果,如下:

    sql语句 节点类型
    update user StaticTextSqlNode
    <set> SetSqlNode
    <if test="name!=null and name !=''"> IfSqlNode
    name = #{name}, StaticTextSqlNode
    </if>
    <if test="age!=null and age !=''"> IfSqlNode
    age = #{age} StaticTextSqlNode
    </if>
    </set>
    where id = #{id} StaticTextSqlNode

    调试结果也正是这样


    image.png

    接下来通过new DynamicSqlSource(configuration, rootSqlNode);封装成SqlSource。
    最后将insert|update等解析结果封装成MappedStatement并加入Configuration中builderAssistant.addMappedStatement(...);

    //MapperBuilderAssistant#addMappedStatement(.....)
    public MappedStatement addMappedStatement(...) {
    ......
    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)
            //获取resultMap
            .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);
        return statement;
      }
    

    注解支持

    前面分析了configurationElement(parser.evalNode("/mapper"));解析mapper.xml。MyBatis其实也提供注解支持。

    MyBatis 常用注解

    1. @SelectProvider
      动态调用某类的某个方法,并使用其返回值作为sql语句
    2. @Select
      使用注解value值作为sql语句
    3. @Select({"<script>",sql,"</script>"})
      动态解析value值生成动态Sql
    4. @Options
      该MappedStatement的配置参数,如:cache timeout等
      具体使用可以参考【MyBatis】基本使用

    注解使用示例:

    public interface UserMapper {
        public void insertUser(User user);
        public User getUser(Integer id);
        public void updateUser(User user);
        public void deleteUser(Integer id);
    
        /**
         * 标识提供sql语句的类和方法,mybatis会调用该类的方法获取sql语句,生成MappedStatement注册
         */
        @SelectProvider(type = SqlProvider.class,method = "getQueryUserSql")
        public User queryUserWithSqlProvider(@Param("id") Integer id);
    
        /**
         * 使用注解配置sql语句
         */
        @Select("select * from user where id = #{id}")
        public User queryUserWithAnnotation(@Param("id") Integer id);
    
        /**
         * 添加<script> 可以则sql语句可以使用动态标签
         */
        @Select({"<script>",
                "        update user\n" +
                "        <set>\n" +
                "            <if test=\"name!=null and name !=''\">\n" +
                "                name = #{name},\n" +
                "            </if>\n" +
                "            <if test=\"age!=null and age !=''\">\n" +
                "                age = #{age}\n" +
                "            </if>\n" +
                "        </set>\n" +
                "        where id = #{id}\n",
                "</script>"})
        public void updateUserWithAnnotation(@Param("id") Integer id,@Param("name")String name,@Param("age")Integer age);
    
    }
    

    回到一开始解析mapper.xml的入口的地方 XMLMapperBuilder#parse ,其中的 bindMapperForNamespace();就是提供注解支持的入口。首先获取mapper.xml的Namespace,根据nameSpace的值加载Class对象(其实是一个接口)。我们要处理的对象就是该接口里所有方法的注解。

    #org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
      public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          //解析mapper.xml
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          //annotation支持
          bindMapperForNamespace();
        }
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    
    private void bindMapperForNamespace() {
        //获取当前的Namespace,根据nameSpace的值生成Class,其实是Mapper接口的Class
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
          Class<?> boundType = null;
          try {
            boundType = Resources.classForName(namespace);
          } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
          }
          if (boundType != null) {
            //判断是否存储了mapper工厂
            if (!configuration.hasMapper(boundType)) {
              // Spring may not know the real resource name so we set a flag
              // to prevent loading again this resource from the mapper interface
              // look at MapperAnnotationBuilder#loadXmlResource
              configuration.addLoadedResource("namespace:" + namespace);
              //将class加入Configuration,里面会有对mapper接口的解析
              configuration.addMapper(boundType);
            }
          }
        }
      }
    

    configuration.addMapper(boundType)中提供了对注解mapper的解析,使用MapperAnnotationBuilder.parse()来对该接口的方法上的annotation进行解析,而且使用knownMappers来保存mapper代理工厂,key就是这个mapper接口类型(nameSpace)。这个MapperProxyFactory会用来产生动态代理bean(注入到Spring),当我们调用bean的方法的时候就会回调代理的invoke方法,在该方法中查找mappedStatement来组装sql并调用。

    //org.apache.ibatis.session.Configuration#addMapper
     public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
      }
    //org.apache.ibatis.binding.MapperRegistry#addMapper
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          boolean loadCompleted = false;
          try {
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    

    具体的解析过程

    1. 遍历接口的每一个方法
    2. 首先根据方法注解创建出一个SqlSource。因为涉及到动态sql的解析,所以这一步也比较复杂;
    3. 然后获取可以识别的注解,提取注解配置的属性到对应的Java对象中;
    4. 最终生成一个MappedStatement并注册到Configuration中;
    public class MapperAnnotationBuilder {
    //org.apache.ibatis.builder.annotation.MapperAnnotationBuilder
     public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
    
        sqlAnnotationTypes.add(Select.class);
        sqlAnnotationTypes.add(Insert.class);
        sqlAnnotationTypes.add(Update.class);
        sqlAnnotationTypes.add(Delete.class);
    
        sqlProviderAnnotationTypes.add(SelectProvider.class);
        sqlProviderAnnotationTypes.add(InsertProvider.class);
        sqlProviderAnnotationTypes.add(UpdateProvider.class);
        sqlProviderAnnotationTypes.add(DeleteProvider.class);
      }
    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
          loadXmlResource();
          configuration.addLoadedResource(resource);
          assistant.setCurrentNamespace(type.getName());
          parseCache();
          parseCacheRef();
          Method[] methods = type.getMethods();
          for (Method method : methods) {
            try {
              // issue #237
              if (!method.isBridge()) {
                parseStatement(method);
              }
            } catch (IncompleteElementException e) {
              configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
          }
        }
        parsePendingMethods();
      }
    
      void parseStatement(Method method) {
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method);
        //根据注解创建SqlSource
        SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
        if (sqlSource != null) {
          //解析@Options 
          Options options = method.getAnnotation(Options.class);
          ......
          if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
            // first check for SelectKey annotation - that overrides everything else
            //解析@SelectKey
            SelectKey selectKey = method.getAnnotation(SelectKey.class);
            ......
          } else {
            keyGenerator = NoKeyGenerator.INSTANCE;
          }
    
          if (options != null) {
            ......
          }
    
          String resultMapId = null;
          //解析@ResultMap
          ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
          if (resultMapAnnotation != null) {
         ......
          } else if (isSelect) {
            resultMapId = parseResultMap(method);
          }
          assistant.addMappedStatement(......);
         
        }
      }
    }
    

    从注解中生成SqlSource的过程,分为两种情况:
    一种是 @Select @Insert @Update @Delete注解
    一种是使用SelectSqlProvider、InsertSqlProvider、UpdateSqlProvider 、DeleteSqlProvider提供Sql语句 。

    //从Annotation中提取Sql语句并交给XMLLanguageDriver解析
    private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
          Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
          Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
          if (sqlAnnotationType != null) {
           //如果是 Select.class Insert.class Update.class Delete.class 则直接将value拿去解析
            if (sqlProviderAnnotationType != null) {
              throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
            }
            Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
            final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
            return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
          } else if (sqlProviderAnnotationType != null) {
             //如果是 SelectSqlProvider.class InsertSqlProvider.class UpdateSqlProvider.class DeleteSqlProvider.class 等类型则封装成ProviderSqlSource
            Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
            return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
          }
          return null;
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
      }
      private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
        final StringBuilder sql = new StringBuilder();
        for (String fragment : strings) {
          sql.append(fragment);
          sql.append(" ");
        }
        return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
      }
    
    • 如果是@Select @Insert @Update @Delete则直接将value拿去XMLLanguageDriver 解析,会判断如果sql是以<script>开头则动态解析sql的子节点,如果不是则直接封装成SqlSource。关于动态解析子节点的直接参考前面mapper.xml 中 insert|update等标签的解析过程(递归生成SqlNode)。
    /**
     * @author Eduardo Macarron
     */
    public class XMLLanguageDriver implements LanguageDriver {
    
      @Override
      public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
      }
    
      @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
      }
    
      @Override
      public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        // issue #3
        if (script.startsWith("<script>")) {
          XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
          return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
        } else {
          // issue #127
          script = PropertyParser.parse(script, configuration.getVariables());
          TextSqlNode textSqlNode = new TextSqlNode(script);
          if (textSqlNode.isDynamic()) {
            return new DynamicSqlSource(configuration, textSqlNode);
          } else {
            return new RawSqlSource(configuration, script, parameterType);
          }
        }
      }
    }
    
    • 如果是SelectSqlProvider InsertSqlProvider UpdateSqlProvider DeleteSqlProvider等类型则封装成ProviderSqlSource。在Excutor执行数据库操作的时候会调用SqlSource.getBoundSql(),这时会首先会从parameterObject抽取出调用参数封装成Map,然后使用该参数反射调用SqlProvider中指定的方法生成sql。
    public class ProviderSqlSource implements SqlSource {
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        SqlSource sqlSource = createSqlSource(parameterObject);
        return sqlSource.getBoundSql(parameterObject);
      }
    
      private SqlSource createSqlSource(Object parameterObject) {
        try {
          int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
          String sql;
          if (providerMethodParameterTypes.length == 0) {
            sql = invokeProviderMethod();
          } else if (bindParameterCount == 0) {
            sql = invokeProviderMethod(providerContext);
          } else if (bindParameterCount == 1 &&
                  (parameterObject == null || providerMethodParameterTypes[(providerContextIndex == null || providerContextIndex == 1) ? 0 : 1].isAssignableFrom(parameterObject.getClass()))) {
            sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
          } else if (parameterObject instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> params = (Map<String, Object>) parameterObject;
            sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
          } else {
            ......
          }
          Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
          return sqlSourceParser.parse(replacePlaceholder(sql), parameterType, new HashMap<String, Object>());
        } catch (BuilderException e) {
          throw e;
        } catch (Exception e) {
        ......
        }
      }
    
      private Object[] extractProviderMethodArguments(Object parameterObject) {
        if (providerContext != null) {
          Object[] args = new Object[2];
          args[providerContextIndex == 0 ? 1 : 0] = parameterObject;
          args[providerContextIndex] = providerContext;
          return args;
        } else {
          return new Object[] { parameterObject };
        }
      }
    
      private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
        Object[] args = new Object[argumentNames.length];
        for (int i = 0; i < args.length; i++) {
          if (providerContextIndex != null && providerContextIndex == i) {
            args[i] = providerContext;
          } else {
            args[i] = params.get(argumentNames[i]);
          }
        }
        return args;
      }
      private String invokeProviderMethod(Object... args) throws Exception {
        Object targetObject = null;
        if (!Modifier.isStatic(providerMethod.getModifiers())) {
          targetObject = providerType.newInstance();
        }
        CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args);
        return sql != null ? sql.toString() : null;
      }
    }
    

    annotation解析工作都完成后,生成一个MappedStatement并注册到Configuration中。其实无论是解析注解还是解析mapper.xml最终都是一个insert|update|delete|select方法或者节点生成一个SqlSource来构造一个MappedStatement并注册到Configuration中。

    相关文章

      网友评论

          本文标题:【MyBatis】mapper.xml解析及annotation

          本文链接:https://www.haomeiwen.com/subject/msqqtqtx.html