一、debug源码环境准备
1、下载mybatis源码
调试mybatis源码理解mybatis工作原理,首先需要下载mybatis源码!
下载地址:https://github.com/mybatis/mybatis-3
带中文注释的源码: https://github.com/tuguangquan/mybatis
2、下载mybatis-parent源码并编译
**注意:**下载的mybatis-parent版本要和mybatis源文件pom.xml 版本一致。
下载地址:https://github.com/mybatis/parent
切换到下载的mybatis-parent目录,执行:
mvn clean install
切换到下载的mybatis源码目录,执行:
mvn clean
mvn install -Dmaven.test.skip=true
上面的步骤进行完毕后,就可以使用idea导入mybatis源码了!
3、调整配置(选做)
在pom.xml的节点下增加如下配置,读取classpath下的配置文件!
<resource>
<directory>src/main/java</directory>
<includes>
<!--properties的配置文件会和编译后的class文件放在一起-->
<include>**/*.properties</include>
</includes>
</resource>
<resource>
<!--加载配置的资源-->
<directory>src/main/resources</directory>
</resource>
注释掉pom.xml中的pdf插件()
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pdf-plugin</artifactId>
</plugin>
-->
如果执行过程中有报错,可以根据报错信息注释掉相应的依赖即可!
4、编写测试代码
public static void main(String[] args) throws IOException {
// 通过字节流读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建SqlSessionFacory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = factory.openSession();
// 获取动态代理产生的mapper接口对象
MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
// 调用接口方法
Blog blog = mapper.selectBlog(1);
System.out.println("blog:"+blog);
}
二、整体流程说明
mybatis运行流程大致分为以下两步:
第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象(configuration)。
①、解析mybatis-config.xml信息
②、解析xxxMapper.xml信息
③、解析xxxMapper.xml配置文件中的每个标签信息(比如)
第二,就是执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互。
①、先获取mapper接口的代理对象
②、通过代理对象调用方法,执行操作。
所以下面就按照这样的步骤来分析!见下面的 【三】和【四】
三、解析xml配置文件
解析xml配置文件也分为两步:
** 第一:**解析全局配置文件mybatis-config.xml
** 第二:**解析与我们的mapper接口对应的xxxxMapper.xml文件,解析完毕后会将每一个xxxMapper.xml中的解析结果保存到Configuration对象的
mappedStatements
(Map结构)中,并且将每一个xxxMapper.xml信息都保存到knownMappers
(Map结构)中。最后将解析的配置文件信息整合到configuration类中!所有解析xml配置文件的最终结果就是得到一个
configuration
对象。
public class Configuration {
// 封装了所有的xxXMapper.xml配置文件中解析出的sql信息
protected final Map<String, MappedStatement> mappedStatements;
}
Ⅰ、前期准备
为了更好的理解这个解析流程,这里先贴出探究源码的过程中会用到的内容(测试代码、配置文件、解析流程图等)。
1)、测试代码
public static void main(String[] args) throws IOException {
// 通过字节流读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建SqlSessionFacory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = factory.openSession();
// 获取动态代理产生的mapper接口对象
MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
// 调用接口方法
Blog blog = mapper.selectBlog(1);
System.out.println("blog:"+blog);
}
2)、mybatis-config.xml和mapper.mxl配置文件
①、mybatis-config.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="jdbc.properties">
<property name="password" value="root"></property>
</properties>
<!-- 指定默认执行器类型
<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
-->
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/MyBlogMapper.xml"/>
<!--<mapper class="bat.ke.qq.com.mapper.BlogMapper"></mapper>-->
</mappers>
</configuration>
②、MyBlogMapper.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">
<mapper namespace="org.apache.ibatis.demo.mapper.MyBlogMapper">
<!-- <cache eviction="LRU" type="bat.ke.qq.com.cache.MybatisRedisCache"/>-->
<select id="selectBlog" resultType="org.apache.ibatis.demo.entity.Blog">
select * from Blog where id = ${id}
</select>
<insert id="insertBlog" parameterType="org.apache.ibatis.demo.entity.Blog">
insert into Blog (id,username,context) values (#{id}, #{username}, #{context})
</insert>
</mapper>
3)、解析配置文件流程图
这里为了下面看代码容易理解,先贴上解析配置文件的流程图!当把整个流程过完以后再回头来看这个流程图就非常清晰了!
Ⅱ、解析过程探究
1)、解析mybatis-config.xml文件
由SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);代码debug进去
①、build()
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//解析mybatis-config.xml
// 委托XMLConfigBuilder来解析xml文件,并构建
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 进入
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.
}
}
}
②、build(parser.parse())
// 解析配置文件信息
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析mybatis-config.xml配置文件顶层节点 <configuration>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
③、解析mybatis-config.xml配置文件核心代码
/**
* 解析配置mybatis-config.xml的顶层节点(即:<configuration>),
* 这里会解析出<configuration>标签下的所有一级子标签
* @param root 顶层节点
*/
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//1.properties
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//6.设置
settingsElement(settings);
// 环境信息
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器,从mybatis-config.xml配置文件中的<configration>标签中解析<mappers>标签
// 这里就是解析Mapper.xml文件的地方,下面分析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
到这里,解析mybatis-config.xml配置文件的流程就完毕了,解析完毕后会将mybatis-config.xml配置文件信息封装为一个Configuration对象返回!
2)、解析xxxMapper.xml文件
说明:
这里解析
xxxMapper.xml
文件,解析完毕后,会将xxxMapper.xml
自身的信息添加到一个名为knownMappers
的map集合中。
mybatis-config.xml文件中我们一定会写一个叫做的标签,这个标签中的
<mapper>
节点存放了我们对数据库进行操作的SQL语句。首先在看源码之前,我们先回忆一下我们在mapper标签内通常会怎样进行配置,通常有如下几种配置方式
<!-- 注意 mapper节点中,可以使用resource/url/class三种方式获取mapper-->
<mappers>
<!-- 通过配置文件路径 -->
<mapper resource="mapper/DemoMapper.xml" ></mapper>
<!-- 通过Java全限定类名 -->
<mapper class="com.mybatistest.TestMapper"/>
<!-- 通过url 通常是mapper不在本地时用 -->
<mapper url=""/>
<!-- 通过包名 -->
<package name="com.mybatistest"/>
</mappers>
①、解析xxxMapper.xml
/**
* 扫描配置mybatis-config.xml文件中的<mappers>节点,根据不同的配置方式,将该节点下指定的所有
* mapper接口添加到一个Map集合中
* @param parent
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//10.0自动扫描包下所有映射器
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
//将包下所有类加入到mapper(一个Map集合,Map<Class<?>, MapperProxyFactory<?>>)
// 这里其实就是循环的调用addMapper()方法
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) {
//10.1使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 【重要】解析mapper.xml配置文件中的标签,比如:<select/>、<update/>等
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用绝对url路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比较复杂,调用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 【重要】解析mapper.xml配置文件中的标签,比如:<select/>、<update/>等
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java类名
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 【重要】直接把这个映射加入配置类中(一个Map集合,Map<Class<?>, MapperProxyFactory<?>>),源码在下面的 【②】
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
②、将解析到的xxxMapper.xml信息添加到名为knownMappers的Map中
/**
* 看一下如何添加一个映射
* @param type
* @param <T>
*/
public class MapperRegistry {
// 将已经添加的映射都放入HashMap,一个mapper接口就对应着一个key--value键值对
// key:接口的全限定名,value: mapper接口对应的MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
//mapper必须是接口!才会添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重复添加了,报错
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 【添加】:将解析到的每一个mapper.xml添加到map集合中,
// key: mapper接口的全限定名,
// value:根据mapper接口创建的MapperProxyFactory对象
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
上面两步的作用是:举个例子,假如我在mybatis-config.xml中有如下配置:
<mappers> <mapper resource="mapper/MyBlogMapper.xml"/> </mappers>
那么在上面两步就是,找到 节点下的所有节点并解析他们,将解析后的mapper的全限定名作为key,new一个mappre相关的MapperProxyFactory作为value,然后put到knownMappers中。
==注意:==上面代码还有一句
mapperParser.parse();
非常重要,我们将在下面做解析!
3)、解析xxxMapper.xml中的子标签
接上面的
mapperParser.parse();
代码继续debug进去,这里就是解析每一个mapper.xml文件中的标签,比如:、等等。解析完成后,每一个被正确解析的子标签(、 、 、 )都会被放入一个名为mappedStatements
的Map集合中!
1、mapperParser.parse();
public void parse() {
//如果没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
//【重要】解析mapper文件节点(主要)
configurationElement(parser.evalNode("/mapper"));
//标记一下,已经加载过了
configuration.addLoadedResource(resource);
//绑定映射器到namespace
bindMapperForNamespace();
}
// 重新解析之前解析不了的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
2、configurationElement()
private void configurationElement(XNode context) {
try {
//1.配置namespace,检查namespace
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//2.配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已经废弃,老式风格的参数映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//5.配置resultMap(高级功能)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定义可重用的 SQL 代码段)
sqlElement(context.evalNodes("/mapper/sql"));
//7.【重要】配置select|insert|update|delete TODO
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
3、buildStatementFromContext()
// 配置select|insert|update|delete
private void buildStatementFromContext(List<XNode> list) {
//调用构建语句
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
4、parseStatementNode()
// 解析Statement
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//如果databaseId不匹配,退出
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//============================================================================
// 这里省略很多代码。。。
// 这里的代码主要作用就是:假如mapper.xml中有一个select语句如下:
// <select id="selectBlog" resultType="org.apache.ibatis.demo.entity.Blog">
// select * from Blog where id = ${id}
// </select>
// 这里相当于是解析这条<select>标签中的各种属性,返回结果集,是select类型还是update类型还是 // delete类型呀、是否要缓存呀,参数类型,超时时间呀 等等。
//============================================================================
//【重要】又去调助手类,添加到MappedStatement中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
5、addMappedStatement()
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加上namespace前缀
id = applyCurrentNamespace(id, false);
//是否是select语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//又是建造者模式
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);
//1.参数映射
setStatementParameterMap(parameterMap, parameterType, statementBuilder);
//2.结果映射
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
MappedStatement statement = statementBuilder.build();
//建造好调用configuration.addMappedStatement
//比如mapper.xml中有一条select 语句,这里会将这条select语句解析后放入到MappedStatement中(MappedStatement是一个Map)
configuration.addMappedStatement(statement);
return statement;
}
4)、总结
上面的解析XML配置文件的代码繁琐但不复杂,里面主要也就是对xml的节点进行解析 。
总结一下大致流程就是:
①、首先是解析mybatis-config.xml配置文件信息。
②、然后是解析mybatis-config.xml配置文件中的节点下的每一个,然后将解析到的信息以接口的全限定名作为 key, mapper接口对应的
MapperProxyFactory
作为 value,放入到一个Map集合中(knownMappers)。③、然后解析每一个mapper.xml里面的子节点(比如、、、),解析这些节点里面的每一个属性,做校验等,解析后的信息保存在
MappedStatement
中。④、然后将这些节点按照 接口的全限定名 + 方法名作为 key,**节点解析后的信息(MappedStatement)**作为 value 放入到
configuration
对象的mappedStatements(Map)属性中。
说明: 这里理一下 Configuration 、 knownMappers 、 mappedStatements之间的关系,如下:
public class Configuration {
// 环境信息
protected Environment environment;
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 存放mapper.xml解析后的一个个sql信息,key: 为mapper接口全限定名 + 方法名
protected final Map<String, MappedStatement> mappedStatements;
}
public class MapperRegistry {
// 配置信息
private final Configuration config;
// 存放一个个的mapper.xml信息,key:为mapper接口的全限定名
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
// 构造方法
public MapperRegistry(Configuration config) {
this.config = config;
}
}
到这里mybatis解析配置文件的流程就完了!
四、执行SQL
上面第一步解析了我们的mybatis-config.xml和mapper.xml配置文件并封装为configuration对象后,即可通过获取
SqlSession
来调用接口的方法进行查询了,代码如下!// 创建SqlSessionFacory,这步会解析xml配置文件 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); // 获取sqlSession SqlSession sqlSession = factory.openSession(); // 获取mapper的动态代理对象 MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class); // 执行查询方法 Blog blog = mapper.selectBlog(1);
Ⅰ、动态代理创建mapper接口的代理对象
跟着 MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
一路debug下去,会发现如下代码:(动态代理,投鞭断流)
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 通过传入的接口的Class对象,去knownMappers中获取Mapper接口相关信息(mapperProxyFactory)
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过mapperProxyFactory创建代理对象,并返回
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
public T newInstance(SqlSession sqlSession) {
// 传入mapper接口信息,创建mapperProxy对象
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 调用下面的方法,返回代理对象
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 用JDK自带的动态代理生成映射器
// 这里要注意:mapperProxy中组合了一个mapper接口,这里是为mapperProxy创建代理对象,代理对象里的方法实现,其实就是MapperProxy里的invoke方法,每次调用接口的方法都会调用到 MapperProxy 里的invoke方法
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
上面的代码便是创建mapper接口代理的代码过程,首先会通过传入的mapper的Class信息去
knownMappers
中获取到一个MapperProxyFactory
,然后调用MapperProxyFactory的newInstance
方法,在newInstance
方法中通过传入mapper接口信息,sqlSession信息,methodCache创建一个mapperProxy
。最后使用JDK代理为mapperProxy
创建代理对象(Proxy.newProxyInstance()
)!
Ⅱ、调用mapper接口方法
1)、代理对象的方法调用
当调用mapper接口中的方法时,其实时调用的代理对象中的对应方法!以debug的方式进入方法调用处!会发现进入了
MapperProxy
的invoke()
方法中!
// 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
// 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是Object的方法(toString、hashCode等)则直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 从这里进去
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
2)、代理对象核心方法
从上面进去以后会发现最终进入了
MapperMethod
的execute()
方法,该方法就是mapper接口方法的实现。在这里判断操作类型(增删改查),获取到要执行的sql,获取sql参数,替换占位符,处理返回结果集等
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
// 处理查询
case SELECT:
//如果有结果处理器
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
//如果结果有多条记录
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
//如果结果是map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//否则就是一条记录
Object param = method.convertArgsToSqlCommandParam(args);
// 我们的示例中只有一条记录,所以从这里跟进去
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
3)、调用SqlSession的查询方法
这里会根据上面的返回结果集条件判断(是否有多条返回值、返回结果集是否是Map等)选择不同的查询方法,但核心的就只有两个方法:一个是
selectOne()
,一个是selectList()
!在selectOne()
里面其实也是调用了selectList()
方法在做查询。
3.1、sqlSession.selectOne()
//核心selectOne
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
// 特别需要注意的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
//而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
3.2、selectList()
// 核心selectList
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 根据statement id找到对应的 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 转而用执行器来查询结果,注意这里传入的ResultHandler是null
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
3.3 Executor简单说明
Executor是一个接口,该接口的实现以及继承 关系图 如下:
可以看出
BaseExecutor
以及CachingExecutor
直接继承了Executor,BaseExecutor
类下有三个子类。这里对这几个执行器做一个简单的说明!**Executor接口:**该接口主要提供了update、query方法及事物相关的方法接口 !
CachingExecutor: CachingExecutor 从内存中获取数据, 在查找数据库前先查找缓存,若没有找到的话调用delegate从数据库查询,并将查询结果存入缓存中 。该执行器主要是针对于mybatis的二级缓存(需要在配置中开启二级缓存才可用)
BaseExecutor: 抽象类BaseExecutor采用模板方法设计模式,具体的实现在其子类:BatchExecutor、ReuseExecutor和SimpleExecutor
**SimpleExecutor:**简单执行器,每次执行SQL需要预编译SQL语句。
BatchExecutor:批处理执行器,只针对修改操作的SQL语句预编译一次,并且需要手动刷新SQL执行才生效
**ReuseExecutor:**可重用执行器,同一SQL语句执行只需要预编译一次SQL语句。
3.4、executor二级缓存查询逻辑
在上面3.2源码中执行了
executor.query()
方法,调用执行器的查询方法,我这里开启了二级缓存,所以会走到缓存执行器的query()方法。
// CachingExecutor里的query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取sql信息
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用本类的另一个query方法,如下
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// CachingExecutor里的query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
//默认情况下是没有开启缓存的(二级缓存).要开启二级缓存
//简单的说,就是先查CacheKey,查不到再委托给实际的执行器去查
if (cache != null) {
// 如果需要则刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 先通过事务缓存管理器,在二级缓存中取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
// 如果从二级缓存中没有取到数据,则调用委托的执行器对象去执行查询,这里的被委托对象可以是:
// simpleExecutor、ReuseExecutor、BatchExecutor。默认是simpleExecutor执行器
// 这里使用的是装饰设计模式
if (list == null) {
// 被委托的对象执行查询方法(这里被委托的对象是BaseExecutor执行器)
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果二级缓存没有值,则调用委托对象(BaseExecutor)的查询方法,查询一级缓存。
3.5、executor一级缓存查询逻辑
在上面的二级缓存没有查询到结果以后,会委托baseExecutor执行器去一级缓存查询。
// baseExecutor的query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//如果已经关闭,报错
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清除局部缓存
clearLocalCache();
}
List<E> list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
// 【查询一级缓存】先根据cachekey从一级缓存localCache去查,一级缓存实质就是从一个Map中拿
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//若查到localCache缓存,处理localOutputParameterCache
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 【从数据库查询】如果一级缓存查询不到数据,则从数据库查
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//清空堆栈
queryStack--;
}
if (queryStack == 0) {
//延迟加载队列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
//清空延迟加载队列
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
//如果是STATEMENT,清本地缓存
clearLocalCache();
}
}
return list;
}
从一级缓存中get
public class PerpetualCache implements Cache {
private final Map<Object, Object> cache = new HashMap<>();
@Override
public Object getObject(Object key) {
return cache.get(key);
}
}
3.6、一级缓存查询不到,从数据库查询
上面的从一级缓存中如果没有查询到结果,则直接调用BaseExecutor的子类执行器去数据库查询!
// BaseExecutor里的 queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//先向缓存中放入占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 执行doQuery()方法从数据库查询【这里执行的是BaseExecutor子类的doQuery(),默认是SimpleExecutor】
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//最后删除占位符
localCache.removeObject(key);
}
//加入缓存
localCache.putObject(key, list);
//如果是存储过程,OUT参数也加入缓存
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
3.7、SimpleExecutor.doQuery查询数据库
// SimpleExecutor里的doQuery()
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 这里就是基础的构建prepareStatement,执行查询操作了
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
执行查询(或其他操作)完毕后,然后再按照一定的条件或逻辑放入缓存或情况缓存!
Ⅲ、总结
上面分析的执行SQL的逻辑用简单的话描述:
①、首先通过 sqlSession.getMapper(MyBlogMapper.class)
创建对应mapper接口的代理对象
②、调用mapper代理对象的方法执行查询(或其他操作)【其中最核心的就是调用sqlSession的API】
③、在执行mapper代理对象的方法中,执行查询的时候会首先查找二级缓存(如果开启了二级缓存),如果命中则返回缓存结果。
④、如果二级缓存没有命中,则调用委托执行器(BaseExecutor)的方法去查询一级缓存,如果命中则返回缓存结果
⑤、如果一级缓存没有命中,则调用BaseExecutor
执行器的子类执行器(默认是 SimpleExecutor
)的方法从数据库查询!
⑥、将查询到的结果按照一定的规则和条件该写缓存的写缓存,然后返回!
其他:
1、 一级缓存作用域
MyBatis中的一级缓存,是默认开启且无法关闭的,一级缓存默认的作用域是一个SqlSession,解释一下,就是当SqlSession被构建了之后,缓存就存在了,只要这个SqlSession不关闭,这个缓存就会一直存在,换言之,只要SqlSession不关闭,那么这个SqlSession处理的同一条SQL就不会被调用两次,只有当会话结束了之后,这个缓存才会一并被释放。
2、 MyBatis 四大核心对象
ParameterHandler:处理SQL的参数对象
ResultSetHandler:处理SQL的返回结果集
StatementHandler:数据库的处理对象,用于执行SQL语句
Executor:MyBatis的执行器,用于执行增删改查操作
常见问题:
1、缓存失效条件?
2、为什么mybatis和spring进行整合的时候一级缓存会失效?
3、一级缓存的作用域?生命周期?
4、sqlSession生命周期?