Mybaits 源码解析 (六)----- 全网最详细:Select 语句的执行过程分析(上篇)(Mapper方法是如何调用到XML中的SQL的?)
上一篇我们分析了mapper接口代理类的生成,本篇接着分析是如何调用到xml中的sql
我们回顾一下mappermethod 的execute方法
public object execute(sqlsession sqlsession, object[] args) { object result; // 根据 sql 类型执行相应的数据库操作 switch (command.gettype()) { case insert: { // 对用户传入的参数进行转换,下同 object param = method.convertargstosqlcommandparam(args); // 执行插入操作,rowcountresult 方法用于处理返回值 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); } else if (method.returnsmap()) { // 执行查询操作,并将结果封装在 map 中返回 result = executeformap(sqlsession, args); } else if (method.returnscursor()) { // 执行查询操作,并返回一个 cursor 对象 result = executeforcursor(sqlsession, args); } else { object param = method.convertargstosqlcommandparam(args); // 执行查询操作,并返回一个结果 result = sqlsession.selectone(command.getname(), param); } break; case flush: // 执行刷新操作 result = sqlsession.flushstatements(); break; default: throw new bindingexception("unknown execution method for: " + command.getname()); } return result; }
selectone 方法分析
本节选择分析 selectone 方法,主要是因为 selectone 在内部会调用 selectlist 方法。同时分析 selectone 方法等同于分析 selectlist 方法。代码如下
// 执行查询操作,并返回一个结果 result = sqlsession.selectone(command.getname(), param);
我们看到是通过sqlsession来执行查询的,并且传入的参数为command.getname()和param,也就是namespace.methodname(mapper.employeemapper.getall)和方法的运行参数。我们知道了,所有的数据库操作都是交给sqlsession来执行的,那我们就来看看sqlsession的方法
defaultsqlsession
public <t> t selectone(string statement, object parameter) { // 调用 selectlist 获取结果 list<t> list = this.<t>selectlist(statement, parameter); if (list.size() == 1) { // 返回结果 return list.get(0); } else if (list.size() > 1) { // 如果查询结果大于1则抛出异常 throw new toomanyresultsexception( "expected one result (or null) to be returned by selectone(), but found: " + list.size()); } else { return null; } }
如上,selectone 方法在内部调用 selectlist 了方法,并取 selectlist 返回值的第1个元素作为自己的返回值。如果 selectlist 返回的列表元素大于1,则抛出异常。下面我们来看看 selectlist 方法的实现。
defaultsqlsession
private final executor executor; public <e> list<e> selectlist(string statement, object parameter) { // 调用重载方法 return this.selectlist(statement, parameter, rowbounds.default); } public <e> list<e> selectlist(string statement, object parameter, rowbounds rowbounds) { try { // 通过mappedstatement的id获取 mappedstatement mappedstatement ms = configuration.getmappedstatement(statement); // 调用 executor 实现类中的 query 方法 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(); } }
我们之前创建defaultsqlsession的时候,是创建了一个executor的实例作为其属性的,我们看到通过mappedstatement的id获取 mappedstatement后,就交由executor去执行了
我们回顾一下前面的文章,executor的创建过程,代码如下
//创建一个执行器,默认是simple public executor newexecutor(transaction transaction, executortype executortype) { executortype = executortype == null ? defaultexecutortype : executortype; executortype = executortype == null ? executortype.simple : executortype; executor executor; //根据executortype来创建相应的执行器,configuration默认是simple if (executortype.batch == executortype) { executor = new batchexecutor(this, transaction); } else if (executortype.reuse == executortype) { executor = new reuseexecutor(this, transaction); } else { //创建simpleexecutor实例,并且包含configuration和transaction属性 executor = new simpleexecutor(this, transaction); } //如果要求缓存,生成另一种cachingexecutor,装饰者模式,默认都是返回cachingexecutor /** * 二级缓存开关配置示例 * <settings> * <setting name="cacheenabled" value="true"/> * </settings> */ if (cacheenabled) { //cachingexecutor使用装饰器模式,将executor的功能添加上了二级缓存的功能,二级缓存会单独文章来讲 executor = new cachingexecutor(executor); } //此处调用插件,通过插件可以改变executor行为,此处我们后面单独文章讲 executor = (executor) interceptorchain.pluginall(executor); return executor; }
executor包含了configuration和transaction,默认的执行器为simpleexecutor,如果开启了二级缓存(默认开启),则cachingexecutor会包装simpleexecutor,那么我们该看cachingexecutor的query方法了
cachingexecutor
public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception { // 获取 boundsql boundsql boundsql = ms.getboundsql(parameterobject); // 创建 cachekey cachekey key = createcachekey(ms, parameterobject, rowbounds, boundsql); // 调用重载方法 return query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); }
上面的代码用于获取 boundsql 对象,创建 cachekey 对象,然后再将这两个对象传给重载方法。cachekey 以及接下来即将出现的一二级缓存将会独立成文进行分析。
获取 boundsql
我们先来看看获取boundsql
// 获取 boundsql boundsql boundsql = ms.getboundsql(parameterobject);
调用了mappedstatement的getboundsql方法,并将运行时参数传入其中,我们大概的猜一下,这里是不是拼接sql语句呢,并将运行时参数设置到sql语句中?
我们都知道 sql 是配置在映射文件中的,但由于映射文件中的 sql 可能会包含占位符 #{},以及动态 sql 标签,比如 <if>、<where> 等。因此,我们并不能直接使用映射文件中配置的 sql。mybatis 会将映射文件中的 sql 解析成一组 sql 片段。我们需要对这一组片段进行解析,从每个片段对象中获取相应的内容。然后将这些内容组合起来即可得到一个完成的 sql 语句,这个完整的 sql 以及其他的一些信息最终会存储在 boundsql 对象中。下面我们来看一下 boundsql 类的成员变量信息,如下:
private final string sql; private final list<parametermapping> parametermappings; private final object parameterobject; private final map<string, object> additionalparameters; private final metaobject metaparameters;
下面用一个表格列举各个成员变量的含义。
变量名 | 类型 | 用途 |
---|---|---|
sql | string | 一个完整的 sql 语句,可能会包含问号 ? 占位符 |
parametermappings | list | 参数映射列表,sql 中的每个 #{xxx} 占位符都会被解析成相应的 parametermapping 对象 |
parameterobject | object | 运行时参数,即用户传入的参数,比如 article 对象,或是其他的参数 |
additionalparameters | map | 附加参数集合,用于存储一些额外的信息,比如 datebaseid 等 |
metaparameters | metaobject | additionalparameters 的元信息对象 |
接下来我们接着mappedstatement 的 getboundsql 方法,代码如下:
public boundsql getboundsql(object parameterobject) { // 调用 sqlsource 的 getboundsql 获取 boundsql,把method运行时参数传进去 boundsql boundsql = sqlsource.getboundsql(parameterobject);return boundsql; }
mappedstatement 的 getboundsql 在内部调用了 sqlsource 实现类的 getboundsql 方法,并把method运行时参数传进去,sqlsource 是一个接口,它有如下几个实现类:
- dynamicsqlsource
- rawsqlsource
- staticsqlsource
- providersqlsource
- velocitysqlsource
当 sql 配置中包含 ${}
(不是 #{})占位符,或者包含 <if>、<where> 等标签时,会被认为是动态 sql,此时使用 dynamicsqlsource 存储 sql 片段。否则,使用 rawsqlsource 存储 sql 配置信息。我们来看看dynamicsqlsource的getboundsql
dynamicsqlsource
public boundsql getboundsql(object parameterobject) { // 创建 dynamiccontext dynamiccontext context = new dynamiccontext(configuration, parameterobject); // 解析 sql 片段,并将解析结果存储到 dynamiccontext 中,这里会将${}替换成method对应的运行时参数,也会解析<if><where>等sqlnode rootsqlnode.apply(context); sqlsourcebuilder sqlsourceparser = new sqlsourcebuilder(configuration); class<?> parametertype = parameterobject == null ? object.class : parameterobject.getclass(); /* * 构建 staticsqlsource,在此过程中将 sql 语句中的占位符 #{} 替换为问号 ?, * 并为每个占位符构建相应的 parametermapping */ sqlsource sqlsource = sqlsourceparser.parse(context.getsql(), parametertype, context.getbindings()); // 调用 staticsqlsource 的 getboundsql 获取 boundsql boundsql boundsql = sqlsource.getboundsql(parameterobject); // 将 dynamiccontext 的 contextmap 中的内容拷贝到 boundsql 中 for (map.entry<string, object> entry : context.getbindings().entryset()) { boundsql.setadditionalparameter(entry.getkey(), entry.getvalue()); } return boundsql; }
该方法由数个步骤组成,这里总结一下:
- 创建 dynamiccontext
- 解析 sql 片段,并将解析结果存储到 dynamiccontext 中
- 解析 sql 语句,并构建 staticsqlsource
- 调用 staticsqlsource 的 getboundsql 获取 boundsql
- 将 dynamiccontext 的 contextmap 中的内容拷贝到 boundsql
dynamiccontext
dynamiccontext 是 sql 语句构建的上下文,每个 sql 片段解析完成后,都会将解析结果存入 dynamiccontext 中。待所有的 sql 片段解析完毕后,一条完整的 sql 语句就会出现在 dynamiccontext 对象中。
public class dynamiccontext { public static final string parameter_object_key = "_parameter"; public static final string database_id_key = "_databaseid"; //bindings 则用于存储一些额外的信息,比如运行时参数 private final contextmap bindings; //sqlbuilder 变量用于存放 sql 片段的解析结果 private final stringbuilder sqlbuilder = new stringbuilder(); public dynamiccontext(configuration configuration, object parameterobject) { // 创建 contextmap,并将运行时参数放入contextmap中 if (parameterobject != null && !(parameterobject instanceof map)) { metaobject metaobject = configuration.newmetaobject(parameterobject); bindings = new contextmap(metaobject); } else { bindings = new contextmap(null); } // 存放运行时参数 parameterobject 以及 databaseid bindings.put(parameter_object_key, parameterobject); bindings.put(database_id_key, configuration.getdatabaseid()); } public void bind(string name, object value) { this.bindings.put(name, value); } //拼接sql片段 public void appendsql(string sql) { this.sqlbuilder.append(sql); this.sqlbuilder.append(" "); } //得到sql字符串 public string getsql() { return this.sqlbuilder.tostring().trim(); } //继承hashmap static class contextmap extends hashmap<string, object> { private metaobject parametermetaobject; public contextmap(metaobject parametermetaobject) { this.parametermetaobject = parametermetaobject; } @override public object get(object key) { string strkey = (string) key; // 检查是否包含 strkey,若包含则直接返回 if (super.containskey(strkey)) { return super.get(strkey); } if (parametermetaobject != null) { // 从运行时参数中查找结果,这里会在${name}解析时,通过name获取运行时参数值,替换掉${name}字符串 return parametermetaobject.getvalue(strkey); } return null; } } // 省略部分代码 }
解析 sql 片段
接着我们来看看解析sql片段的逻辑
rootsqlnode.apply(context);
对于一个包含了 ${} 占位符,或 <if>、<where> 等标签的 sql,在解析的过程中,会被分解成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。在源码中,片段这个概念等价于 sql 节点,即 sqlnode。
statictextsqlnode 用于存储静态文本,textsqlnode 用于存储带有 ${} 占位符的文本,ifsqlnode 则用于存储 <if> 节点的内容。mixedsqlnode 内部维护了一个 sqlnode 集合,用于存储各种各样的 sqlnode。接下来,我将会对 mixedsqlnode 、statictextsqlnode、textsqlnode、ifsqlnode、wheresqlnode 以及 trimsqlnode 等进行分析
public class mixedsqlnode implements sqlnode { private final list<sqlnode> contents; public mixedsqlnode(list<sqlnode> contents) { this.contents = contents; } @override public boolean apply(dynamiccontext context) { // 遍历 sqlnode 集合 for (sqlnode sqlnode : contents) { // 调用 salnode 对象本身的 apply 方法解析 sql sqlnode.apply(context); } return true; } }
mixedsqlnode 可以看做是 sqlnode 实现类对象的容器,凡是实现了 sqlnode 接口的类都可以存储到 mixedsqlnode 中,包括它自己。mixedsqlnode 解析方法 apply 逻辑比较简单,即遍历 sqlnode 集合,并调用其他 sqlnode实现类对象的 apply 方法解析 sql。
statictextsqlnode
public class statictextsqlnode implements sqlnode { private final string text; public statictextsqlnode(string text) { this.text = text; } @override public boolean apply(dynamiccontext context) { //直接拼接当前sql片段的文本到dynamiccontext的sqlbuilder中 context.appendsql(text); return true; } }
statictextsqlnode 用于存储静态文本,直接将其存储的 sql 的文本值拼接到 dynamiccontext 的sqlbuilder中即可。下面分析一下 textsqlnode。
textsqlnode
public class textsqlnode implements sqlnode { private final string text; private final pattern injectionfilter; @override public boolean apply(dynamiccontext context) { // 创建 ${} 占位符解析器 generictokenparser parser = createparser(new bindingtokenparser(context, injectionfilter)); // 解析 ${} 占位符,通过ongl 从用户传入的参数中获取结果,替换text中的${} 占位符 // 并将解析结果的文本拼接到dynamiccontext的sqlbuilder中 context.appendsql(parser.parse(text)); return true; } private generictokenparser createparser(tokenhandler handler) { // 创建占位符解析器 return new generictokenparser("${", "}", handler); } private static class bindingtokenparser implements tokenhandler { private dynamiccontext context; private pattern injectionfilter; public bindingtokenparser(dynamiccontext context, pattern injectionfilter) { this.context = context; this.injectionfilter = injectionfilter; } @override public string handletoken(string content) { object parameter = context.getbindings().get("_parameter"); if (parameter == null) { context.getbindings().put("value", null); } else if (simpletyperegistry.issimpletype(parameter.getclass())) { context.getbindings().put("value", parameter); } // 通过 ongl 从用户传入的参数中获取结果 object value = ognlcache.getvalue(content, context.getbindings()); string srtvalue = (value == null ? "" : string.valueof(value)); // 通过正则表达式检测 srtvalue 有效性 checkinjection(srtvalue); return srtvalue; } } }
generictokenparser 是一个通用的标记解析器,用于解析形如 ${name},#{id} 等标记。此时是解析 ${name}的形式,从运行时参数的map中获取到key为name的值,直接用运行时参数替换掉 ${name}字符串,将替换后的text字符串拼接到dynamiccontext的sqlbuilder中
举个例子吧,比喻我们有如下sql
select * from user where name = '${name}' and id= ${id}
假如我们传的参数 map中name值为 chenhao,id为1,那么该 sql 最终会被解析成如下的结果:
select * from user where name = 'chenhao' and id= 1
很明显这种直接拼接值很容易造成sql注入,假如我们传入的参数为name值为 chenhao'; drop table user;# ,解析得到的结果为
select * from user where name = 'chenhao'; drop table user;#'
由于传入的参数没有经过转义,最终导致了一条 sql 被恶意参数拼接成了两条 sql。这就是为什么我们不应该在 sql 语句中是用 ${} 占位符,风险太大。接着我们来看看ifsqlnode
ifsqlnode
public class ifsqlnode implements sqlnode { private final expressionevaluator evaluator; private final string test; private final sqlnode contents; public ifsqlnode(sqlnode contents, string test) { this.test = test; this.contents = contents; this.evaluator = new expressionevaluator(); } @override public boolean apply(dynamiccontext context) { // 通过 ongl 评估 test 表达式的结果 if (evaluator.evaluateboolean(test, context.getbindings())) { // 若 test 表达式中的条件成立,则调用其子节点节点的 apply 方法进行解析 // 如果是静态sql节点,则会直接拼接到dynamiccontext中 contents.apply(context); return true; } return false; } }
ifsqlnode 对应的是 <if test='xxx'> 节点,首先是通过 ongl 检测 test 表达式是否为 true,如果为 true,则调用其子节点的 apply 方法继续进行解析。如果子节点是静态sql节点,则子节点的文本值会直接拼接到dynamiccontext中
好了,其他的sqlnode我就不一一分析了,大家有兴趣的可以去看看
解析 #{} 占位符
经过前面的解析,我们已经能从 dynamiccontext 获取到完整的 sql 语句了。但这并不意味着解析过程就结束了,因为当前的 sql 语句中还有一种占位符没有处理,即 #{}。与 ${} 占位符的处理方式不同,mybatis 并不会直接将 #{} 占位符替换为相应的参数值,而是将其替换成?。其解析是在如下代码中实现的
sqlsource sqlsource = sqlsourceparser.parse(context.getsql(), parametertype, context.getbindings());
我们看到将前面解析过的sql字符串和运行时参数的map作为参数,我们来看看parse方法
public sqlsource parse(string originalsql, class<?> parametertype, map<string, object> additionalparameters) { // 创建 #{} 占位符处理器 parametermappingtokenhandler handler = new parametermappingtokenhandler(configuration, parametertype, additionalparameters); // 创建 #{} 占位符解析器 generictokenparser parser = new generictokenparser("#{", "}", handler); // 解析 #{} 占位符,并返回解析结果字符串 string sql = parser.parse(originalsql); // 封装解析结果到 staticsqlsource 中,并返回,因为所有的动态参数都已经解析了,可以封装成一个静态的sqlsource return new staticsqlsource(configuration, sql, handler.getparametermappings()); } public string handletoken(string content) { // 获取 content 的对应的 parametermapping parametermappings.add(buildparametermapping(content)); // 返回 ? return "?"; }
我们看到将sql中的 #{} 占位符替换成"?",并且将对应的参数转化成parametermapping 对象,通过buildparametermapping 完成,最后创建一个staticsqlsource,将sql字符串和parametermappings为参数传入,返回这个staticsqlsource
private parametermapping buildparametermapping(string content) { /* * 将#{xxx} 占位符中的内容解析成 map。 * #{age,javatype=int,jdbctype=numeric,typehandler=mytypehandler} * 上面占位符中的内容最终会被解析成如下的结果: * { * "property": "age", * "typehandler": "mytypehandler", * "jdbctype": "numeric", * "javatype": "int" * } */ map<string, string> propertiesmap = parseparametermapping(content); string property = propertiesmap.get("property"); class<?> propertytype; // metaparameters 为 dynamiccontext 成员变量 bindings 的元信息对象 if (metaparameters.hasgetter(property)) { propertytype = metaparameters.getgettertype(property); /* * parametertype 是运行时参数的类型。如果用户传入的是单个参数,比如 employe 对象,此时 * parametertype 为 employe.class。如果用户传入的多个参数,比如 [id = 1, author = "chenhao"], * mybatis 会使用 parammap 封装这些参数,此时 parametertype 为 parammap.class。 */ } else if (typehandlerregistry.hastypehandler(parametertype)) { propertytype = parametertype; } else if (jdbctype.cursor.name().equals(propertiesmap.get("jdbctype"))) { propertytype = java.sql.resultset.class; } else if (property == null || map.class.isassignablefrom(parametertype)) { propertytype = object.class; } else { /* * 代码逻辑走到此分支中,表明 parametertype 是一个自定义的类, * 比如 employe,此时为该类创建一个元信息对象 */ metaclass metaclass = metaclass.forclass(parametertype, configuration.getreflectorfactory()); // 检测参数对象有没有与 property 想对应的 getter 方法 if (metaclass.hasgetter(property)) { // 获取成员变量的类型 propertytype = metaclass.getgettertype(property); } else { propertytype = object.class; } } parametermapping.builder builder = new parametermapping.builder(configuration, property, propertytype); // 将 propertytype 赋值给 javatype class<?> javatype = propertytype; string typehandleralias = null; // 遍历 propertiesmap for (map.entry<string, string> entry : propertiesmap.entryset()) { string name = entry.getkey(); string value = entry.getvalue(); if ("javatype".equals(name)) { // 如果用户明确配置了 javatype,则以用户的配置为准 javatype = resolveclass(value); builder.javatype(javatype); } else if ("jdbctype".equals(name)) { // 解析 jdbctype builder.jdbctype(resolvejdbctype(value)); } else if ("mode".equals(name)) {...} else if ("numericscale".equals(name)) {...} else if ("resultmap".equals(name)) {...} else if ("typehandler".equals(name)) { typehandleralias = value; } else if ("jdbctypename".equals(name)) {...} else if ("property".equals(name)) {...} else if ("expression".equals(name)) { throw new builderexception("expression based parameters are not supported yet"); } else { throw new builderexception("an invalid property '" + name + "' was found in mapping #{" + content + "}. valid properties are " + parameterproperties); } } if (typehandleralias != null) { builder.typehandler(resolvetypehandler(javatype, typehandleralias)); } // 构建 parametermapping 对象 return builder.build(); }
sql 中的 #{name, ...} 占位符被替换成了问号 ?。#{name, ...} 也被解析成了一个 parametermapping 对象。我们再来看一下 staticsqlsource 的创建过程。如下:
public class staticsqlsource implements sqlsource { private final string sql; private final list<parametermapping> parametermappings; private final configuration configuration; public staticsqlsource(configuration configuration, string sql) { this(configuration, sql, null); } public staticsqlsource(configuration configuration, string sql, list<parametermapping> parametermappings) { this.sql = sql; this.parametermappings = parametermappings; this.configuration = configuration; } @override public boundsql getboundsql(object parameterobject) { // 创建 boundsql 对象 return new boundsql(configuration, sql, parametermappings, parameterobject); } }
最后我们通过创建的staticsqlsource就可以获取boundsql对象了,并传入运行时参数
boundsql boundsql = sqlsource.getboundsql(parameterobject);
也就是调用上面创建的staticsqlsource 中的getboundsql方法,这是简单的 return new boundsql(configuration, sql, parametermappings, parameterobject); ,接着看看boundsql
public class boundsql { private string sql; private list<parametermapping> parametermappings; private object parameterobject; private map<string, object> additionalparameters; private metaobject metaparameters; public boundsql(configuration configuration, string sql, list<parametermapping> parametermappings, object parameterobject) { this.sql = sql; this.parametermappings = parametermappings; this.parameterobject = parameterobject; this.additionalparameters = new hashmap(); this.metaparameters = configuration.newmetaobject(this.additionalparameters); } public string getsql() { return this.sql; } //略 }
我们看到只是做简单的赋值。boundsql中包含了sql,#{}解析成的parametermappings,还有运行时参数parameterobject。好了,sql解析我们就介绍这么多。我们先回顾一下我们代码是从哪里开始的
cachingexecutor
1 public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler) throws sqlexception { 2 // 获取 boundsql 3 boundsql boundsql = ms.getboundsql(parameterobject); 4 // 创建 cachekey 5 cachekey key = createcachekey(ms, parameterobject, rowbounds, boundsql); 6 // 调用重载方法 7 return query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); 8 }
如上,我们刚才都是分析的第三行代码,获取到了boundsql,cachekey 和二级缓存有关,我们留在下一篇文章单独来讲,接着我们看第七行重载方法 query
public <e> list<e> query(mappedstatement ms, object parameterobject, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { // 从 mappedstatement 中获取缓存 cache cache = ms.getcache(); // 若映射文件中未配置缓存或参照缓存,此时 cache = null if (cache != null) { flushcacheifrequired(ms); if (ms.isusecache() && resulthandler == null) { ensurenooutparams(ms, boundsql); list<e> list = (list<e>) tcm.getobject(cache, key); if (list == null) { // 若缓存未命中,则调用被装饰类的 query 方法,也就是simpleexecutor的query方法 list = delegate.<e>query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); tcm.putobject(cache, key, list); // issue #578 and #116 } return list; } } // 调用被装饰类的 query 方法,这里的delegate我们知道应该是simpleexecutor return delegate.<e>query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); }
上面的代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方法。被装饰类为simpleexecutor,而simpleexecutor继承baseexecutor,那我们来看看 baseexecutor 的query方法
baseexecutor
public <e> list<e> query(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, cachekey key, boundsql boundsql) throws sqlexception { if (closed) { throw new executorexception("executor was closed."); } if (querystack == 0 && ms.isflushcacherequired()) { clearlocalcache(); } list<e> list; try { querystack++; // 从一级缓存中获取缓存项,一级缓存我们也下一篇文章单独讲 list = resulthandler == null ? (list<e>) localcache.getobject(key) : null; if (list != null) { 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(); } deferredloads.clear(); if (configuration.getlocalcachescope() == localcachescope.statement) { clearlocalcache(); } } return list; }
从一级缓存中查找查询结果。若缓存未命中,再向数据库进行查询。至此我们明白了一级二级缓存的大概思路,先从二级缓存中查找,若未命中二级缓存,再从一级缓存中查找,若未命中一级缓存,再从数据库查询数据,那我们来看看是怎么从数据库查询的
baseexecutor
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 进行查询 list = doquery(ms, parameter, rowbounds, resulthandler, boundsql); } finally { // 移除占位符 localcache.removeobject(key); } // 缓存查询结果 localcache.putobject(key, list); if (ms.getstatementtype() == statementtype.callable) { localoutputparametercache.putobject(key, parameter); } return list; }
调用了doquery方法进行查询,最后将查询结果放入一级缓存,我们来看看doquery,在simpleexecutor中
simpleexecutor
public <e> list<e> doquery(mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, boundsql boundsql) throws sqlexception { statement stmt = null; try { configuration configuration = ms.getconfiguration(); // 创建 statementhandler statementhandler handler = configuration.newstatementhandler(wrapper, ms, parameter, rowbounds, resulthandler, boundsql); // 创建 statement stmt = preparestatement(handler, ms.getstatementlog()); // 执行查询操作 return handler.<e>query(stmt, resulthandler); } finally { // 关闭 statement closestatement(stmt); } }
我们先来看看第一步创建statementhandler
创建statementhandler
statementhandler有什么作用呢?通过这个对象获取statement对象,然后填充运行时参数,最后调用query完成查询。我们来看看其创建过程
public statementhandler newstatementhandler(executor executor, mappedstatement mappedstatement, object parameterobject, rowbounds rowbounds, resulthandler resulthandler, boundsql boundsql) { // 创建具有路由功能的 statementhandler statementhandler statementhandler = new routingstatementhandler(executor, mappedstatement, parameterobject, rowbounds, resulthandler, boundsql); // 应用插件到 statementhandler 上 statementhandler = (statementhandler) interceptorchain.pluginall(statementhandler); return statementhandler; }
我们看看routingstatementhandler的构造方法
public class routingstatementhandler implements statementhandler { private final statementhandler delegate; public routingstatementhandler(executor executor, mappedstatement ms, object parameter, rowbounds rowbounds, resulthandler resulthandler, boundsql boundsql) { // 根据 statementtype 创建不同的 statementhandler switch (ms.getstatementtype()) { case statement: delegate = new simplestatementhandler(executor, ms, parameter, rowbounds, resulthandler, boundsql); break; case prepared: delegate = new preparedstatementhandler(executor, ms, parameter, rowbounds, resulthandler, boundsql); break; case callable: delegate = new callablestatementhandler(executor, ms, parameter, rowbounds, resulthandler, boundsql); break; default: throw new executorexception("unknown statement type: " + ms.getstatementtype()); } } }
routingstatementhandler 的构造方法会根据 mappedstatement 中的 statementtype 变量创建不同的 statementhandler 实现类。那statementtype 是什么呢?我们还要回顾一下mappedstatement 的创建过程
我们看到statementtype 的默认类型为prepared,这里将会创建preparedstatementhandler。
接着我们看下面一行代码preparestatement,
创建 statement
创建 statement 在 stmt = preparestatement(handler, ms.getstatementlog()); 这句代码,那我们跟进去看看
private statement preparestatement(statementhandler handler, log statementlog) throws sqlexception { statement stmt; // 获取数据库连接 connection connection = getconnection(statementlog); // 创建 statement, stmt = handler.prepare(connection, transaction.gettimeout()); // 为 statement 设置参数 handler.parameterize(stmt); return stmt; }
在上面的代码中我们终于看到了和jdbc相关的内容了,创建完statement,最后就可以执行查询操作了。由于篇幅的原因,我们留在下一篇文章再来详细讲解
上一篇: 不结晶的蜂蜜可以吃吗,纯正蜂蜜会结晶吗