mybatis源码(二)sql提取

上篇文章描述研究源码所用的demo,本篇文章开始正式追踪源码,在开看前,再次确认下我们要研究的问题:
        1、mybatis如何找到存储sql的xml文件
        2、mybatis如何解析xml中sql相关标签并拼装成完整的sql语句
        3、mybatis如何将xml中sql与接口中方法绑定
        4、mybatis如何将sql发送出去并获得结果

另外,mybatis一般有两种xml配置,一种是mybatis-config.xml这种配置管理mybatis全局属性的文件,一种是person.xml配置接口类方法对应sql的文件,为了容易区分,再后续的文章中,前者有时也称为配置文件,后者有时则称为mapper文件或mapper.xml文件或包含sql的文件。

1)首先我们指定mybatis的配置文件路径,根据配置文件创建SqlSessionFactory。

 

 build方法中有两个比较重要,一个是根据指定的mybatis-config.xml文件流生成xml解析器,另一个则是调用解析器的parse方法解析xml文件并生成配置对象。生成xml解析器的过程不属于我们关注的重点,所以这里不深入阅读,这里我们着重看下parse方法。

parse方法中,首先会去解析configuration标签中的内容,生成XNode对象(存储xml文件的数据结构),然后在parseConfiguration进行处理,我们先断点看下XNode对象中存储的信息。

可以看到mybatis-config.xml的文件信息全存储到了XNode中,接下来我们再来看看parseConfiguration方法中的处理流程。

在 parseConfiguration中会依次处理properties、settings、typeAlias .... mappers等xml标签,而我们编辑sql的xml文件便是在mapppers标签中指定的,如下:

 root.evalNode("mappers")首先会获取mappers标签的内容,随后通过mapperElement方法进行处理,所以接下来深入mapperElement方法来看看mappers标签的处理过程(暂时打断下,我们的第一个问题“mybatis如何找到存储sql的xml文件”至此已经解决)。
