MyBatis源码解析(一)——MyBatis初始化过程解析

转载于 https://www.jianshu.com/p/7bc6d3b7fb45

1. 准备工作

为了看清楚MyBatis的整个初始化过程,先创建一个简单的Java项目,目录结构如下图所示:
MyBatis源码解析(一)——MyBatis初始化过程解析_第1张图片

1.1 Product 产品实体类
public class Product {
    private long id;
    private String productName;
    private String productContent;
    private String price;
    private int sort;
    private int falseSales;
    private long category_id;
    private byte type;
    private byte state;
    // PS:省略setter、getter函数
}
1.2 ProductMapper 产品持久化接口
public interface ProductMapper {
    /**
     * 查询所有的产品
     * @return
     */
    List selectProductList();
}
1.3 ProductMapper.xml 产品映射文件

 
 
     
 
1.4 db.properties 数据库配置文件
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/waimai?useUnicode=true&characterEncoding=utf8
username=root
password=xxxxxx
1.5 mybatis.xml MyBatis的配置文件



    
        
        
    

    
        
            
            
                
                
                
                
            
        
    
    
        
    

1.6 Main 主函数
public class Main {
    public static void main(String[] args) throws IOException {

        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            List productList = productMapper.selectProductList();
            for (Product product : productList) {
                System.out.printf(product.toString());
            }
        } finally {
            sqlSession.close();
        }
    }
}

2. MyBatis初始化过程

2.1 获取配置文件

当系统初始化时,首先会读取配置文件,并将其解析成InputStream

String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
2.2 创建SqlSessionFactoryBuilder对象

从SqlSessionFactoryBuilder的名字中可以看出,SqlSessionFactoryBuilder是用来创建SqlSessionFactory对象的。
来看一下SqlSessionFactoryBuilder源码:
MyBatis源码解析(一)——MyBatis初始化过程解析_第2张图片
SqlSessionFactoryBuilder中只有一些重载的build函数,这些build函数的入参都是MyBatis配置文件的输入流,返回值都是SqlSessionFactory;由此可见,SqlSessionFactoryBuilder的作用很纯粹,就是用来通过配置文件创建SqlSessionFactory对象的。

2.3 SqlSessionFactory创建过程

下面具体来看一下,build函数是如何创建SqlSessionFactory对象的。

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) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
2.3.1 构造XMLConfigBuilder对象

build函数首先会构造一个XMLConfigBuilder对象,从名字上大致可以猜到,该对象是用来解析XML配置文件的。下面来看一下XMLConfigBuilder的体系结构。
MyBatis源码解析(一)——MyBatis初始化过程解析_第3张图片

  • XMLxxxBuilder是用来解析XML配置文件的,不同类型XMLxxxBuilder用来解析MyBatis配置文件的不同部位。比如:XMLConfigBuilder用来解析MyBatis的配置文件,XMLMapperBuilder用来解析MyBatis中的映射文件(如上文提到的ProductMapper.xml),XMLStatementBuilder用来解析映射文件中的SQL语句。
  • 这些XMLxxxBuilder都有一个共同的父类——BaseBuilder。这个父类维护了一个全局的Configuration对象,MyBatis的配置文件解析后就以Configuration对象的形式存储。
  • 当创建XMLConfigBuilder对象时,就会初始化Configuration对象,并且在初始化Configuration对象的时候,一些别名会被注册到Configuration的typeAliasRegistry容器中。
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  super(new Configuration());
   ErrorContext.instance().resource("SQL Mapper Configuration");
   this.configuration.setVariables(props);
   this.parsed = false;
   this.environment = environment;
   this.parser = parser;
   }

public Configuration() {
   typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
   typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
   
   typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
   typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
   typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
   
   typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
   typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
   typeAliasRegistry.registerAlias("LRU", LruCache.class);
   typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
   typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
   ……
}
2.3.2 解析配置文件

当有了XMLConfigBuilder对象之后,接下来就可以用它来解析配置文件了。

 private void parseConfiguration(XNode root) {
  try {
    // 解析节点
    propertiesElement(root.evalNode("properties"));
    // 解析节点
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    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);
  }
}

从上述代码中可以看到,XMLConfigBuilder会依次解析配置文件中的< settings >< environments>< typeAliases >< plugins >< mappers >等属性。下面介绍下几个重要属性的解析过程。

2.3.2.1 节点的解析过程

节点的定义如下:


  
  

节点的解析过程:

/**
  * @Param context 节点
  */
