1.mybatis原理及启动过程
这部分可以参考
mybatis的实现及mybatis在spring和springboot中的实现_流光亦流连的博客-CSDN博客_springboot中mybatis如何实现数据注入schema
为了方便后续的讲解,把mybatis启动过程的代码再拿出来写一遍:
public class testStart {
@Test
public void testStart() throws IOException {
//1.mybatis 主配置文件,文件写在根目录下
String config = "mybatis.xml";
//2. 读取配置文件,读取里面的数据库配置信息,及mapper接口中sql语句的所在的类
InputStream in = Resources.getResourceAsStream(config);
//3. 创建 SqlSessionFactory 对象, , 目的是获取 Sql Session
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4. 获取 SqlSession,SqlSession 能执行 sql 语句
SqlSession session = factory.openSession();
//5. 执行 SqlSession 的 selectList() 参数中name space +id 匹配相应sql语句
List<Student> studentList =session.selectList("com.cry.dao.StudentDao.selectStudents");
//6. 循环输出查询结果
studentList.forEach( student -> System.out.println(student.toString()));
//7. 关闭 SqlSession ,释放资源
session.close();
}
2.源码解析
2.1 sqlSessionFactory部分
根据上面的7步,首先来看1-3步,从代码本身来看,大意是根据你写的mybatis.xml文件,将文件转换为输入流,再根据这个输入流构建sqlSessionFactory
那么首先来分析这3步的源码
找到SqlsessionFactory.build(Reader reader)方法,并找到其重载方法,下面对其重载方法进行分析。
public SqlSessionFactory build(Reader reader) {
return this.build((Reader)reader, (String)null, (Properties)null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
//根据mybatis.xml文件生成XMLConfiguration
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//生成了一个SqlSessionFactory对象,注意参数中的parse
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
可以看到try块中,XMLConfigBuilder是一个用于对我们的XML配置文件进行解析的对象,其parse方法用于解析xml文件的属性。
在解析处xml文件中相关的属性后,将解析后的属性封装为Configuration对象用于构建sqlSessionFactory
那么我们来看看XMLConfigBuilder是怎么解析XML文件的
public Configuration parse() {
//parsed的初始值为false,第二次调用此方法,进入if
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
//核心方法
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
重点关注parseConfiguration方法,方法具体的代码如下:
private void parseConfiguration(XNode root) {
//在root中已经包含了xml文件中配置的所有的属性值
try {
//以下内容用于解析configuration标签中配置的各项属性
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
//别名
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
//数据库
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
//mapper接口映射文件
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
看到里面的字段是不是有些眼熟?再让我们来看看xml文件里面写了啥
<?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>
<properties resource="db.properties"/>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<typeAliases>
<typeAlias type="com.cry.domain.Student" alias="student"/>
</typeAliases>
<!-- 配置 mybatis 环境 -->
<environments default="mysql">
<!--id: 数据源的名称 -->
<environment id="mysql">
<!-- 配置事务类型:使用 JDBC 事务(使用 Connection 的提交和回滚) -->
<transactionManager type="JDBC"/>
<!-- 数据源 dataSource :创建数据库 Connection 对象
type: POOLED 使用数据库的连接池-->
<dataSource type="POOLED">
<!-- 连接数据库的四个要素 -->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 告诉 mybatis 要执行的 sql 语句的位置 -->
<mapper resource="com/cry/dao/StudentDao.xml"/>
</mappers>
</configuration>
是不是感觉出奇的一致?
其实parse方法的核心思想就是将configuration标签作为一个对象,configuration标签中子标签的内容作为对象的属性进行封装,也就是说将xml文件中的内容解析成一个configuration对象进行返回。有了这个概念,再读这部分代码就很轻松了。
这里面比较重要的两个属性是environments(对应数据库配置)和mapper(对应mapper接口的路径),因此只对这两部分进行源码分析。
environments部分:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (this.environment == null) {
this.environment = context.getStringAttribute("default");
}
Iterator var2 = context.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
//取出id对应的数据
String id = child.getStringAttribute("id");
//判断id对应的数据库是否为特定的数据库
if (this.isSpecifiedEnvironment(id)) {
//获取事务类型
TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
//获取数据库的相关信息,详见dataSourceElement源码
DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
//获取数据源
DataSource dataSource = dsFactory.getDataSource();
//根据数据库类型,事务类型,数据源生成builder
Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
//生成的builder注入到configuration的environment属性中去(对应父子结点)
this.configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//获取连接池的类型
String type = context.getStringAttribute("type");
//驱动类,url,username,password
Properties props = context.getChildrenAsProperties();
//通过连接池类型,用反射造出工厂类
DataSourceFactory factory = (DataSourceFactory)this.resolveClass(type).newInstance();
//将四要素注入到生成的工厂类中
factory.setProperties(props);
return factory;
} else {
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
}
指的注意的是,数据库连接的四要素,我写在了db.properties文件中,该文件在xml文件中的<properties>标签中进行引入,已经被解析为properties对象,因此这里可以直接注入。
可以看到在解析enviroments配置时,可以根据你的数据连接池,数据库类型,四要素,生成数据库的工厂类,依据此工厂类,生成数据源,并将其作为属性注入到configuration对象中
mapper部分:
在分析mapper部分的解析过程之前,我们首先要知道mapper接口在xml文件中有4种引入方式,分别是
这里我是通过第一种方式引入的,就按第一种方式对应的代码来进行分析
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
//根据resource路径生成对应的XMLMapperBuilder
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
}
重点关注parse()方法
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
//对应mapper标签中url路径对应的文件内容,此时已经可以拿到里面全部的内容了
this.configurationElement(this.parser.evalNode("/mapper"));
this.configuration.addLoadedResource(this.resource);
this.bindMapperForNamespace();
}
this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace != null && !namespace.equals("")) {
this.builderAssistant.setCurrentNamespace(namespace);
this.cacheRefElement(context.evalNode("cache-ref"));
this.cacheElement(context.evalNode("cache"));
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
this.sqlElement(context.evalNodes("/mapper/sql"));
//重要:这里就是构建sql语句的地方,将带有s/i/u/d标签的标签中的内用封装为集合
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} else {
throw new BuilderException("Mapper's namespace cannot be empty");
}
} catch (Exception var3) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
}
}
其实通过configurationElement已经可以看到,对mapper.xml解析的思路是跟configuration是一样的,即将mapper.xml中<mapper>标签作为一个对象,将其中所有的子标签作为mapper对象的属性进行填充。
至此,mapper对象中封装了我们写的mapper.xml文件中的全部内容,而mapper又将作为configuration中的一个依赖进行注入,因此configuration对象中也同样会存储着解析mapper.xml文件后得到的mapper对象。
而生成的sqlSessionFactory的方法中注入了configuration对象,因此,整个mybatis.xml文件中解析的配置将全部被sqlSessionFactory获取。
2.2 dao层与数据库的交互
在刚才解析mapper.xml文件的方法中有一个重要的方法,buildStatementFromContext
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
Iterator var3 = list.iterator();
while(var3.hasNext()) {
XNode context = (XNode)var3.next();
//遍历每条sql标签生成对应的sql语句转换器对象
XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
try {
//解析标签中的相关内容,并将namespace+方法id生成key取出对应的sql语句
statementParser.parseStatementNode();
} catch (IncompleteElementException var7) {
this.configuration.addIncompleteStatement(statementParser);
}
}
}
//applyCurrentNamespace()将当前的namespace+.+id拼成key值
keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
Object keyGenerator;
if (this.configuration.hasKeyGenerator(keyStatementId)) {
//取出对应的sql语句
keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//根据xml文件中各部分解析的值,以及对应的sql语句,调用addMappedStatement进行statement的创建
this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
以下面这个mapper.xml文件为例
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace :必须有值,自定义的唯一字符串
推荐使用: dao 接口的全限定名称
namespace 与 id组合成唯一的key作为 Map <String, MapperStatement> 的 key使用的
-->
<mapper namespace="com.cry.dao.StudentDao">
<!--
<select>: 查询数据, 标签中必须是 select 语句
id: sql 语句的自定义名称,推荐使用 dao 接口中方法名称,
使用名称表示要执行的 sql 语句
resultType: 查询语句的返回结果数据类型,使用全限定类名
-->
<select id="selectStudents" resultType="com.cry.domain.Student">
<!-- 要执行的 sql 语句 -->
select id,name,email,age from student
</select>
<insert id="insert" >
insert into student(id,name,email,age) values(#{id},#{name},#{email},#{age})
</insert>
</mapper>
它的原理是将select/insert/update/delete标签中配置的全部内容解析为一个mapperStatement对象
将mapper中的id与namespace一起拼成一个字符串,与解析出来的mapperStatement对象进行一一映射,存储在mybatis中的一个map对象中。
这也就是为什么mapper。xml文件中的id和namespace组成的值必须唯一的原因,因为key重复时,无法完成dao层接口的方法与mapperStatement的一一映射。
也就是说有了这个一一映射的关系,我们在执行dao层相应的方法时,代理类拦截此方法后,就可以找到这个方法对应的mapperStatement对象来决定执行的sql语句,输入的参数值,返回值类型等。
由于这个mapperStatement对象最终将存储在configuration中,所以sqlSession是可以获取mapperStament的,而通过openSession()方法生成的sqlSession对象同样也可以拿到这个mapperStatement配置。
至此,第4步生成的sqlSession已经获取你在mybatis.xml和mapper.xml中的所有配置,这也就是后面dao层方法执行以及数据库与java直接交互的基础。
3.sql语句的执行与方法的返回值
这部分重点对应第5步,首先来看看selectList的代码
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
//对应前面的addMapperStatement,在这里拿到对应的statement对象
MappedStatement ms = this.configuration.getMappedStatement(statement);
//调用query方法拿到对应的sql语句
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
可以看到首先sqlSession获取了刚才解析的mapperStatement,并调用executor的query方法完成了查询功能。
那么再来看query方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//ms中包含着之前解析select/insert/delete/update标签生成的所有内容,此处取出其中的sql语句,同时注入参数
BoundSql boundSql = ms.getBoundSql(parameter);
//生成缓存
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
//获取confinguration中解析的所有配置
Configuration configuration = ms.getConfiguration();
//根据ms中的内容生成statementHandler
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//这里生成了preparedStatement
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
//connection连接数据库
Connection connection = this.getConnection(statementLog);
//调用prepare()方法拿到statement对象
Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
可以看到,在查询过程中,首先将参数注入到sql语句中生成了boundSql对象,这个对象对应着我们查询时输入的sql语句。
在查询时首先检查缓存中是否存在之前的查询结果,如果存在,直接返回结果,如果不存在,则继续调用query方法进行查询,query方法中核心方法为doQuery
可以看到在doQuery方法中,sqlsession获取了mybatis.xml中配置的configuration,然后根据configuration生成了StatementHandler,进而生成了prepareStatement,在生成prepareStatement的是否已经建立了与数据库的连接,由于之前数据的配置已经解析为enviroments对象注入到configuration中,因此是可以与本地配置的数据库建立连接的。
最后只需要调用handler对象的query方法,将生成的prepareStatement对象和处理结果映射的resultHandler对象一起传入,就可以完成对数据库的查询和结果的返回了。
var9 = handler.query(stmt, resultHandler);
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement)statement;
//执行对应的sql语句
ps.execute();
//封装返回的结果
return this.resultSetHandler.handleResultSets(ps);
}
至此查询过程完毕,且结果映射为java对象。
第6步为对结果的输出,用于查看返回的结果。
第7步为关闭连接,节省资源。
至此mybatis源码部分的分析完毕。