一、概述
之前我们说了mybatis中的动态代理模式,我们知道了在mybatis中dao接口的实现类是通过动态代理(jdk)创建的,创建出来的实现类里面通过调用sqlsession去执行jdbc操作,这就和我们自己用sqlsession操作后续的流程是一样的了,就是下面这个流程。
也就是sqlsession就顺着那个excutor的路子下去了,这就把这个动态代理的类和我们之前说的那些组件关联起来了。
OK,我们此时再来看一下那段核心代码:
// 读取配置文件转化为流
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
//这⾥不调⽤SqlSession的api,⽽是获得了接⼝对象,调⽤接⼝中的⽅法,底层再用sqlsession的方法。使用JDK动态代理产生代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
至此为止,我们解决了那段动态代理的代码了,剩下的就是上面那三行了,也就是这段:
// 读取配置文件转化为流
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
我们来看这段代码,我们看到他做的就是通过io流读取sqlMapConfig.xml这个核心配置文件,我们知道这里配置了所有的核心配置,并且我们在核心配置文件里面还注册了mapper.xml的路径,也就是这个。
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
我们在核心配置里面注册了mapper.xml的路径,所以他读取核心配置也就能读取到mapper.xml文件,所以顺藤摸瓜就能把所有的配置文件一起读取到。
OK我们下面就来看看具体是怎么个读取流程。
二、读取流程
// 读取流程
// 获取文件,并且读取转化为输入流
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 输入流作为参数,我们就跟进去这个build方法
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建流程
SqlSession sqlSession = factory.openSession();
首先明确一点,那种什么什么Builder对象都是建造者模式的体现,而他调用的build方法就是真正执行的地方。
开始看源码
我们点进去build方法看一下。来到一个重载方法:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
跟进这个重载方法:
/**
* @Author levi
* @Param * @param inputStream 参数是这个配置文件的输入流
* @param environment 这里传参进来是Null,没设置东西
* @param properties 这里传参进来是Null,没设置东西
* @return org.apache.ibatis.session.SqlSessionFactory
**/
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建XMLConfigBuilder 看名字像是一个xml配置文件的建造者,传进去参数是文件的流 其余那两个我们前面看到是Null,所以不做分析 入口1 我们就来分析一下这个入口做了什么
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 入口2
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.
}
}
}
1、入口1:创建XMLConfigBuilder
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
// 是否解析过了,解析过就是false
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
我们看到他就是创建出一个Configuration对象,以及给这个对象设置了一些值,并且把一些变量给了初始值。
基于此我们看到入口1就是创建了一个前期准备的类。所以我们知道重点就落在了入口2这里,我们点进去看看入口2、
2、入口2:正式开始工作
入口2的代码有两步:
1、parser.parse()
2、build(parser.parse())
我们就分两步来看看这两步到底干啥了。
2.1、parser.parse()
我们点进去看看这个方法。
public Configuration parse() {
// 解析过了就是false,之前还没解析过呢,上面我们设置为false,下面开始解析
if (parsed) {
// 解析过了抛出只能解析一次的异常
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 走过判断,设置为true,也就是解析过了,下次再解析,就抛出上面的异常
parsed = true;
// 这是xpath的语法,他解析xml,从头节点configuration开始解析,所以这里就是解析xml,
parseConfiguration(parser.evalNode("/configuration"));
// 经过下面的封装,最后返回这个分装好的configuration对象。
return configuration;
}
于是parseConfiguration()方法·就成了最后的落脚点。
我们点进去看看。
2.1,1、解析核心配置
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
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mapper文件,这里注意,他并且解析了mapper.xml文件,因为我们在配置文件中,有这个配置
/**
<mappers>
<!--<package name="com.lwq.dao"/>-->
<mapper resource="UserMapper.xml"/>
</mappers>
我们这里读取这个节点其实就能找到mapper.xml文件了。也就能去解析这个文件了。
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们看到上面使用xpath解析了很多xml的标签,这些标签的属性都是在我们的sqlMapconfig.xml文件中配置好的。所以这里其实就是一个个的标签一点点的解析出来。我们随便点开一个看看,就以environmentsElement(root.evalNode(“environments”));为例点进去看看。
// 解析xml文件,读取environments标签
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中
configuration.setEnvironment(environmentBuilder.build());
break;
}
}
}
}
我们看到最后读取完了,最后封装到了configuration里面,所以所有的属性最后都封装到了这里configuration这个类里面,这里是汇总一切的地方,之前我们说过了。
OK我们这一步就得到了configuration对象结束了。
2.1.2、解析mapper.xml文件
我们上面也看到了他同时在解析sqlMapConfig.xml的时候也还处理了mapper.xml文件,也就是这段代码:
// 解析mapper文件,这里注意,他并且解析了mapper.xml文件,我们点进去看看
mapperElement(root.evalNode("mappers"));
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果你在配置文件里面是以package配置的就走这里
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);
try(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);
// 这里也解析了xml,其实这里就是解析mapper.xml的入口,上面那个分支也是,我就拿这个示例了,点进去看看。
try(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.");
}
}
}
}
}
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 从根标签开始解析
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
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"));
// 这里又是一个Builfd的入口,我们跟进去看看
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);
}
}
最后点进去方法来到这里
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 上面就是解析一系列内容最后封装这个对象,也就是我们以前一直说的MappedStatement ,也就是最后mapper.xml文件会被
// 解析,把内部属性全部封装到对象MappedStatement 中,这样他就存在于jvm中,以后用的地方就能直接拿来用。
MappedStatement statement = statementBuilder.build();
// 并且MappedStatement 还封装在了configuration对象里面,至此所有的文件都被解析完成了。
configuration.addMappedStatement(statement);
return statement;
}
这是第一个大路子,接下来我们走入第二个路子。
2.2、build(parser.parse())
点进去来到这里:
public SqlSessionFactory build(Configuration config) {
//. 我们看到这里以前面执行解析文件封装出来的Configuration 为参数,这里啥都有,创建了一个DefaultSqlSessionFactory对象返回了。
return new DefaultSqlSessionFactory(config);
}
至此,我们得到了一个拥有配置文件里面一切内容的Configuration 对象,和一个DefaultSqlSessionFactory,而DefaultSqlSessionFactory这个一看就是一个工厂类,看名字也知道是创建SqlSession的工厂,于是来到了下一步。SqlSession sqlSession = factory.openSession();
三、创建流程
关于创建流程其实就是这段代码:
SqlSession sqlSession = factory.openSession();
factory就是我们之前读取流程的DefaultSqlSessionFactory,于是我们此时很清楚,我们点进去openSession这个方法即可。而且我们上面看到他创建的是DefaultSqlSessionFactory,所以我们跟进去DefaultSqlSessionFactory这个选择。
@Override
public SqlSession openSession() {
/**
* 这里面三个参数,configuration就是前面初始化的时候封装的配置
* 第二个level:是事务的隔离级别,这里我们没设置,就是null,使用默认的RR
* 第三个autoCommit:设置自动事务提交,这里设置为false,不自动提交
*/
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
再点进去:
/**
* 进⼊penSessionFromDataSource。
* ExecutorType 为Executor的类型,TransactionIsolationLevel为事务隔离级别, autoCommit是否开启事务
* penSession的多个重载⽅法可以指定获得的SeqSession的Executor类型和事务的处理
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//.获取配置文件里面的环境environment 标签,这里面有数据库连接信息
final Environment environment = configuration.getEnvironment();
// 事务相关
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据configuration里面的类型创建executor,我们看到了,sqlsession其实也是最后使用excutor 来往下调用的,
// 这个我们前面结构分析说过了。Executor等那几个组件是通过configuration创建出来的。
// 所以他这里就创建出来了。
final Executor executor = configuration.newExecutor(tx, execType);
// 创建返回一个DefaultSqlSession,里面封装了创建好的configuration, executor, autoCommit
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
// 创建sqlsession的时候就把Executor 也加进去了
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
我们看到最后创建了一个创建返回一个DefaultSqlSession,这里面此时封装了一些事务属性以及configuration这个对象,里面包含了一切我们前面解析xml时候的东西,至此创建出来了sqlsession,并且里面有配置文件的所有东西,并且里面还包含了excutor对象,excutor里面还有各种handler,后面就是我们前面说的过程了,就是这里就前后接上了。
至此我们经过读取和创建两个流程,解析完了执行sql之前的所有准备工作。此时就通过解析xml拿到了所有的执行的内容包括sql和参数封装到了configuration和MappedStatement ,并且通过configuration创建出来了Executor 执行器,一起封装在了DefaultSqlSession(sqlSession的子类)中,此时用户角度的sqlsession就拥有了执行的能力。而他此时还是一个接口,下面就开始通过我们的动态代理开始创建实现类,然后就可以开始执行下面的代码了。但是执行的时候,我们是要执行真正的sql的,其实这个真正的sql在解析的时候就解析了sql和参数封装好了,后面拿到直接传入JDBC进行执行。
四、思辨
我们看了一下上面的代码,发现很多地方都是工厂模式和建造者模式的使用,这些设计模式的使用使得代码的结构很优秀,解耦还是扩展都很好。
再有一个就是我们看到Mybatis使用了xpath解析了xml文件,其中各种api的使用我们以后再工作的时候如果要解析xml,可以来抄一抄这里配置文件的解析操作,这也是学源码的一个作用吧。
至于xpath,我看到糊涂工具包里面也封装了一波,下面是地址,向所有致力于开源的工作者们致以敬意。
https://hutool.cn/docs/#/core/%E5%B7%A5%E5%85%B7%E7%B1%BB/XML%E5%B7%A5%E5%85%B7-XmlUtil