这个方法中需要讲解的内容比较多,为了更好的讲解源码,这里不再截图,直接在源码中进行注释说明:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //如果不写mapper.xml文件(含有sql的文件),可以直接在dao方法上添加@Select,@Insert等注解。此时只需要在配置文件的mappers标签中增加package属性进行。此时mybatis会扫描指定的包路径下的所有接口类,并查找其中的注解,解析后放入类型为Map<Class<?>, MapperProxyFactory<?>> 属性名为knownMappers的集合中。
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
        //分别读取mappers标签中resource、url、class的属性值
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
        //resouce不为空,url和class为空的场景
          if (resource != null && url == null && mapperClass == null) {
            //初始化创建一个错误信息存储对象,该对象通过threadlocal存储,可以做到线程独立性
            ErrorContext.instance().resource(resource);
            //获取resource指定路径文件的流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //创建mapper解析器(与前面mybatis-config.xml配置文件解析器XMLConfigBuilder类似,要区分开)
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            //解析包含sql的xml文件
            mapperParser.parse();
        //url不为空,resouce和class为空的场景
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
        //class不为空,url和resouce为空的场景
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

 通过上面的源码可以知道,mybatis不仅可以通过xml文件配置sql语句,直接通过注解也可以设置sql语句(注解中其实还会再扫描确认一次是否有含sql的xml文件)。因为我们的demo是通过xml编辑的sql,且mappers标签中只设置了resource属性,所以这里着重看下resouce不为空,url和class为空的代码处理逻辑:

public void parse() {
    //如果resource指定的xml文件未被加载过则进行处理
    if (!configuration.isResourceLoaded(resource)) {
      //获取mapper标签内容并通过configurationElement方法处理
      configurationElement(parser.evalNode("/mapper"));
      //将该resource放入set集合,该集合统计已经加载过的xml文件,避免重复加载
      configuration.addLoadedResource(resource);
      //将resource指定的xml文件和命名空间绑定,表明该文件处理过。并通过命名空间将加载接口类,将接口信息存入到Configuration中的mapperRegistry属性中。
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

比较重要的主要有configurationElement、bindMapperForNamespace等方法,接下来我们再进入看一下:

  private void configurationElement(XNode context) {
    try {
      //获取标签中namespace属性值,namespace为空间命名,非接口编程时可以随便写。在通过接口编程时,该namespace则为对应接口的包路径。
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      //暂存namespace空间命名信息,再后面会根据命名空间加载接口类,并存储命名空间防止包含sql的映射文件被多次加载(前面已经暂存过resource指定的路径防止多次加载,这里是另一重保险,具体就不细讲了)
      builderAssistant.setCurrentNamespace(namespace);
      //处理cache-ref标签
      cacheRefElement(context.evalNode("cache-ref"));
      //处理cache标签
      cacheElement(context.evalNode("cache"));
      //处理parameterMap参数对象映射标签
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //处理resultMap结果集映射标签
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //处理sql标签
      sqlElement(context.evalNodes("/mapper/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);
    }
  }

 configurationElement方法中会对不同的标签进行不同的操作处理,因为demo中只有一个select标签,所以我们直接深入buildStatementFromContext方法查看进一步的处理:

  // select|insert|update|delete 标签可能有多个,所以使用List参数
  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) {
    //遍历 select|insert|update|delete 所有的标签内容
    for (XNode context : list) {
      //创建sql语句解析器
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        //解析sql
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

和前面解析配置文件和mapper文件类似,这里也是通过解析器解析select|insert|update|delete标签中的sql内容,这里我们再接着看下parseStatementNode方法:

  public void parseStatementNode() {
    // 获取sql语句对应标签中的各种属性 (这些是xml解析的重点,但不是回答我们问题的重点,所以下面关于一些结果类型的解析并未深入查看)
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    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);
    // 加载结果类
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    
    // 获取当前标签节点的名称
    String nodeName = context.getNode().getNodeName();
    //根据标签节点的名称判断当前sql的类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //如果当前节点描述的是select查询语句,则置位isSelect属性
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //根据是否为select查询语句,获取flushCache和useCache的标识位
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //select查询默认会开启缓存,当然这里仅仅是开启mybatis的自带缓存(session级别或namesapce级别,且缓存有限),如果想做到分布式缓存,最好是外接使用第三方缓存中间件
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 创建include标签解析器
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    // 初始化解析标签中的include子标签,解析完成后移除include标签
    includeParser.applyIncludes(context.getNode());

    // 解析标签中的selectKey子标签,解析完成后移除selectKey标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // 创建一个包含sql语句的源对象,源对象包含提取的sql信息以及参数解析器等
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //分别获取标签中resultSets keyProperty keyColumn三个属性
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    //主键生成器,一般insert语句用的到
    KeyGenerator keyGenerator;
    // 解析xml namespace属性,namespace加id构成了sql的唯一标识信息(也是我们解答第三题的关键)
    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;
    }
    //将这条sql标签对应的所有信息存入到MappedStatement对象中(建造者模式),并放入到一个map集合中,该map的key即为“包名.类型.标签中id属性名”
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

这个方法结束后,sql被提取了出来,并且该条sql对应的所有信息存储成一个以包名.类型.标签中id属性名为key的键值对,至此sql如何被解析出来我们基本已经清楚,主要是在生成SqlSource的过程中提取出来的,后面我们会再接着看。再接着看之前我想再提醒大家注意一个和第三问相关的点,目前xml中提取的sql存储到一个map集合中,这个map的key则是namespace.标签中id属性名请注意此时这个key实际也是包名.类名.接口名,因为id属性名一般都是定义为接口中的方法名,所以这个key可以直接定位到方法,即达到sql和方法绑定的目的(我们解答第三题的关键点之一)。demo中的源码情况如下所示:

 具体sql和方法的绑定调用我们放在下一篇文章中讲,这里先提醒大家注意下。本文后续还是接着讲如何提取sql,下面我们深入到SqlSource的创建源码中去看。



 这里可以看到仍然是老套路创建“XML脚本解析器”,然后通过解析器解析节点,我们着重看下解析方法。

 
终于看到我们朝思暮想的sql提取方法了,这里会创建一个DynamicContext对象,通过该对象提取XML中的sql语句,这里因为源码不太好深入进去看,而且优点偏向另一个领域的知识,所以sql的获取就追踪到这。有兴趣的可以再深入看看。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目Maven构建,真实大型互联网架构,做到高并发,大数据处理,整个项目使用定制化服务思想,提供模块化、服务化、原子化的方案,将功能模块进行拆分,可以公用到所有的项目中。架构采用分布式部署架构,所有模块进行拆分,使项目做到绝对解耦,稳定压倒一切~~ 持续集成: 1. 我的待办工作流服务(提供Webservice服务) 2. 我的待办工作流集成JMS消息服务(支持高并发,可支持成千上万系统集成) 3. 我的任务提供Rest服务,完成日常的工作管理,通过定时调度平台,动态生成我的任务、循环周期任务、定时邮催提醒完成任务等 4. 文件上传、多线程下载服务化、发送邮件、短信服务化、部门信息服务化、产品信息服务化、信息发布服务化、我的订阅服务化、我的任务服务化、公共链接、我的收藏服务化等 系统模块: 1. 用户管理: 用户信息管理(添加、删除、修改、用户授权、用户栏目管理、查询等) 用户组管理(添加、删除、修改、用户组栏目授权,栏目授权、查询、用户组人员添加查询等) 用户角色管理(添加、删除、修改、用户角色授权、用户角色栏目信息查询设置等) 2. 文章管理: 栏目管理:查询无限极栏目树、创建无限极栏目树分类(导航栏目、图片列表栏目、文章列表栏目、文章内容栏目等)、删除、修改栏目信息。 文章管理:创建、删除、修改文章,多维度文章查询,包括已发布、未发布、所有文章等。文章富文本编辑器、文章多文件上传、文章状态控制等。 3. 系统设置: 数据字典管理:支持中、英文信息,支持无限级别分类配置,动态控制是否可用等。 部门信息管理:支持中、英文无限级别部门信息增加,删除,修改操作,部门列表、树心查询等。 日志管理:系统日志列表查询、在线查看、在线下载等 路线管理:集成百度地图API,提供线路查询管理功能 Druid Monitor(监控):集成阿里巴巴连接池,提供在线连接池监控程序,包括:数据源、SQL监控、URL监控、Session监控、Spring监控等 网站信息管理:通过系统配置文件进行网站内容操作,包括邮件服务器配置、公司基本信息配置等。 4. 集成REST服务,可以用作独立服务平台(提供大量实例及测试平台,包括:文件上传下载、邮件短信发送、部门、产品、公共连接、我的收藏、我的任务、信息发布等) 5. 集成Quartz调度,可以用作定时调度平台(动态配置调度类、调度时间,使程序自动执行某些业务) 6. Lucene搜索引擎,可以将文件资料索引化,支持文件内容搜索、关键字搜索、高亮关键字等,使信息在毫秒内提取查询出来 7. 用户设置功能:包括修改用户信息,修改密码、发送消息,修改个人图片,查看角色、查看用户组,管理员修改角色、用户、用户组等。 8. 集成Webservice平台,包括jaxws服务、CXF框架,配置双加密的权限认证。使服务集成更加安全。 9. Bootstrap html5提供了两套前台开环境,包括CMS和电子商务网站,使您的开发更加的简洁。 技术点: 1. Springmvc + Mybatis集成、SpringSecurity权限控制、Spring AOP事务处理。 2. Wink Rest服务、Webservice服务:jaxws、CXF等 3. IO 流上传下载文件,多线程操作 4. 发送邮件,配置邮件服务器,发基于html、纯文本格式的邮件(可以免费赠送网络爬虫,使其群发邮件,做到广告推送等) 5. MD5加密(登陆密码校验加密等),用户统一Session、Cookie管理,统一验证码校验等。 6. 数据库连接池统一配置 7. Quartz定时调度任务集成(直接通过配置即可) 8. Httpclient破解验证码,登陆联通充值平台 9. 汉字、英文拆分、可以用作文档关键字搜索等。 10. Base64图片处理,支持PC,Android,IOS 11. Service Socket 、Client Socket 通信技术(已经做过GPRS数据获取,并用到了项目中) 12. 提供大量工具类,可以直接使用 13. Maven项目构建,您可以直接做架构,可以提升自己的学习能力,使您成为真正的架构师。 版本支持: 支持版本: jdk 1.6、1.7、1.8 Web容器: Tomcat 6、7、 8 数据库: mysql

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值