欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Mybaits 源码解析 (六)----- 全网最详细:Select 语句的执行过程分析(上篇)(Mapper方法是如何调用到XML中的SQL的?)

程序员文章站 2023-10-28 23:08:04
上一篇我们分析了Mapper接口代理类的生成,本篇接着分析是如何调用到XML中的SQL 我们回顾一下MapperMethod 的execute方法 public Object execute(SqlSession sqlSession, Object[] args) { Object result; ......

上一篇我们分析了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;
}

该方法由数个步骤组成,这里总结一下:

  1. 创建 dynamiccontext
  2. 解析 sql 片段,并将解析结果存储到 dynamiccontext 中
  3. 解析 sql 语句,并构建 staticsqlsource
  4. 调用 staticsqlsource 的 getboundsql 获取 boundsql
  5. 将 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 的创建过程

Mybaits 源码解析 (六)----- 全网最详细:Select 语句的执行过程分析(上篇)(Mapper方法是如何调用到XML中的SQL的?)

 

我们看到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,最后就可以执行查询操作了。由于篇幅的原因,我们留在下一篇文章再来详细讲解