private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 获取节点的所有子节点
    Properties defaults = context.getChildrenAsProperties();
    // 获取节点上的resource属性
    String resource = context.getStringAttribute("resource");
    // 获取节点上的url属性
    String url = context.getStringAttribute("url");
    // resource和url不能同时存在
    if (resource != null && url != null) {
      throw new BuilderException("The properties element cannot specify both a URL 
      and a resource based property file reference.  Please specify one or the other.");
    }
    if (resource != null) {
      // 获取resource属性值对应的properties文件中的键值对,并添加至defaults容器中        
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      // 获取url属性值对应的properties文件中的键值对,并添加至defaults容器中
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    // 获取configuration中原本的属性,并添加至defaults容器中
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    // 将defaults容器添加至configuration中
    configuration.setVariables(defaults);
  }
}

首先读取节点下的所有节点,并将每个节点的name和value属性存入Properties中。
然后读取节点上的resource、url属性,并获取指定配置文件中的name和value,也存入Properties中。(PS:由此可知,如果resource节点上定义的属性和properties文件中的属性重名,那么properties文件中的属性值会覆盖resource节点上定义的属性值。)
最终,携带所有属性的Properties对象会被存储在Configuration对象中。

2.3.2.2 节点的解析过程

节点的定义如下:


  
  
  

节点的解析过程:
属性的解析过程和 属性的解析过程极为类似,这里不再赘述。最终,所有的setting属性都被存储在Configuration对象中。

2.3.2.3 属性的解析过程

属性的定义方式有如下两种:

方式1:


  
  

方式2:


  

采用这种方式时,MyBatis会为指定包下的所有类起一个别名,该别名为首字母小写的类名。

节点的解析过程如下:

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    // 遍历下的所有子节点
    for (XNode child : parent.getChildren()) {
      // 若当前结点为
      if ("package".equals(child.getName())) {
        // 获取上的name属性(包名)
        String typeAliasPackage = child.getStringAttribute("name");
        // 为该包下的所有类起个别名,并注册进configuration的typeAliasRegistry中          
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } 
      // 如果当前结点为< typeAlias >
      else {
        // 获取alias和type属性
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        // 注册进configuration的typeAliasRegistry中
        try {
          Class clazz = Resources.classForName(type);
          if (alias == null) {
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }
}

如果节点下定义了节点,那么MyBatis会给该包下的所有类起一个别名(以类名首字母小写作为别名)
如果节点下定义了节点,那么MyBatis就会给指定的类起指定的别名。
这些别名都会被存入configuration的typeAliasRegistry容器中。

2.3.2.4 节点的解析过程

节点的定义方式有如下四种:
方式1:


  

方式2:


  

方式3:


  

方式4:


  

节点的解析过程如下:

 private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历下所有子节点
    for (XNode child : parent.getChildren()) {
      // 如果当前节点为
      if ("package".equals(child.getName())) {
        // 获取的name属性(该属性值为mapper class所在的包名)
        String mapperPackage = child.getStringAttribute("name");
        // 将该包下的所有Mapper Class注册到configuration的mapperRegistry容器中
        configuration.addMappers(mapperPackage);
      } 
      // 如果当前节点为
      else {
        // 依次获取resource、url、class属性
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // 解析resource属性(Mapper.xml文件的路径)
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          // 将Mapper.xml文件解析成输入流
          InputStream inputStream = Resources.getResourceAsStream(resource);
          // 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class注册进configuration对象的mapperRegistry容器中
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, 
          resource, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // 解析url属性(Mapper.xml文件的路径)
        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属性(Mapper Class的全限定名)
        else if (resource == null && url == null && mapperClass != null) {
          // 将Mapper Class的权限定名转化成Class对象
          Class mapperInterface = Resources.classForName(mapperClass);
          // 注册进configuration对象的mapperRegistry容器中
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url,
           resource or class, but not more than one.");
        }
      }
    }
  }
}

MyBatis会遍历下所有的子节点,如果当前遍历到的节点是,则MyBatis会将该包下的所有Mapper Class注册到configuration的mapperRegistry容器中。
如果当前节点为,则会依次获取resource、url、class属性,解析映射文件,并将映射文件对应的Mapper Class注册到configuration的mapperRegistry容器中。

其中,节点的解析过程如下:

XMLMapperBuilder mapperParser = 
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

在解析前,首先需要创建XMLMapperBuilder,创建过程如下:

private XMLMapperBuilder(XPathParser parser, Configuration configuration, 
String resource, Map sqlFragments) {

  // 将configuration赋给BaseBuilder
  super(configuration);
  // 创建MapperBuilderAssistant对象(该对象为MapperBuilder的协助者)
  this.builderAssistant = new  MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}

