前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mybatis源码学习(一)SqlSessionFactoryBuilder

Mybatis源码学习(一)SqlSessionFactoryBuilder

作者头像
虞大大
发布2020-08-26 17:21:53
5600
发布2020-08-26 17:21:53
举报
文章被收录于专栏:码云大作战码云大作战

一、什么是Mybatis

引用Mybatis文档中的介绍:Mybatis是一款优秀的持久层框架,他支持自定义Sql、存储过程以及高级映射。Mybatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。Mybatis可以通过简单的XML或注解来配置和映射原始类型、接口和Java的POJO为数据库中的记录。

而对于我自己想学习Mybatis的源码的原因,一是想了解Mybatis的源码,二是想学习下Mybatis中的设计模式,比如责任链、代理、装饰者模式等。

二、传统的JDBC操作

1、加载数据库驱动。

2、通过DriverManager注册驱动。

3、通过DriverManager创建数据库连接Connection。

4、通过Connection创建Statement/PreparedStatement。

5、通过Statement进行数据库操作。

6、处理结果集。

7、关闭资源。

由于传统的JDBC中,如果每一个线程对数据库进行操作时,都需要进行上述的七步操作,从而导致开发中不停的造轮子,因此我们可以引入Mybatis框架,交给Mybatis来帮我们做这些事,我们只需要关心对应的配置信息和sql的编写即可。

三、SqlSessionFactoryBuilder

引用Mybatis官方文档,构建SqlSessionFactory时会使用到SqlSessionFactoryBuilder。

因此SqlSessionFactoryBuilder的作用为构建SqlSessionFactory。

· SqlSessionFactoryBuilder源码

代码语言:javascript
复制
public class SqlSessionFactoryBuilder {
  //...
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
}

build中的源码过程即对parser进行解析,解析完后,生成SqlSessionFactroy对象。

观察DefaultSqlSessionFactory(config)构造函数,可以发现,SqlSessionFactory类中维护了一个configuration对象,用于存储environment(数据库相关环境信息)、mapperRegistry(mapper或dao层接口映射器信息)、typeHandlerRegistr(类型处理器信息)等。

代码语言:javascript
复制
public DefaultSqlSessionFactory(Configuration configuration) {
  this.configuration = configuration;
}

· 解析XML文件

代码语言:javascript
复制
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    //解析XML文件
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
代码语言:javascript
复制
    //...
代码语言:javascript
复制
}
代码语言:javascript
复制
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);  //调用jdk方法,生成document树  this.document = createDocument(new InputSource(inputStream));
}

引用Mybatis官方文档配置的xml信息为:

========>

转换为document树后的结点信息为:

解析document的核心代码(理解即可,我没细看):

代码语言:javascript
复制
public boolean scanDocument(boolean complete)
throws IOException, XNIException {
    //...
    int event = next();
    do {
        switch (event) {
            case XMLStreamConstants.START_DOCUMENT :
                //遇到了开始的document元素
                break;
            case XMLStreamConstants.START_ELEMENT :
代码语言:javascript
复制
                //遇到了开始的element元素
代码语言:javascript
复制
                break;
            case XMLStreamConstants.CHARACTERS :                //字符集处理
                fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity);
                fDocumentHandler.characters(getCharacterData(),null);
                break;
            case XMLStreamConstants.SPACE:
                //空格处理
                break;   
代码语言:javascript
复制
            //...
代码语言:javascript
复制
            case XMLStreamConstants.COMMENT :                                 //注释处理                fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity);
                fDocumentHandler.comment(getCharacterData(),null);
                break;
            case XMLStreamConstants.DTD :
                //DTD约束
                break;       
代码语言:javascript
复制
            //...
代码语言:javascript
复制
            case XMLStreamConstants.NAMESPACE :                //nameSpace处理                break;
            case XMLStreamConstants.ATTRIBUTE :                //attribute处理                break;
            case XMLStreamConstants.END_ELEMENT :
                //element元素结束符号
                break;
            default :
                throw new InternalError("processing event: " + event);
        }
        event = next();
    } while (event!=XMLStreamConstants.END_DOCUMENT && complete);
代码语言:javascript
复制
    //...
代码语言:javascript
复制
}

· 解析document树

代码语言:javascript
复制
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    //解析XML文件生成document树
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);        //解析document树,生成Configuration对象    return build(parser.parse());
代码语言:javascript
复制
    //...
