注:本系列源码分析基于mybatis 3.5.6,源码的gitee仓库仓库地址:funcy/mybatis.
在上一篇文章中,我们提供了一个mybatis使用demo,本文就基于该demo进行分析。
1. SqlSessionFactory的构建:SqlSessionFactoryBuilder#build(...)
从本节开始,就开始正式分析 mybatis 源码了,我们从SqlSessionFactory的构建入手:
SqlSessionFactory factory = builder.build(inputStream);
对应方法为SqlSessionFactoryBuilder#build(InputStream):
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
/**
* 具体的解析操作
*/
public SqlSessionFactory build(InputStream inputStream, String environment,
Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 1. parser.parse():解析配置文件
// 2. build(...):构建 SqlSessionFactory
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
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
这部分的代码比较简单,主要功能在 SqlSessionFactoryBuilder#build(InputStream, String, Properties)方法中,其实这个方法就包含两个操作:
parser.parse():解析配置文件build(...):返回SqlSessionFactory,最终返回的是SqlSessionFactory类型是DefaultSqlSessionFactory
我们主要来看看解析配置文件的操作,跟进方法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;
}
在XMLConfigBuilder#parse中又调用了parseConfiguration(...)方法,传入的值为/configuration,从这里开始就是在解析mybatis配置文件了,我们在mybatis-config.xml文件中指定的根节点就是configuration.继续进入XMLConfigBuilder#parseConfiguration方法:
/**
* 解析`mybatis`配置文件的根节点
*/
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 environments,数据源也是在这里获取的
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 sql 的 xml 文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
对比下 mybatis 配置文件里支持的节点(可参考xml配置):
可以看到,这里正是解析mybatis配置文件的各节点。
这里解析的内容比较多,我们主要关注两个:
- 解析数据源(
datasource) - 解析映射器(
mappers)
2. 解析数据源
配置文件中 ,数据源的配置在environments节点下,对应的方法为XMLConfigBuilder#environmentsElement:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 解析事务管理器
TransactionFactory txFactory = transactionManagerElement(
child.evalNode("transactionManager"));
// 获取数据源
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
继续跟进XMLConfigBuilder#dataSourceElement方法:
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
// 处理节点下的配置
Properties props = context.getChildrenAsProperties();
// 得到数据源工厂
DataSourceFactory factory = (DataSourceFactory) resolveClass(type)
.getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
这个解析还是处理 xml 文件,会获取dataSource节点下的所有配置,然后将这些配置转化为Properties对象,得到的内容如下:
然后根据dataSource节点配置的type值初始化对应的数据源工厂,这里配置的的type值为POOLED:
<dataSource type="POOLED">
对应的DataSourceFactory为org.apache.ibatis.datasource.pooled.PooledDataSourceFactory,最终得到的对象为:
这里注意到PooledDataSourceFactory,还有个UnpooledDataSource,这两者的区别就是连接池与非连接池的区别了,这里就不多作分析了。
再回到XMLConfigBuilder#environmentsElement方法,得到的数据源会设置到configuration中,这一步得到的configuration如下:
可以看到,environment 已经有值了,数据源也在其中了。
3 解析映射器
解析映射器的方法为XMLConfigBuilder#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");
// 处理 resource 加载
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();
// 处理 url 方式的加载
} 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("...");
}
}
}
}
}
这里需要说明下,mybatis加载sql映射语句有几种方式(内容来自于 mybatis文档-映射器):
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
上述内容中介绍了4种加载sql映射语句,这里我们重点关注两种方式:
- 使用相对于类路径的资源引用(
resource加载) - 将包内的映射器接口实现全部注册为映射器(
package加载)
3.1 处理 resource 加载方式
处理 resource 加载方式的代码如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 处理包的加载方式
if ("package".equals(child.getName())) {
...
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 处理 resource 加载
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();
}
...
}
}
}
}
从代码来看,mybatis会把resource路径读取为输入流,然后使用xml解析来处理输入流,这与前面解析mybatis-config.xml配置文件的方式一致。我们进入XMLMapperBuilder#parse:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper节点
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
我们先来看看mapper节点的解析,进入XMLMapperBuilder#configurationElement方法:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
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"));
// 解析 sql 语句
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);
}
}
这里就是解析mapper.xml文件里的标签了,关于这个文件内容的配置,可以参考 mybatis xml映射器.
我们重点关注sql的解析,方法入口为XMLMapperBuilder#buildStatementFromContext(List<XNode>),一路跟下去,最终到了XMLStatementBuilder#parseStatementNode:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser
= new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
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内容
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(
context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这个方法看着长,实际上还是在解析sql语句的内容,这块内容就不多说了。
解析到xml文件里的内容后,会把参数传给builderAssistant.addMappedStatement(...)方法,这个方法的参数真不是一般的多,我们简单地看下他最终做了什么:
public MappedStatement addMappedStatement(...) {
// 省略参数组装
...
// 将解析到的sql语句(select,insert,update,delete)添加到configuration中
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
这一步是将statement添加到configuration了,这里的statement类型为MappedStatement,这是对sql内容的封装,得到的statement为
具体的sql语句在MappedStatement.sqlSource中:
sql的解析是在XMLScriptBuilder#parseScriptNode中处理的,这里就不多说了。
添加到configuration后,Configuration#mappedStatements内容如下:
Configuration#mappedStatements是一个map,key 是"包名.类名.方法名",value 是 MappedStatement,key表示了xml中唯一的方法,从这里可以看出,xml 中的方法并不支持重载。
到这里,mapper.xml文件就解析好了,那么UserMapper.java何时处理呢?我们再回到XMLMapperBuilder#parse方法:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
...
// 绑定 Namespace
bindMapperForNamespace();
}
...
}
进入XMLMapperBuilder#bindMapperForNamespace方法:
private void bindMapperForNamespace() {
// 拿到 namespace,这里的 namespace 就是 mapper.java 文件的 "包名.类名"
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
// 代码中忽略了报错信息,也就是说如果仅有 mapper.xml 文件但没 mapper.java,代码也不报错
}
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
// 继续看这里的操作
configuration.addMapper(boundType);
}
}
}
继续跟进MapperRegistry#addMapper方法:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
最终调用的是MapperRegistry#addMapper方法,关于这个方法,我们在分析处理 package 加载方式一节再分析。
3.2 处理 package 加载方式
让我们回到XMLConfigBuilder#mapperElement,处理 package 加载方式的方法如下:
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);
}
...
}
}
}
进入Configuration#addMappers(java.lang.String)方法:
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
继续进入MapperRegistry#addMappers(String)
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class<?> superType) {
// 根据包名获取包下的所有类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 处理添加操作
addMapper(mapperClass);
}
}
这一块操作很清晰,就是获取包名下的所有类,然后遍历,对每一个类逐一调用MapperRegistry#addMapper(Class)方法。在分析处理 resource 加载方式时,从xml文件中拿到namespace后,也会调用MapperRegistry#addMapper(Class)方法,我们跟进去看看这个方法做了什么:
public <T> void addMapper(Class<T> type) {
// 判断是否为接口
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加到 knownMappers 中,创建的是 MapperProxyFactory 对象
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 在这里会解析 xml 文件,当然了,解析前会先判断是否已经解析过了,解析过就不再解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
在MapperRegistry中,有一个Map:Map<Class<?>, MapperProxyFactory<?>> knownMappers,该Map用来存放MapperProxyFactory,我们来看看MapperProxyFactory有啥:
public class MapperProxyFactory<T> {
// Mapper.java
private final Class<T> mapperInterface;
// 方法调用缓存
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
...
}
MapperProxyFactory从名字来看,是Mapper 代理工厂,Mapper实例在这里产生,其中有一个成员变量mapperInterface保存Mapper的类型,另一个成员变量methodCache存放方法缓存。
我们再回到MapperRegistry#addMapper(Class)方法,这个方法关键地方有两处:
- 添加
MapperProxyFactory 对象到knownMappers中 - 生成
MapperAnnotationBuilder并解析
关于第1点,就是根据传入的Mapper类型生成MapperProxyFactory对象,然后保存到MapperRegistry的knownMappers,该结构是一个Map,关于MapperProxyFactory对象的妙用,后面会再分析。
我们重点关注MapperAnnotationBuilder的解析,方法为MapperAnnotationBuilder#parse:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 加载xml文件
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
// 解析方法上的注解
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
// 解析 @select 注解
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
这一块会解析xml文件,同时也会解析方法上的注解。在实际项目中,mybatis 的注解方式使用并不多,本文就不分析了,我们重点还是来看xml解析,进入MapperAnnotationBuilder#loadXmlResource:
private void loadXmlResource() {
// 判断xml是否解析过,如果已解析过,就不再解析
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 获取xml文件路径
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
// 如果没找到,就不处理
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
xmlResource, configuration.getSqlFragments(), type.getName());
// 解析xml
xmlParser.parse();
}
}
}
这个方法所做工作并不多,先是判断xml是否解析过了,解析过了就不再处理了,然后将类名转换为xml路径名,如Mapper的名称为org.apache.ibatis.demo.mapper.UserMapper,转换后得到的xml路径名为org/apache/ibatis/demo/mapper/UserMapper.xml,也就是说,这种方式下,Mapper与对应的xml要放在同一包下才能被解析到。
具体解析xml的方法为XMLMapperBuilder#parse,在前面分析 resource 加载方式时,使用的也是这个方法来解析xml的,这里就不于分析了。
最后,我们来小结下这两种加载方式的区别:resource 加载方式 会先解析xml,然后由xml文件中的namespace加载Mapper.java;而 package 加载方式会根据Mapper.java找到对应的xml然后进行解析。
4. 总结
本文主要分析了mybatis解析配置文件的流程,其中重点分析了数据源的解析及映射器的解析。
- 数据源在配置文件的
environments节点下配置,解析后得到的类为Environment,其中包含了数据源的配置; - 映射器的配置共有4种,本文重点介绍了两种:
resource加载方式与package加载方式,resource加载方式 会先解析xml,然后由xml文件中的namespace加载Mapper.java;而package加载方式会根据Mapper.java找到对应的xml然后进行解析; mapper.xml文件中的select|insert|delete|update语句会被解析成一个个MappedStatement,MappedStatement中的成员变量id的值为包名.类名.方法名,Mapper.java类会被包装成MapperProxyFactory,其成员变量mapperInterface就是Mapper的类型;- 最终配置文件会被解析为
Configuration类,这个类包含了mybatis配置文件的所有内容。
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。