首先会初始化父类BaseBuilder,并将configuration赋给BaseBuilder;
然后创建MapperBuilderAssistant对象,该对象为XMLMapperBuilder的协助者,用来协助XMLMapperBuilder完成一些解析映射文件的动作。

当有了XMLMapperBuilder后,便可进入解析的过程:

public void parse() {
  // 若当前的Mapper.xml尚未被解析,则开始解析
  // PS:若节点下有相同的节点,那么就无需再次解析了
  if (!configuration.isResourceLoaded(resource)) {
    // 解析节点
    configurationElement(parser.evalNode("/mapper"));
    // 将该Mapper.xml添加至configuration的LoadedResource容器中,下回无需再解析
    configuration.addLoadedResource(resource);
    // 将该Mapper.xml对应的Mapper Class注册进configuration的mapperRegistry容器中
    bindMapperForNamespace();
  }

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

configurationElement函数

private void configurationElement(XNode context) {
try {
  // 获取节点上的namespace属性,该属性必须存在,表示当前映射文件对应的Mapper Class是谁
  String namespace = context.getStringAttribute("namespace");
  if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  // 将namespace属性值赋给builderAssistant
  builderAssistant.setCurrentNamespace(namespace);
  // 解析节点
  cacheRefElement(context.evalNode("cache-ref"));
  // 解析节点
  cacheElement(context.evalNode("cache"));
  // 解析节点
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // 解析节点
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  // 解析节点
  sqlElement(context.evalNodes("/mapper/sql"));
  // 解析sql语句      
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}

resultMapElements函数
该函数用于解析映射文件中所有的节点,这些节点会被解析成ResultMap对象,存储在Configuration对象的resultMaps容器中。

节点定义如下:

 
  
     
     
  
  
  

节点的解析过程:

private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings) 
throws Exception {

  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  // 获取上的id属性
  String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
  // 获取上的type属性(即resultMap的返回值类型)
  String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
        resultMapNode.getStringAttribute("resultType",
            resultMapNode.getStringAttribute("javaType"))));
  // 获取extends属性
  String extend = resultMapNode.getStringAttribute("extends");
  // 获取autoMapping属性
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  // 将resultMap的返回值类型转换成Class对象
  Class typeClass = resolveClass(type);
  Discriminator discriminator = null;
  // resultMappings用于存储下所有的子节点
  List resultMappings = new ArrayList();
  resultMappings.addAll(additionalResultMappings);
  // 获取并遍历下所有的子节点
  List resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    // 若当前节点为,则将它的子节点们添加到resultMappings中去
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前节点为,则进行条件判断,并将命中的子节点添加到resultMappings中去
    else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前节点为,则将其添加到resultMappings中去
    else {
      // PS:flags仅用于区分当前节点是否是,因为这两个节点的属性名为name,
      // 而其他节点的属性名为property
      List flags = new ArrayList();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  // ResultMapResolver的作用是生成ResultMap对象,并将其加入到Configuration对象的resultMaps容器中(具体过程见下)
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id,
   typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

ResultMapResolver这个类很纯粹,有且仅有一个函数resolve,用于构造ResultMap对象,并将其存入Configuration对象的resultMaps容器中;而这个过程是借助于MapperBuilderAssistant.addResultMap完成的。

public ResultMap resolve() {
  return assistant.addResultMap(this.id, this.type, this.extend,  
  this.discriminator, this.resultMappings, this.autoMapping);
}

sqlElement函数
该函数用于解析映射文件中所有的节点,并将这些节点存储在当前映射文件所对应的XMLMapperBuilder对象的sqlFragments容器中,供解析sql语句时使用。

 ${alias}.id,${alias}.username,${alias}.password 

buildStatementFromContext函数
该函数会将映射文件中的sql语句解析成MappedStatement对象,并存在configuration的mappedStatements。

2.3.3 创建SqlSessionFactory对象
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) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

回过头来再看一下SqlSessionFactory的build函数,刚才说了半天,介绍了XMLConfigBuilder解析映射文件的过程,解析完成之后parser.parse()函数会返回一个包含了映射文件解析结果的configuration对象,紧接着,这个对象将作为参数传递给另一个build函数,如下:

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

这个函数将configuration作为参数,创建了DefaultSqlSessionFactory对象。
DefaultSqlSessionFactory是接口SqlSessionFactory的一个实现类,SqlSessionFactory的体系结构如下图所示:

MyBatis源码解析(一)——MyBatis初始化过程解析_第4张图片
此时,SqlSessionFactory创建完毕!

你可能感兴趣的:(Mybatis源码分析,Mybatis源码分析)