• mybatis源码学习(三):MappedStatement的解析过程


    我们之前介绍过MappedStatement表示的是XML中的一个SQL。类当中的很多字段都是SQL中对应的属性。我们先来了解一下这个类的属性:

    public final class MappedStatement {
      
      private String resource;
      private Configuration configuration;
      //sql的ID
      private String id;
      //尝试影响驱动程序每次批量返回的结果行数和这个设置值相等
      private Integer fetchSize;
      //SQL超时时间
      private Integer timeout;
      //Statement的类型,STATEMENT/PREPARE/CALLABLE
      private StatementType statementType;
      //结果集类型,FORWARD_ONLY/SCROLL_SENSITIVE/SCROLL_INSENSITIVE 
      private ResultSetType resultSetType;
      //表示解析出来的SQL
      private SqlSource sqlSource;
      //缓存
      private Cache cache;
      //已废弃
      private ParameterMap parameterMap;
      //对应的ResultMap
      private List<ResultMap> resultMaps;
      private boolean flushCacheRequired;
      private boolean useCache;
      private boolean resultOrdered;
      //SQL类型,INSERT/SELECT/DELETE
      private SqlCommandType sqlCommandType;
      //和SELECTKEY标签有关
      private KeyGenerator keyGenerator;
      private String[] keyProperties;
      private String[] keyColumns;
      private boolean hasNestedResultMaps;
      //数据库ID,用来区分不同环境
      private String databaseId;
      private Log statementLog;
      private LanguageDriver lang;
      //多结果集时
      private String[] resultSets;
    
      MappedStatement() {
        // constructor disabled
      }
    
      ...
      }

    对一些重要的字段我都增加了备注,方便理解。其中真正表示SQL的字段是SqlSource这个对象。

    SqlSource接口很简单,只有一个getBound方法:

    public interface SqlSource {
    
      BoundSql getBoundSql(Object parameterObject);
    
    }

    它有很多实现,需要我们重点关注的是StaticSqlSource,RawSqlSource和DynamicSqlSource。在正式学习他们前,我们先了解一下Mybatis动态SQL和静态SQL的区别。

    动态SQL表示这个SQL节点中含有${}或是其他动态的标签(比如,if,trim,foreach,choose,bind节点等),需要在运行时根据传入的条件才能确定SQL,因此对于动态SQL的MappedStatement的解析过程应该是在运行时。

    而静态SQL是不含以上这个节点的SQL,能直接解析得到含有占位符形式的SQL语句,而不需要根据传入的条件确定SQL,因此可以在加载时就完成解析。所在在执行效率上要高于动态SQL。

    而DynamicSqlSource和RawSqlSource就分别对应了动态SQL和静态SQL,它们都封装了StaticSqlSource。

    我们先从简单的入手,了解静态SQL的解析过程。

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">
    
    
        <select id="selectAllAuthors" resultType="org.apache.ibatis.domain.blog.Author">
            select * from author
        </select>
    
    
    </mapper>

    这是我们要解析的XML文件,mapper节点下只有一个select节点。

    public class XmlMapperBuilderTest {
    
       @Test
      public void shouldSuccessfullyLoadXMLMapperFile() throws Exception {
        Configuration configuration = new Configuration();
        String resource = "org/apache/ibatis/builder/AuthorMapper.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
        builder.parse();
        inputStream.close();
      }
    }

    这是我们测试解析过程的代码。我们可以看到解析是由XMLMapperBuilder开始的。我们先了解一下它的字段:

    public class XMLMapperBuilder extends BaseBuilder {
        //用来解析XML
      private final XPathParser parser;
        //再解析完成后,用解析所得的属性来帮助创建各个对象
      private final MapperBuilderAssistant builderAssistant;
      //保存SQL节点
      private final Map<String, XNode> sqlFragments;
      //...  
    }

    它还从父类中继承了configuration(配置对象),typeAliasRegistry(类型别名注册器)和typeHandlerRegistry(类型处理器注册器)。

    接下来看一下它的parse方法:

    public void parse() {
       //判断是否已经加载过资源    
        if (!configuration.isResourceLoaded(resource)) {
        //从mapper根节点开始解析
          configurationElement(parser.evalNode("/mapper"));
        //将该资源添加到为已经加载过的缓存中
          configuration.addLoadedResource(resource);
        //将解析的SQL和接口中的方法绑定
          bindMapperForNamespace();
        }
        
        //对一些未完成解析的节点再解析
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
        

    主要的解析过程在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.setCurrentNamespace(namespace);
          //解析<cache-ref>节点,它有一个namespace属性,表示引用该命名空间下的缓存
          cacheRefElement(context.evalNode("cache-ref"));
          //解析<cache>节点,可以设置缓存类型和属性,或是指定自定义的缓存
          cacheElement(context.evalNode("cache"));
          //已废弃,不再使用    
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          //解析resultMap节点
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          //解析<SQL>节点,SQL节点可以使一些SQL片段被复用     
          sqlElement(context.evalNodes("/mapper/sql"));
          //解析SQL语句(select|insert|update|delete节点) 
          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语句的解析过程,上述buildStatementFromContext(List<XNode>)方法会增加dateBaseId的参数,然后调用另一个重载方法:

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        //遍历XNode节点
        for (XNode context : list) {
          //为每个节点创建XMLStatementBuilder对象,
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
          //解析Node
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            //对不能完全解析的节点添加到incompleteStatement,在parsePendingStatements方法中再解析
            configuration.addIncompleteStatement(statementParser);
          }
        }
      }

    先看看XMLStatementBuilder对象:

    public class XMLStatementBuilder extends BaseBuilder {
    
      private final MapperBuilderAssistant builderAssistant;
      private final XNode context;
      private final String requiredDatabaseId;
      // ...
    }
    View Code

    含有的字段相对简单,不再具体解释。直接看parseStatementNode方法:

    public void parseStatementNode() {
        //获取id
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        //验证databaseId是否匹配
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        //已废弃
        String parameterMap = context.getStringAttribute("parameterMap");
        //参数类型;将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
        //结果类型;外部 resultMap 的命名引用。
        String resultMap = context.getStringAttribute("resultMap");
        //结果类型;表示从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。不能和resultMap同时使用。
        String resultType = context.getStringAttribute("resultType");
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
    
        Class<?> resultTypeClass = resolveClass(resultType);
        //结果集类型;FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
        String resultSetType = context.getStringAttribute("resultSetType");
        //STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    
        String nodeName = context.getNode().getNodeName();
        //SQLCommand类型
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        //flushCache;在执行语句时表示是否刷新缓存
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        //是否对该语句进行二级缓存;默认值:对 select 元素为 true。
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        //根嵌套结果相关
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        //引入SQL片段
        // 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 = 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;
        }
    
        //通过buildAssistant将解析得到的参数设置构造成MappedStatement对象
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }

    将解析得到参数通过BuilderAssistant.addMappedStatement方法,解析得到MappedStatement对象。

    上面已经说过sqlsource表示的一个SQL语句,因此我们关注langDriver.createSqlSource这个方法。看XMLLanguageDriver这个实现。

     @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
      }

    可以看到他将创建sqlsource的工作交给了XMLScrpitBuilder(又一个创建者模式的应用)。来看parseScriptNode方法:

    public SqlSource parseScriptNode() {
        //解析SQL语句节点,创建MixedSqlNode对象
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource = null;
        //根据是否是动态的语句,创建DynamicSqlSource或是RawSqlSource对象,并返回
        if (isDynamic) {
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
     }

    MixedSqlNode是SqlNode的一个实现,包含了各个子节点,用来遍历输出子节点。SqlNode还有很多不同的实现,分别对应不同的节点类型。对应关系如下:

    SqlNode实现 对应SQL语句中的类型
    TextSqlNode ${}
    IfSqlNode If节点
    TrimSqlNode/WhereSqlNode/SetSqlNode Trim/Where/Set节点
    Foreach节点 foreach标签
    ChooseSqlNode节点 choose/when/otherwhise节点
    ValDeclSqlNode节点 bind节点
    StaticTextSqlNode 不含上述节点

    除了StaticTextSqlNode节点外,其余对应的都是动态语句。

    因此我们本文的关注点在StaticTextSqlNode。

    让我们对应文初sql语句的解析来看一下parseDynamicTags方法,为了便于理解,我将在右边注释出每一步的结果

    protected MixedSqlNode parseDynamicTags(XNode node) {// node是我们要解析的SQL语句: <select resultType="org.apache.ibatis.domain.blog.Author" id="selectAllAuthors">select * from author</select>
        List<SqlNode> contents = new ArrayList<SqlNode>();
        //获取SQL下面的子节点
        NodeList children = node.getNode().getChildNodes();//这里的children只有一个节点;
        //遍历子节点,解析成对应的sqlNode类型,并添加到contents中
        for (int i = 0; i < children.getLength(); i++) {
          XNode child = node.newXNode(children.item(i));//第一个child节点就是SQL中的文本数据:select * from author
          //如果是文本节点,则先解析成TextSqlNode对象
          if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            //获取文本信息
            String data = child.getStringBody("");//data:select * from author
            //创建TextSqlNode对象
            TextSqlNode textSqlNode = new TextSqlNode(data);
            //判断是否是动态Sql,其过程会调用GenericTokenParser判断文本中是否含有"${"字符
            if (textSqlNode.isDynamic()) {//如果是动态SQL,则直接使用TextSqlNode类型,并将isDynamic标识置为true
              contents.add(textSqlNode);
              isDynamic = true;
            } else {//不是动态sql,则创建StaticTextSqlNode对象,表示静态SQL
              contents.add(new StaticTextSqlNode(data));
            }
          } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他类型的节点,由不同的节点处理器来对应处理成本成不同的SqlNode类型
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
              throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            isDynamic = true;
          }
        }
        //用contents构建MixedSqlNode对象
        return new MixedSqlNode(contents);
      }

    上述过程中,我们主要关注静态SQL的解析过程,对于动态SQL的解析将在之后介绍。

    得到MixedSqlNode后,静态的SQL会创建出RawSqlSource对象。

    看一下RawSqlSource:

      public class RawSqlSource implements SqlSource {
      //内部封装的sqlSource对象,getBoundSql方法会委托给这个对象
      private final SqlSource sqlSource;
    
      public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
        this(configuration, getSql(configuration, rootSqlNode), parameterType);
      }
    
      public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
          //创建sqlSourceBuilder
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
           //解析sql,创建StaticSqlSource对象
        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
      }
    
      //获取sql语句
      private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
        DynamicContext context = new DynamicContext(configuration, null);
        //这里的rootSqlNode就是之前得到的MixedSqlNode,它会遍历内部的SqlNode,逐个调用sqlNode的apply方法。StaticTextSqlNode会直接context.appendSql方法
        rootSqlNode.apply(context);
        return context.getSql();
      }
    
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        return sqlSource.getBoundSql(parameterObject);
      }
    
    }

    代码相对简单,主要的步骤就是(1)通过SqlNode获得原始SQL语句;(2)创建SqlSourceBuilder对象,解析SQL语句,并创建StaticSqlSource对象;(3)将getBoundSql方法委托给内部的staticSqlSource对象。

    其中比较关键的一步是解析原始SQL语句,并创建StaticSqlSource对象。因此我们继续看SqlSourceBuilder对象。

    public class SqlSourceBuilder extends BaseBuilder {
    
      private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
    
      public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
      }
    
      public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
          //创建TokenHandler,用来将原始Sql中的'#{}' 解析成'?'
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        //解析原始sql
        String sql = parser.parse(originalSql);
        //创建出StaticSqlSource对象
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
    
      private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
    
        private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
        private Class<?> parameterType;
        private MetaObject metaParameters;
    
        public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
          super(configuration);
          this.parameterType = parameterType;
          this.metaParameters = configuration.newMetaObject(additionalParameters);
        }
    
        public List<ParameterMapping> getParameterMappings() {
          return parameterMappings;
        }
    
        @Override
        public String handleToken(String content) {
          //解析'#{}'中的参数,创建ParameterMapping对象
          parameterMappings.add(buildParameterMapping(content));
          //将'#{}'替换成'?'
          return "?";
        }
    
        private ParameterMapping buildParameterMapping(String content) {
          Map<String, String> propertiesMap = parseParameterMapping(content);
          String property = propertiesMap.get("property");
          Class<?> propertyType;
          if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
            propertyType = metaParameters.getGetterType(property);
          } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
            propertyType = parameterType;
          } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
            propertyType = java.sql.ResultSet.class;
          } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
            propertyType = Object.class;
          } else {
            MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
            if (metaClass.hasGetter(property)) {
              propertyType = metaClass.getGetterType(property);
            } else {
              propertyType = Object.class;
            }
          }
          ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
          Class<?> javaType = propertyType;
          String typeHandlerAlias = null;
          for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
            String name = entry.getKey();
            String value = entry.getValue();
            if ("javaType".equals(name)) {
              javaType = resolveClass(value);
              builder.javaType(javaType);
            } else if ("jdbcType".equals(name)) {
              builder.jdbcType(resolveJdbcType(value));
            } else if ("mode".equals(name)) {
              builder.mode(resolveParameterMode(value));
            } else if ("numericScale".equals(name)) {
              builder.numericScale(Integer.valueOf(value));
            } else if ("resultMap".equals(name)) {
              builder.resultMapId(value);
            } else if ("typeHandler".equals(name)) {
              typeHandlerAlias = value;
            } else if ("jdbcTypeName".equals(name)) {
              builder.jdbcTypeName(value);
            } else if ("property".equals(name)) {
              // Do Nothing
            } else if ("expression".equals(name)) {
              throw new BuilderException("Expression based parameters are not supported yet");
            } else {
              throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);
            }
          }
          if (typeHandlerAlias != null) {
            builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
          }
          return builder.build();
        }
    
        private Map<String, String> parseParameterMapping(String content) {
          try {
            return new ParameterExpression(content);
          } catch (BuilderException ex) {
            throw ex;
          } catch (Exception ex) {
            throw new BuilderException("Parsing error was found in mapping #{" + content + "}.  Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
          }
        }
      }
    
    }

    parse方法主要分为以下几步:

    (1)创建了ParameterMappingTokenHandler对象

    (2)将ParameterMappingTokenHandler对象传入GenericTokenParser的构造函数中,创建GenericTokenParser对象

    (3)通过GenericTokenParser对象解析原始SQL,这个过程中会将#{}替换成?,并将#{}中的参数,解析形成ParamterMapping对象

    (4)用得到的SQL和ParamterMapping对象创建StaticSqlSource对象。

    解析完成后回到一开始的XMLMapperBuilder,它会在资源添加到已加载的列表中,并bindMapperForNamespace方法中为创建的MappedStatement添加命名空间。

  • 相关阅读:
    PHP生成xml 无法识别或是无法读取或是浏览器不识别等问题
    关于PHP 采集类
    Centos7 下安装Docke
    Git使用之设置SSH Key
    yii2.0中Rbac 怎么添加超加管理员
    Undefined index: HTTP_RAW_POST_DATA的解决办法
    window下phpstudy的nginx配置虚拟主机
    yii2.0中添加二维数组,多条数据。
    预防onion比特币勒索病毒,如何快速关闭135,137,138,139,445端口
    github与git之间怎么建立连接
  • 原文地址:https://www.cnblogs.com/insaneXs/p/9083003.html
Copyright © 2020-2023  润新知