代码语言:javascript
复制
}
代码语言:javascript
复制
public Configuration parse() {

//...

//1、parser.evalNode,将document树解析成XNode结点对象

代码语言:javascript
复制
  //2、解析XNode为Configuration对象
代码语言:javascript
复制
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

四、parseConfiguration

代码语言:javascript
复制
private void parseConfiguration(XNode root) {
  try {
    Properties settings = settingsAsPropertiess(root.evalNode("settings"));
    propertiesElement(root.evalNode("properties"));
    loadCustomVfs(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

选择几个我认为是重要的看下是如何解析的。

· environment

代码语言:javascript
复制
private void parseConfiguration(XNode root) {
    //...
    environmentsElement(root.evalNode("environments"));
代码语言:javascript
复制
    //...

}

代码语言:javascript
复制
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
        if (environment == null) {        //从上下文中设置默认的environment对象        environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
        //获取mybatis事物管理器对象
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"))        //获取数据库连接dataSource资源对象        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();        //构造器模式创建environment对象        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());

environment比较简单,即加载了配置的事物管理器、数据库资源对象,并通过构造器模式封装成environment对象被加载到configuration中。

· mapper

代码语言:javascript
复制
private void parseConfiguration(XNode root) {
    //...
    mapperElement(root.evalNode("mappers"));
代码语言:javascript
复制
    //...
代码语言:javascript
复制
}
代码语言:javascript
复制
public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}
代码语言:javascript
复制
public <T> void addMapper(Class<T> type) {    //mapper必须为接口  if (type.isInterface()) {
      //...
      knownMappers.put(type, new MapperProxyFactory<T>(type)); 
代码语言:javascript
复制
      //...
代码语言:javascript
复制
  }
}

从addMapper中可以看出,所有的mapper都会被放到mapperRegistry中key-mapper接口,value-动态代理工厂,并且mapper必须为接口类型。

因此在Mybatis官方文档示例中的session.getMapper就相当于从mapperRegistry这个map中取出来对应的mapper接口。

但是此时只解析的mapper接口,而XML中的增删改查的方法还没有被解析出来。

1、根据mapper接口文件获取文件路径名。

代码语言:javascript
复制
public <T> void addMapper(Class<T> type) {
      //...
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();  
代码语言:javascript
复制
      //...
代码语言:javascript
复制
}
代码语言:javascript
复制
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;  //...}

2、开始加载mapper.xml

代码语言:javascript
复制
public void parse() {  //获取接口路径名  String resource = type.toString();
  //防止重复加载  if (!configuration.isResourceLoaded(resource)) {    //加载mapper.xml    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    //加载缓存    parseCache();
    parseCacheRef();
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        if (!method.isBridge()) {          //加载mapper中的sql注解信息          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }  //移除掉未完成信息  parsePendingMethods();
}
代码语言:javascript
复制
private void loadXmlResource() {
  //防止重复加载
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {    //xml标识,路径名    String xmlResource = type.getName().replace('.', '/') + ".xml";
    InputStream inputStream = null;
    try {
      inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
    } catch (IOException e) {
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());            //加载xml      xmlParser.parse();
    }
  }
}
代码语言:javascript
复制
public void parse() {
if (!configuration.isResourceLoaded(resource)) {    //加载xml中的namespace、resultMap、paramterMap、sql、增删改查标签    configurationElement(parser.evalNode("/mapper"));
    //将mapper资源,放入到已加载列表中    configuration.addLoadedResource(resource);
    //绑定mapper.xml文件和nameSpace:+路径 到队列中    bindMapperForNamespace();
  }
  //加载完成后移除队列操作处理
  parsePendingResultMaps();
  parsePendingChacheRefs();
  parsePendingStatements();
}

加载mapper.xml的核心方法:

代码语言:javascript
复制
private void configurationElement(XNode context) {
try {//namespace作为标识符,不能为空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"));
    //parameterMap的初始化    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    //resultMap的初始化    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //sql标签的初始化    sqlElement(context.evalNodes("/mapper/sql"));
    //增删改查标签的初始化    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

到此刻为止,mapper.xml中的所有信息已经被加载到mapperRegistry中了。

3、开始加载mapper接口中带注解的方法

通过parseStatement方法进行加载。

代码语言:javascript
复制
public void parse() {          //...parseStatement(method);          //...
}

mapper元素的加载,其实在于mapperRegistry注册中心。加载mapper元素时会将xml中所有的mapper进行循环遍历,全部放到mapperRegistry中(key-mapper.xml根据路径转换后的class接口,value-该class接口的工厂类,内部其实为一个JDK动态代理类)。

然后开始先对mapper.xml文件中的元素进行加载。根据顺序加载mybatis缓存、parameterMap、resultMap、sql标签信息、增删改查标签信息进行初始化封装。最后将namespace:+mapper.java文件路径名的信息作为资源名放入到队列中,作为是否重复加载的依据。

接着再通过parseStatement方法对mapper.java中的增删改查sql注解进行加载。全部加载完毕后,将他们从未加载的队列中移除。

注意点:阅读源码时发现,至少有两处地方都对同一个class文件进行了对mapperRegistry的注册。

原因:spring可能无法知道真正的mapper资源是否被加载,因此这里设置了一个特殊的标识符即namespace:+mapper.java文件的路径名放入到队列中作为是否重复加载的依据,所以在最后还需要调用下mapperRegistry.put。

五、SqlSessionFactory

此时的configuration中已经封装了绝大部分mybatis的信息,最后,SqlSessionFactoryBuilder的build方法执行完毕后,会调用SqlSessionFactory的初始化方法,加载出SqlSessionFactory,并将configuration对象进行赋值。完成最后使命。

代码语言:javascript
复制
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}
代码语言:javascript
复制
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }  //...

六、总结

SqlSessionFactoryBuilder类的作用其实就是解析配置文件、创建SqlSessionFactory。

问题一:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession的作用域如何?

SqlSessionFactoryBuilder被创建出来就是为了解析Mybatis的配置文件、创建SqlSessionFactory,一旦SqlSessionFactory被创建出来,该类就不再被需要了。

SqlSessionFactory一旦被创建就与服务器应用共存亡,用来创建SqlSession。相当于数据库连接池。

SqlSession相当于一个数据库中的事务,由每个线程来创建SqlSession,进行数据库操作。因此SqlSession的作用域和线程相关。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码云大作战 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档