简介
Mybatis是一款优秀的ORM框架,是国内使用的ORM框架中的主流,它使用简单,入们很快。对于有过几年Java开发经验的老司机,我相信都对它如何使用已经能够轻车熟路了,但是,阅读Mybatis的源码的估计不多,如果各位朋友和要是和本人一样,对阅读源码很感兴趣,想要知道Mybatis庐山真面目的,可以一起探讨探讨。
Mybatis的使用的入口
我们先来看一下最简单的查询一个对象的实例 。
//读取配置
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml");
//创建sqlSeessionFactory
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
//打开sqlSession
SqlSession ssn = ssf.openSession();
//查询一个user对象
User user = ssn.selectOne("mapping.userMapper.getUserByName", "testUser");
//关闭会话
ssn.close();
上面的代码是Mybatis最简单的使用例子,麻雀虽小,五脏俱全,虽然只有短短5行代码,它已经完整的包括了Mybatis的使用周期。Mybatis的周期从大体上可分为三部分
我们要对Mybatis源码解析,首先我们需要先列出Mybatis整个流程当中可能会用到的对象类型列出来,先弄清大体的框架,再去抠细节
上面的类型,其实还可以分得更简单一些,分为三个过程,再将每个类型对位到不同的过程,像下图那样
初始化
那么如何从代码的角度去分析mybatis的初始化过程,首先我用一个图简单表示一下需要用到的类型前后关系
Mybatis的使用的第一步是传入根配置信息,我们拿最开头的事例的头两行代码出来看看
//Reader只是一个读文件的接口,这里还没有开始读
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml");
//将配置文入口传给SqlSessionFactoryBuilder的实例方法
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
我再点击进去看一下SqlSessionFactoryBuilder的build()方法的实现
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
build代码很简单,它有几个重载但没有多大区别,我们看到new XMLConfigBuilder(reader, environment, properties),它有个reader参数就是我们在外面传的配置文件入口,environment是指定的环境(是一个字符串),properties是系统配置信息,我们上面的例子中,没有用到这两个参数,所以传的都是空。接下来,我们在看到build(parser.parse()),parse是解析的意思,它的作用是把配置信息解析成为Configuration对象,然后再创建DefaultSqlSessionFactory对象,DefaultSqlSessionFactory有一个Configuration类型的参数的构造函数。
我们知道DefaultSqlSessionFactory是怎么new来了,那我们再一探个究竟,看看DefaultSqlSessionFactory的部分源码和SqlSessionFactory接口定义
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
从上面的代码可以看到SqlSessionFactory 的接口定义的都是openSession方法和getConfiguration方法,也就是说它的作用只有一个就是用来创建SqlSession对象,并且里面存有一个Configuration的字段。我们再来看看DefaultSqlSessionFactory 的构造函数的实现,DefaultSqlSessionFactory 构造函数只做一件事情,就是保存configuration字段变量。
看了上面的代码之后,其实我们还是不知道Mybatis怎么初始化,因为整个初始化的核心是其实不在SqlSessionFactoryBuiler上,而是在XMLConfigBuilder上。它才是解析整个配置的核心,XMLConfigBuilder的作用是读取根配置文件的配置信息,然后解析为Java对象,最终将它放到Configuration对象里面。
在讲解XMLConfigBuilder之前,我们先来看一下Configuration类内部情况,因为XMLConfigBuilder解析的过程离不开configuration对象
上面的代码很多,但我们不需要细看每句代码,从大体上看,它分为各种配置字段和工厂方法,它的构造函数会初始化一部分配置。那为什么要把工厂方法也放到Configuration上呢? 其实这里的工厂方法都是和配置相关的,例如创建MappedStatement对象,我们知道Mybatis的sql都是写在Mapper配置里面, 创建MappedStatement对象需要读取配置信息,所以把工厂方法放到Configuration里面也是合情合理的,所以不要把Configuration理解为只有配置信息(其实还包括工厂方法,不过个人觉得Configuration和工厂方法放在一起耦合有点大,如果要自己写个自定义Executor类型,想要简单的加到Configuration上是没有办法的)。
接下来,我们来分析加载过程的真正核心,XMLConfigBuilder的内部实现。下面列出XMLConfigBuilder的部分源码
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
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;
}
}
上面列出了XMLConfigBuilder的构造函数和它的父类,先看构造函数,首先它new了一个configuration对象,然后调用父类的构造函数,父类的构造函数做了什么呢?BaseBuilder它只有有三个字段成员,分别为configuration ,typeAliasRegistry,typeHandlerRegistry。configuration是外面传进来的,只需把它存起来就行了, typeAliasRegistry和typeHandlerRegistry是从configuration内部取出来的,它的作用是什么了,看下面的截图,其实不难猜到,typeAliasRegistry是一个别名(Alias)和类型(Type)的映射器(有多个别名对应一个type),因为Java的基础类型和数据库的数据类型是不一样的,所以下面TypeHandlerRegistry可以看到,每个基础类型都注册了一个Handler类型处理程序,所以它的作用就是用来处理类型转换的。例如:我们从数据表里面拿到了一个int类型的字段, 那我们就需要从TypeHandlerRegistry里面找到一个IntegerTypeHandler对象,然后把它转为Java的Integer对象。现在调用完父类的构造函数后,接下来要处理的是设置environment (是环境的名称,比如dev,prod等等)和系统环境参数props, 设置parsed为false,最后设置parser解析器,parser的类型是XPathParser,它的作用是用来解析Xml文件。
接下来,我们再进一步分析,看看XMLConfigBuilder类里面的parse()方法是怎么解析的
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
首先执行parse方法时,需要判断是否已经解析过,因为Configuration实例的初始化不是幂等的,不能重复执行,不然会造成configuration配置重复的问题,接下来我们看到parser.evalNode("/configuration")方法,evalNode()方法的作用是找到Xml文件中的某个节点并将它转为XNode对象返回。
下面是一个简单的MyBatis的根配置信息
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC" />
<!-- 配置数据库连接信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://192.168.31.18:3306/UM?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="abc123" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapping/userMapper.xml"/>
<mapper class="mapping.account_mapper"/>
</mappers>
</configuration>
通过parser.evalNode("/configuration")拿到了<configuration>节点信息之后,我们还需要给它解析下面的所有子节点.parse()方法里面通过parseConfiguration()方法解析configuration节点的每个子节点。接下来我们继续在XMLConfigBuilder类中找到parseConfiguration()方法的实现
private void parseConfiguration(XNode root) {
try {
//解析properties节点
propertiesElement(root.evalNode("properties"));
//解析settings节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//解析typeAliases节点
typeAliasesElement(root.evalNode("typeAliases"));
//解析plugins节点
pluginElement(root.evalNode("plugins"));
//解析objectFactory节点
objectFactoryElement(root.evalNode("objectFactory"));
//解析objectWrapperFactory节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析reflectorFactory节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
//解析environments节点
environmentsElement(root.evalNode("environments"));
//解析databaseIdProvider节点
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析typeHandlers节点
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
上面看到configuration的子节点有十多种,每一种节点的特点和作用都不一样,但节点的读取统一都是evalNode()方法,不过拿到了它们的XNode节点之后,每个都是要通过不同的方式把它加到configuration里面去的。这里我们就只拿我们最关心的mapper节点去作介绍,Mybatis是怎么将Mapper读取到配置的,下面可以看mapperElement()方法的实现
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} 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();
} 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.");
}
}
}
}
}
parent参数对应的是的<mappers>节点(如下图),由于mappers的是一个集合,所以上面的源码对它的子节点进行了遍历, maper节点有个name属性,如果name等于"package"值时,它是一个包(就是把多个配置包在一起),其他不等于"package"的对应的都是一个单独的Mapper。
如果节点的name属性是package的话,那么通过configuration.addMappers()方法直接加到configuration的配置里面(在addMappers()方法里面还会做进一步的解析)。
如果非package节点,又分为三种情况:
1.只存在resource的属性的节点:它是一个资源文件的地址,需要将它转为inputStream类型,再通过XMLMapperBuilder去解析,并且将解析后的结果存到configuration的里面。
2.只存在url属性的节点:和第1种很像,只是resource是项目内的路径,url是项目外部的路径,其实作用一样。
3.只存在class属性的节点:这种使用的是Java配置,采用注解的形式,它不需要将配置解析为XNode对象,而是拿到它的接口信息,再通过configuration.addMapper()方法直接加到configuration里面。
下面是XMLMapperBuilder的处理细节,由于代码太长,这里就不一一细说
public class XMLMapperBuilder extends BaseBuilder {
//Xml解析器
private final XPathParser parser;
//Mapper辅助建造器
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
//资源路径
private final String resource;
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
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"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
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);
}
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
}
mappers里面的mapper节点对应的是一个mapper.xml文件,XMLMapperBuilder是专门对它进行解析的,XMLMapperBuilder通过很多步骤之后,拿到context节点,它也是mapper.xml文件的根节点mapper,然后把它传给configurationElement()方法去处理,从中拿到"select、insert、update、delete"四种节点(如下图)再传给buildStatementFromContext()方法,最后通过XMLStatementBuilder类的parseStatementNode()方法将它们转成MappedStatement并添加到configuration的mappedStatements缓存
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
String parameterType = context.getStringAttribute("parameterType");
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
。。。。。。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
上面是parseStatementNode()方法的实现(省略了大部分代码),它从context节点里面拿到id,sql脚本等信息,然后再调用builderAssistant.addMappedStatement()方法。(Mybatis的builder的类有点多, BuilderAssistant也是继承了BaseBuilder类的,内部有configuration的成员变量,这里最终会调用configuration的addMappedStatement()方法)
结语
好吧,Mybatis的初始化过程已经讲完了,它的作用其实就是为了初始化configuration,然后把它放到SqlSessionFactory里面,虽然初始过程的代码很复杂,但是Mybatis的总体的结构还是很清晰的。
这里只讲了第最开始部分,下一篇继续介绍SqlSession管理。
谢谢能够看到这里的各位小伙伴,本人能力有限,如果有什么说错的地方请指正。