手把手带你阅读Mybatis源码(一)构造篇(下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 手把手带你阅读Mybatis源码(一)构造篇(下)

代码执行至此,基本就结束了对Configuration对象的构建,MyBatis的第一阶段:构造,也就到这里结束了,现在再来回答我们在文章开头提出的那两个问题:MyBatis需要构造什么对象?以及是否两个配置文件对应着两个对象?,似乎就已经有了答案,这里做一个总结:

MyBatis需要对配置文件进行解析,最终会解析成一个Configuration对象,但是要说两个配置文件对应了两个对象实际上也没有错:


  • Configuration对象,保存了mybatis-config.xml的配置信息。


  • MappedStatement,保存了XXXMapper.xml的配置信息。


但是最终MappedStatement对象会封装到Configuration对象中,合二为一,成为一个单独的对象,也就是Configuration。


最后给大家画一个构建过程的流程图:


image.png


填坑


SQL语句在哪解析?


细心的同学可能已经发现了,上文中只说了去节点中获取一些属性从而构建配置对象,但是最重要的SQL语句并没有提到,这是因为这部分我想要和属性区分开单独说,由于MyBatis支持动态SQL和${}#{}的多样的SQL,所以这里单独提出来说会比较合适。

首先可以确认的是,刚才我们走完的那一整个流程中,包含了SQL语句的生成,下面贴代码(这一段代码相当绕,不好读)。


//解析Sql(重要)  根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);


这里就是生成Sql的入口,以单步调试的角度接着往下看。


/*进入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    //进入这个构造
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    //进入parseScriptNode
    return builder.parseScriptNode();
}
/**
进入这个方法
*/
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;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //获取select标签下的子标签
    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) {
          //如果是查询
        //获取原生SQL语句 这里是 select * from test where id = #{id}
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //检查sql是否是${}
        if (textSqlNode.isDynamic()) {
            //如果是${}那么直接不解析
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
            //如果不是,则直接生成静态SQL
            //#{} -> ?
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          //如果是增删改
        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;
      }
    }
    return new MixedSqlNode(contents);
  }



/*从上面的代码段到这一段中间需要经过很多代码,就不一段一段贴了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //这里会生成一个GenericTokenParser,传入#{}作为开始和结束,然后调用其parse方法,即可将#{}换为 ?
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //这里可以解析#{} 将其替换为?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }
//经过一段复杂的解析过程
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    //遍历里面所有的#{} select ?  ,#{id1} ${}
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
            //使用占位符 ?
            //注意handler.handleToken()方法,这个方法是核心
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}
//BindingTokenParser 的handleToken
//当扫描到${}的时候调用此方法  其实就是不解析 在运行时候在替换成具体的值
@Override
public String handleToken(String content) {
  this.isDynamic = true;
  return null;
}
//ParameterMappingTokenHandler的handleToken
//全局扫描#{id} 字符串之后  会把里面所有 #{} 调用handleToken 替换为?
@Override
public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
}


这段代码相当绕,我们应该站在一个宏观的角度去看待它。所以我直接在这里概括一下:

首先这里会通过<select>节点获取到我们的SQL语句,假设SQL语句中只有${},那么直接就什么都不做,在运行的时候直接进行赋值。


而如果扫描到了#{}字符串之后,会进行替换,将#{}替换为 ?

那么他是怎么进行判断的呢?


这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{},那么openToken就是#{,closeToken就是 },然后通过parse方法中的handler.handleToken()方法进行替换。


在这之前由于已经进行过SQL是否含有#{}的判断了,所以在这里如果是只有${},那么handler就是BindingTokenParser的实例化对象,如果存在#{},那么handler就是ParameterMappingTokenHandler的实例化对象。


分别进行处理。


上文中提到的解析不了的节点是什么意思?


根据上文的代码我们可知,解析Mapper.xml文件中的每个节点是有顺序的。

那么假设我写了这么一个几个节点:


<select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'>
</select>
<resultMap id="demoResultMap" type="demo">
    <id column property>
    <result coulmn property>
</resultMap>


select节点是需要获取resultMap的,但是此时resultMap并没有被解析到,所以解析到<select>这个节点的时候是无法获取到resultMap的信息的。


我们来看看MyBatis是怎么做的:


private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      //解析xml节点
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      //xml语句有问题时 存储到集合中 等解析完能解析的再重新解析
      configuration.addIncompleteStatement(statementParser);
    }
  }
}


当解析到某个节点出现问题的时候,会抛一个异常,然后会调用configuration的addIncompleteStatement方法,将这个解析对象先暂存到这个集合中,等到所有的节点都解析完毕之后,在对这个集合内的解析对象继续解析:


public void parse() {
      //判断文件是否之前解析过
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //绑定Namespace里面的Class对象
      bindMapperForNamespace();
    }
    //重新解析之前解析不了的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}
private void parsePendingResultMaps() {
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
      Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
      while (iter.hasNext()) {
        try {
            //添加resultMap
          iter.next().resolve();
          iter.remove();
        } catch (IncompleteElementException e) {
          // ResultMap is still missing a resource...
        }
      }
    }
}
public ResultMap resolve() {
    //添加resultMap
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
  }


结语


至此整个MyBatis的查询前构建的过程就基本说完了,简单地总结就是,MyBatis会在执行查询之前,对配置文件进行解析成配置对象:Configuration,以便在后面执行的时候去使用,而存放SQL的xml又会解析成MappedStatement对象,但是最终这个对象也会加入Configuration中。


至于Configuration是如何被使用的,以及SQL的执行部分,我会在下一篇说SQL执行的时候分享。


END

相关文章
|
3月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
2月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
57 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
139 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
4月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
4月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
4月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
4月前
|
缓存 Java 数据库连接
我要手撕mybatis源码
该文章深入分析了MyBatis框架的初始化和数据读写阶段的源码,详细阐述了MyBatis如何通过配置文件解析、建立数据库连接、映射接口绑定、动态代理、查询缓存和结果集处理等步骤实现ORM功能,以及与传统JDBC编程相比的优势。
我要手撕mybatis源码
|
2月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
119 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
58 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
358 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个

相关实验场景

更多
下一篇
无影云桌面