Mybatis源码学习(一)SqlSessionFactoryBuilder

2020-08-26 17:21:53 浏览数 (1)

一、什么是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的作用域和线程相关。

0 人点赞