入口为读取配置文件mybatis-config.xml,并拿着读取到的配置文件流通过SqlSessionFactoryBuilder去创建sqlSessionFactory,可参考上篇文章《【Mybatis源码探索】 — 开篇 • 搭建一个最简单的Mybatis框架》。代码如下:
@Before
public void init() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 1.读取mybatis配置文件创SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
SqlSessionFactoryBuilder其实相对比较简单,如若build(…)方法的参数为InputStream的话,其所走的源码如下:
所在类:SqlSessionFactoryBuilder
//拿着读取到的InputStream调用下面的build(...)方法创建SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//获取解析器 --- 这里注意一下:由上面的build(InputStream inputStream)可知environment 和properties都为null
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//build方法在下面,可以看到它的参数是Configuration
//也就是说parser.parse()这个方法就是获取Configuration对象
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
//拿着获取的Configuration对象new一个SqlSessionFactory对象
return new DefaultSqlSessionFactory(config);
}
从上面抽离出来的源码其实可以很清楚地看到SqlSessionFactoryBuilder主要的工作是:
(1)将读取到的mybatis-config.xml流文件解析成一个Configuration对象
(2)根据获取到的Configuration对象创建SqlSessionFactory对象
这里先不对Configuration对象和SqlSessionFactory对象进行具体的解析。
首先来看一下new XMLConfigBuilder(inputStream, environment, properties)
这句话到底做了什么,其源码如下:
所在类:XMLConfigBuilder
//该方法只是一个简单的有参构造函数,并且它实际上会调用到下面第二个方法来对属性进行赋值
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
//将InputStream封装到XPathParser里,并调用下面的private方法
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
//真正的对属性赋值
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//这里其实应该可以想到,2.1中的parser.parse()方法的目的就是要获取一个Configuration对象,而且该方法是一个空参方法,
//因此其实它获取到的Configuration对象就是这里new出来的Configuration对象
//并且这里需要提示的一点是:Configuration对象其实是一个单例对象,它只会在这里初始化一次,
//其他用到Configuration对象的地方如DefaultSqlSessionFactory,都是通过构造函数等方式将此处new出来的对象传递进去的
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
除了我在上面源码中注释的内容外,不知道大家会不会有这样的疑惑 :
看2.1中的代码可以知道,它先new了一个XMLConfigBuilder对象,然后调用该对象的parse()方法将
mybatis-config.xml流文件
解析成一个Configuration对象,其实Configuration对象完全可以作为XMLConfigBuilder对象的一个属性啊,为啥它还用了个super(new Configuration())
呢?
其实答案很简单,要我设计我应该也会做类似的设计,它就是用到了模版方法。XMLConfigBuilder的父类为BaseBuild,它里面真正封装了Configuration属性,并封装了一些解析文件的通用方法。BaseBuild的类继承关系图如下:
通过该图应该可以很容易的猜到,BaseBuild的这几个子类其实都是在解析配置文件,并将解析结果封装到BaseBuild的属性中,接下来会对其进行验证
。
接着来看一下parser.parse()方法,其源码如下:
所在类:XMLConfigBuilder
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//parser.evalNode("/configuration")的实际值就是mybatis-config.xml中节点内的xml
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//真正解析mybatis-config.xml中配置的内容
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties")); //解析properties标签
Properties settings = settingsAsProperties(root.evalNode("settings"));//解析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标签 --- >数据源在environments标签里
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mappers标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
从上面的源码可以看出:parse()方法就是拿着mybatis-config.xml中< configuration >节点内的xml内容,去调用parseConfiguration(XNode root) 方法去真正解析每一个xml标签里的内容。
联系2.1应该可以猜到,parseConfiguration这些方法肯定是将解析的内容,set到Configuration对象里 —>
接下来会对其进行验证。
由2.2.2中的源码可以看出其实mybatis-config.xml可配的标签还是挺多的,本篇文章只对environments标签和mappers的解析流程做一下具体的分析。
environmentsElement(…)方法的具体源码如下:
所在类:XMLConfigBuilder
private void environmentsElement(XNode context) throws Exception {
//这里的context就是mybatis-config.xml中节点内的xml
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
//child就是标签下的每一个标签的内容
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对象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//这里其实就可以验证前面所说的内容了 --- 即将解析到的mybatis-config.xml内容set到Configuration对象里
//将Environment对象set到Configuration对象里
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
简单来看一下DataSourceFactory的构建过程(其实TransactionFactory的构建过程和它基本一致):
所在类:XMLConfigBuilder
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
//获取到配置的数据源信息
Properties props = context.getChildrenAsProperties();
//拿着获取的type通过反射获取到数据源工厂
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
//将数据源信息set到数据源工厂里
factory.setProperties(props);
return factory; //返回数据源工厂
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
接着来看一下 mapperElement(…)方法,其源码如下:
所在类:XMLConfigBuilder
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果使用package标签进行扫描mapper.xml
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标签进行扫描mapper.xml
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
//获取到mapper.xml的流文件
InputStream inputStream = Resources.getResourceAsStream(resource);
//拿着mapper.xml的流文件、Configuration对象、mapper.xml的路径和Configuration对象中的sql碎片对象
//去构建XMLMapperBuilder 对象
//这里的XMLMapperBuilder在2.2.1中的图片里就已经见到过了,它就是专门用来解析mapper的Builder类
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析mapper.xml文件 --- 可以看到这里其实也是一个无参方法
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方式进行扫描
} 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.");
}
}
}
}
}
可以看出上面的源码就是对四种mapper配置方式解析的模版,本文只就resource方式进行分析(因为上篇文章我配置的是resource☺☺☺),其实一种明白了,其他三种也就可以很轻松的理解了。
mapperParser.parse()方法的源码如下
所在类:XMLMapperBuilder
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//解析mapper.xml--- 注意这里的parser.evalNode("/mapper")其实就是mapper.xml的具体内容了
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource); //标识该标签已经解析完成
bindMapperForNamespace(); //构建当前mapper.xml对应的Mapper接口
}
//下面的代码不细究了
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
可以看到这里的解析其实分了五步:分别为解析当前mapper.xml、解析获得当前mapper.xml对应的Mapper接口、解析待处理的结果、解析待处理的缓存引用、解析待处理的语句,本文接下来主要分析一下前两步。
(1)XMLMapperBuilder中解析mapper.xml的核心源码主要如下:
所在类:XMLMapperBuilder
//这里定义了解析mapper文件的模版
private void configurationElement(XNode context) {
try {
//获取到namespace ,如:com.nrsc.mybatis.mapper.TUserMapper
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//这里留意一下builderAssistant,它也在2.2.1中的图片里就已经见过了,它其实是用来帮助解析mapper.xml文件的
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));//解析标签
cacheElement(context.evalNode("cache"));//解析缓存标签
parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析parameterMap标签
resultMapElements(context.evalNodes("/mapper/resultMap")); //解析resultMap
sqlElement(context.evalNodes("/mapper/sql")); //解析sql片段
buildStatementFromContext(context.evalNodes("select|insert|update|delete")); //真正解析sql语句
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
//这里的list其实就是当前mapper.xml文件里的一个个select、insert、update、delete标签的内容
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
//解析select、insert、update、delete标签
buildStatementFromContext(list, null);
}
//循环遍历select、insert、update、delete标签组成的list集合,解析每一个标签
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//构建XMLStatementBuilder对象,用于解析select、insert、update、delete标签
//这里注意一下:XMLStatementBuilder也在2.2.1中的图片里就已经见过了,它是真正解析sql语句的一个builder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//真正去解析select、insert、update、delete标签
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
由上面的源码可以知道XMLMapperBuilder是用来解析当前mapper.xml的,但是select、insert、update、delete标签是在XMLStatementBuilder类里进行解析的。
(2)跟一下 XMLStatementBuilder中的parseStatementNode方法,其核心源码最主要如下:
所在类:XMLStatementBuilder
public void parseStatementNode() {
//此处省略n行,主要逻辑就是解析当前mapper.xml中的select、insert、update、delete标签,并将解析内容封装为一个个的对象
//拿着解析到的内容利用builderAssistant类将解析结果进行封装
//这里应该可以想到,还会将封装结果放到Configuration对象里
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
(3)再跟一下MapperBuilderAssistant的builderAssistant方法
builderAssistant其实有两个作用:
(1)将select、 insert、update、delete标签解析结果进行封装
(2)将封装后的对象set到Configuration对象里
代码不具体跟了,将断点跟踪过程用如下图进行展示一下:
这里要注意:
(1)select、 insert、update、delete一个个的标签被解析后封装的对象为MappedStatement对象
(2)这些MappedStatement对象都会被加到Configuration对象的一个map里 —mappedStatements
,该map的key其实就是namespace.方法名
或者说类的全额限定名.方法名
。如: com.nrsc.mybatis.mapper.TUserMapper.selectByPrimaryKey
(3)其实每一个MappedStatement对象里也都包含了Configuration对象 ,这一点也要格外注意。
(1)bindMapperForNamespace方法的源码如下:
所在类:XMLMapperBuilder
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {//获取当前mapper.xml绑定的Mapper接口的类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType); //解析当前mapper.xml对应的Mapper接口,并将其放入Configuration对象
}
}
}
}
(2)跟一下configuration.addMapper(boundType)方法,源码如下:
所在类:Configuration
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
注意:
这里的mapperRegistry其实是Configuration对象的一个属性,它在Configuration对象的初始化语句如下,
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
由此可知,其实MapperRegistry这个属性里也包含了当前的Configuration对象。
其实我觉得在这里一定要理清一个关系,就是Configuration对象里有MapperRegistry属性,MapperRegistry也有Configuration属性 --- 而且Configuration对象里的MapperRegistry是加了final修饰符的,Configuration对象在spring容器里是单例的。
这样的话,只要MapperRegistry对象的属性发生变化,通过Configuration对象都能获取到最新的变化结果。
(3) 接着跟一下mapperRegistry.addMapper(type)方法的源码:
所在类:MapperRegistry
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是MapperRegistry对象里的一个Map对象,用来保存所有的mapper.xml对应的Mapper接口的封装类
//这句话,其实就把当前mapper.xml对应的Mapper接口的封装类加入到Configuration对象了
knownMappers.put(type, new MapperProxyFactory<>(type));
//下面的代码不细究了
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
在这里看一下knownMappers属性,它在MapperRegistry对象里的声明源码如下:
所在类:MapperRegistry
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
由此可知,其实knownMappers的key并不是String,而是一个class类型,其实在Mybatis源码中,它就是一个个Mapper接口的class类型。而Value是MapperProxyFactory
对象 —> 即mapper代理工厂对像 —> 该对象非常重要!!!
其源码如下:
所在类:MapperProxyFactory
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
//唯一的构造函数,需要传进来一个class类型 ---> 即需要将Mapper接口的class类型传递进来
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
//拿着接口类型创建代理对象
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
//拿着接口类型创建代理对象
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
该对象会在我接下来的几篇文章里都会提到。
本篇文章主要介绍了Mybatis解析mybatis-config.xml文件的主要流程。着重分析了解析environments标签和mappers标签获取数据源、sql、Mapper接口的封装类 — MapperProxyFactory的流程。现将其总结如下: