Mybatis源码详解系列(三)--从Mapper接口开始看Mybatis的执行逻辑

简介

Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository 层中解耦出来,除了这些基本功能外,它还提供了动态 sql、延迟加载、缓存等功能。 相比 Hibernate,Mybatis 更面向数据库,可以灵活地对 sql 语句进行优化。

本文继续分析 Mybatis 的源码,第1点内容上一篇博客已经讲过,本文将针对 2 和 3 点继续分析:

  1. 加载配置、初始化SqlSessionFactory
  2. 获取SqlSessionMapper
  3. 执行Mapper方法。

除了源码分析,本系列还包含 Mybatis 的详细使用方法、高级特性、生成器等,相关内容可以我的专栏 Mybatis

注意,考虑可读性,文中部分源码经过删减

隐藏在Mapper背后的东西

从使用者的角度来看,项目中使用 Mybatis 时,我们只需要定义Mapper接口和编写 xml,除此之外,不需要去使用 Mybatis 的其他东西。当我们调用了 Mapper 接口的方法,Mybatis 悄无声息地为我们完成参数设置、语句执行、结果映射等等工作,这真的是相当优秀的设计。

既然是分析源码,就必须搞清楚隐藏 Mapper 接口背后都是什么东西。这里我画了一张 UML 图,通过这张图,应该可以对 Mybatis 的架构及 Mapper 方法的执行过程形成比较宏观的了解。

mybatis_source_execute01

针对上图,我再简单梳理下:

  1. MapperSqlSession可以认为是用户的入口(项目中也可以不用Mapper接口,直接使用SqlSession),Mybatis 为我们生产的Mapper实现类最终都会去调用SqlSession的方法;
  2. Executor作为整个执行流程的调度者,它依赖StatementHandler来完成参数设置、语句执行和结果映射,使用Transaction来管理事务。
  3. StatementHandler调用ParameterHandler为语句设置参数,调用ResultSetHandler将结果集映射为所需对象。

那么,我们开始看源码吧。

Mapper代理类的获取

一般情况下,我们会先拿到SqlSession对象,然后再利用SqlSession获取Mapper对象,这部分的源码也是按这个顺序开展。

// 获取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取 Mapper
EmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class);

先拿到SqlSession对象

SqlSession的获取过程

上一篇博客讲了DefaultSqlSessionFactory的初始化,现在我们将利用DefaultSqlSessionFactory来创建SqlSession,这个过程也会创建出对应的ExecutorTransaction,如下图所示。

mybatis_source_execute02

图中的SqlSession创建时需要先创建Executor,而Executor又要依赖Transaction的创建,Transaction则需要依赖已经初始化好的TransactionFactoryDataSource

进入到DefaultSqlSessionFactory.openSession()方法。默认情况下,SqlSession是线程不安全的,主要和Transaction对象有关,如果考虑复用SqlSession对象的话,需要重写Transaction的实现

@Override
public SqlSession openSession() {
    // 默认会使用SimpltExecutors,以及autoCommit=false,事务隔离级别为空
    // 当然我们也可以在入参指定
    // 补充:SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement);BATCH 执行器不仅重用语句还会执行批量更新。
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 获取Environment中的TransactionFactory和DataSource,用来创建事务对象
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 创建执行器,这里也会给执行器安装插件
        final Executor executor = configuration.newExecutor(tx, execType);
        // 创建DefaultSqlSession
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

如何给执行器安装插件

上面的代码比较简单,这里重点说下安装插件的过程。我们进入到Configuration.newExecutor(Transaction, ExecutorType),可以看到创建完执行器后,还需要给执行器安装插件,接下来就是要分析下如何给执行器安装插件。

protected final InterceptorChain interceptorChain = new InterceptorChain();
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
	// 根据executorType选择创建不同的执行器
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // 安装插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

进入到InterceptorChain.pluginAll(Object)方法,这是一个相当通用的方法,不是只能给Executor安装插件,后面我们看到的StatementHandlerResultSetHandlerParameterHandler等都会被安装插件。

private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
    // 遍历安装所有执行器
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}
// 进入到Interceptor.plugin(Object)方法,这个是接口里的方法,使用 default 声明
default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

在定义插件时,一般我们都会采用注解来指定需要拦截的接口及其方法,如下。安装插件的方法之所以能够通用,主要还是@Signature注解的功劳,注解中,我们已经明确了拦截哪个接口的哪个方法。注意,这里我也可以定义其他接口,例如StatementHandler

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // do something
    }
}

上面这个插件将对Executor接口的以下两个方法进行拦截:

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

那么,Mybatis 是如何实现的呢?我们进入到Plugin这个类,它是一个InvocationHandler,也就是说 Mybatis 使用的是 JDK 的动态代理来实现插件功能,后面代码中, JDK 的动态代理也会经常出现。

public class Plugin implements InvocationHandler {
	// 需要被安装插件的类
    private final Object target;
    // 需要安装的插件
    private final Interceptor interceptor;
    // 存放插件拦截的接口接对应的方法
    private final Map<Class<?>, Set<Method>> signatureMap;
	
    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
		// 根据插件的注解获取插件拦截哪些接口哪些方法
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 获取目标类中需要被拦截的所有接口
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            // 创建代理类
            return Proxy.newProxyInstance(
                type.getClassLoader(),
                interfaces,
                new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 以“当前方法的声明类”为key,查找需要被插件拦截的方法
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            // 如果包含当前方法,那么会执行插件里的intercept方法
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            // 如果并不包含当前方法,则直接执行该方法
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }

以上就是获取SqlSession和安装插件的内容。默认情况下,SqlSession是线程不安全的,不断地创建SqlSession也不是很明智的做法,按道理,Mybatis 应该提供线程安全的一套SqlSession实现才对。

再获取Mapper代理类

Mapper 代理类的获取过程比较简单,这里我们就不一步步看源码了,直接看图就行。我画了 UML 图,通过这个图基本可以梳理以下几个类的关系,继而明白获取 Mapper 代理类的方法调用过程,另外,我们也能知道,Mapper 代理类也是使用 JDK 的动态代理生成。

mybatis_source_execute03

Mapper 作为一个用户接口,最终还是得调用SqlSession来进行增删改查,所以,代理类也必须持有对SqlSession的引用。通常情况下,这样的 Mapper代理类是线程不安全的,因为它持有的SqlSession实现类DefaultSqlSession也是线程不安全的,但是,如果实现类是SqlSessionManager就另当别论了。

Mapper方法的执行

执行Mapper代理方法

因为Mapper代理类是通过 JDK 的动态代理生成,当调用Mapper代理类的方法时,对应的InvocationHandler对象(即MapperProxy)将被调用,所以,这里就不展示Mapper代理类的代码了,直接从MapperProxy这个类开始分析。

同样地,还是先看看整个 UML 图,通过图示大致可以梳理出方法的调用过程。MethodSignature这个类可以重点看下,它的属性非常关键。

mybatis_source_execute04

下面开始看源码,进入到MapperProxy.invoke(Object, Method, Object[])。这里的MapperMethodInvoker对象会被缓存起来,因为这个类是无状态的,不需要反复的创建。当缓存中没有对应的MapperMethodInvoker时,方法对应的MapperMethodInvoker实现类将被创建并放入缓存,同时MapperMethodMethodSignaturesqlCommand等对象都会被创建好。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 如果是Object类声明的方法,直接调用
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            // 先从缓存拿到MapperMethodInvoker对象,再调用它的方法
            // 因为最终会调用SqlSession的方法,所以这里得传入SqlSession对象
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        // 缓存有就拿,没有需创建并放入缓存
        return methodCache.computeIfAbsent(method, m -> {
            // 如果是接口中定义的default方法,创建MapperMethodInvoker实现类DefaultMethodInvoker
            // 这种情况我们不关注
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return new DefaultMethodInvoker(getMethodHandleJava9(method));
                    }
                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                         | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // 如果不是接口中定义的default方法,创建MapperMethodInvoker实现类PlainMethodInvoker,在此之前也会创建MapperMethod
                return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

由于我们不考虑DefaultMethodInvoker的情况,所以,这里直接进入到MapperProxy.PlainMethodInvoker.invoke(Object, Method, Object[], SqlSession)

private final MapperMethod mapperMethod;
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    // 直接调用MapperMethod的方法,method和proxy的入参丢弃
    return mapperMethod.execute(sqlSession, args);
}

进入到MapperMethod.execute(SqlSession, Object[])方法。前面提到过 Mapper 代理类必须依赖SqlSession对象来进行增删改查,在这个方法就可以看到,方法中会通过方法的类型来决定调用SqlSession的哪个方法。

在进行参数转换时有三种情况:

  1. 如果参数为空,则 param 为 null;

  2. 如果参数只有一个且不包含Param注解,则 param 就是该入参对象;

  3. 如果参数大于一个或包含了Param注解,则 param 是一个Map<String, Object>,key 为注解Param的值,value 为对应入参对象。

另外,针对 insert|update|delete 方法,Mybatis 支持使用 void、Integer/int、Long/long、Boolean/boolean 的返回类型,而针对 select 方法,支持使用 Collection、Array、void、Map、Cursor、Optional 返回类型,并且支持入参 RowBounds 来进行分页,以及入参 ResultHandler 来处理返回结果

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 判断属于哪种类型,来决定调用SqlSession的哪个方法
    switch (command.getType()) {
		// 如果为insert类型方法
        case INSERT: {
            // 参数转换
            Object param = method.convertArgsToSqlCommandParam(args);
            // rowCountResult将根据返回类型来处理result,例如,当返回类型为boolean时影响的rowCount是否大于0,当返回类型为int时,直接返回rowCount
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
		// 如果为update类型方法
        case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        // 如果为delete类型方法
        case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
        // 如果为select类型方法
        case SELECT:
            // 返回void,且入参有ResultHandler
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            // 返回类型为数组或List
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            // 返回类型为Map
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            // 返回类型为Cursor
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            // 这种一般是返回单个实体对象或者Optional对象
            } 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;
        // 如果为FLUSH类型方法,这种情况不关注
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    // 当方法返回类型为基本类型,但是result却为空,这种情况会抛出异常
    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;
}

增删改的我们不继续看了,至于查的,只看method.returnsMany()的情况。进入到MapperMethod.executeForMany(SqlSession, Object[])。通过这个方法可以知道,当返回多个对象时,Mapper 中我们可以使用List接收,也可以使用数组或者Collection的其他子类来接收,但是处于性能考虑,如果不是必须,建议还是使用List比较好。

RowBounds作为 Mapper 方法的入参,可以支持自动分页功能,但是,这种方式存在一个很大缺点,就是 Mybatis 会将所有结果查放入本地内存再进行分页,而不是查的时候嵌入分页参数。所以,这个分页入参建议不要单独使用,最好配合插件一起使用。

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    // 转换参数
    Object param = method.convertArgsToSqlCommandParam(args);
    // 如果入参包含RowBounds对象,这个一般用于分页使用
    if (method.hasRowBounds()) {
        RowBounds rowBounds = method.extractRowBounds(args);
        result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
    // 不用分页的情况
        result = sqlSession.selectList(command.getName(), param);
    }
    // 如果SqlSession方法的返回类型和Mapper方法的返回类型不一致
    // 例如,mapper返回类型为数组、Collection的其他子类
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
        // 如果mapper方法要求返回数组
        if (method.getReturnType().isArray()) {
            return convertToArray(result);
        } else {
        // 如果要求返回Set等Collection子类,这个方法感兴趣的可以研究下,非常值得借鉴学习
            return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
        }
    }
    return result;
}

从Mapper进入到SqlSession

接下来就需要进入SqlSession的方法了,这里选用实现类DefaultSqlSession进行分析。SqlSession作为用户入口,代码不会太多,主要工作还是通过执行器来完成。

在调用执行器方法之前,这里会对参数对象再次包装,一般针对入参只有一个参数且不包含Param注解的情况:

  1. 如果是Collection子类,将转换为放入"collection"=object键值对的 map,如果它是List的子类,还会再放入"list"=object的键值对
  2. 如果是数组,将转换为放入"array"=object键值对的 map
@Override
public <E> List<E> selectList(String statement, Object parameter) {
    // 这里还是给它传入了一个分页对象,这个对象默认分页参数为0,Integer.MAX_VALUE
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 利用方法id从配置对象中拿到MappedStatement对象
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 接着执行器开始调度,传入resultHandler为空
        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();
    }
}

执行器开始调度

接下来就进入执行器的部分了。注意,由于本文不会涉及到 Mybatis 结果缓存的内容,所以,下面的代码都会删除缓存相关的部分

那么,还是回到最开始的图,接下来将选择SimpleExecutor进行分析。

mybatis_source_execute01

进入到BaseExecutor.query(MappedStatement, Object, RowBounds, ResultHandler)。这个方法中会根据入参将动态语句转换为静态语句,并生成对应的ParameterMapping

例如,

<if test="con.gender != null">and e.gender = {con.gender}</if>

将被转换为 and e.gender = ?,并且生成

ParameterMapping{property='con.gender', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

ParameterMapping

生成的ParameterMapping将根据?的索引放入集合中待使用。

这部分内容我就不展开了,感兴趣地可以自行研究。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 将sql语句片段中的动态部分转换为静态,并生成对应的ParameterMapping
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成缓存的key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

接下来一大堆关于结果缓存的代码,前面说过,本文不讲,所以我们直接跳过进入到SimpleExecutor.doQuery(MappedStatement, Object, RowBounds, ResultHandler, BoundSql)。可以看到,接下来的任务都是由StatementHandler来完成,包括了参数设置、语句执行和结果集映射等。

@Override
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对象,在上面的UML中可以看到,它非常重要
        // 创建StatementHandler对象时,会根据StatementType自行判断选择SimpleStatementHandler、PreparedStatementHandler还是CallableStatementHandler实现类
        // 另外,还会给它安装执行器的所有插件
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 获取Statement对象,并设置参数
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 执行语句,并映射结果集
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

本文将对以下两行代码分别展开分析。其中,关于参数设置的内容不会细讲,更多地精力会放在结果集映射上面

// 获取Statement对象,并设置参数
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行语句,并映射结果集
return handler.query(stmt, resultHandler);

语句处理器开始处理语句

在创建StatementHandler时,会通过MappedStatement.getStatementType()自动选择使用哪种语句处理器,有以下情况:

  1. 如果是 STATEMENT,则选择SimpleStatementHandler
  2. 如果是 PREPARED,则选择PreparedStatementHandler
  3. 如果是 CALLABLE,则选择CallableStatementHandler
  4. 其他情况抛出异常。

本文将选用PreparedStatementHandler进行分析。

获取语句对象和设置参数

进入到SimpleExecutor.prepareStatement(StatementHandler, Log)。这个方法将会获取当前语句的PreparedStatement对象,并给它设置参数。

protected Transaction transaction;
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 获取连接对象,通过Transaction获取
    Connection connection = getConnection(statementLog);
    // 获取Statement对象,由于分析的是PreparedStatementHandler,所以会返回实现类PreparedStatement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 设置参数
    handler.parameterize(stmt);
    return stmt;
}

进入到PreparedStatementHandler.parameterize(Statement)。正如 UML 图中说到的,这里实际上是调用ParameterHandler来设置参数。

protected final ParameterHandler parameterHandler;
@Override
public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
}

进入到DefaultParameterHandler.setParameters(PreparedStatement)。前面讲过,在将动态语句转出静态语句时,生成了语句每个?对应的ParameterMapping,并且这些ParameterMapping会按照语句中对应的索引被放入集合中。在以下方法中,就是遍历这个集合,将参数设置到PreparedStatement中去。

private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;

@Override
public void setParameters(PreparedStatement ps) {
    // 获得当前语句对应的ParameterMapping
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        // 遍历ParameterMapping
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            // 一般情况mode都是IN,至于OUT的情况,用于结果映射到入参,比较少用
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;// 用于设置到ps中的?的参数
                // 这个propertyName对应mapper中#{value}的名字
                String propertyName = parameterMapping.getProperty();
                // 判断additionalParameters是否有这个propertyName,这种情况暂时不清楚
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                // 如果入参为空
                } else if (parameterObject == null) {
                    value = null;
                // 如果有当前入参的类型处理器
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                // 如果没有当前入参的类型处理器,这种一般是传入实体对象或传入Map的情况
                } else {
                    // 这个原理和前面说过的MetaClass差不多
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                // 如果未指定jdbcType,且入参为空,没有在setting中配置jdbcTypeForNull的话,默认为OTHER
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                try {
                    // 利用类型处理器给ps设置参数
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException | SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

使用ParameterHandler设置参数的内容就不再多讲,接下来分析语句执行和结果集映射的代码。

语句执行和结果集映射

进入到PreparedStatementHandler.query(Statement, ResultHandler)方法。语句执行就是普通的 JDBC,没必要多讲,重点看看如何使用ResultSetHandler完成结果集的映射。

protected final ResultSetHandler resultSetHandler;
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 直接执行
    ps.execute();
    // 映射结果集
    return resultSetHandler.handleResultSets(ps);
}

为了满足多种需求,Mybatis 在处理结果集映射的逻辑非常复杂,这里先简单说下。

一般我们的 resultMap 是这样配置的:

<resultMap id="blogResult" type="Blog">
	<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
	SELECT * FROM BLOG WHERE ID = #{id}
</select>

然而,Mybatis 竟然也支持多 resultSet 映射的情况,这里拿到的第二个结果集将使用resultSet="authors"的resultMap 进行映射,并将得到的Author设置进Blog的属性。

<resultMap id="blogResult" type="Blog">
    <id property="id" column="id" />
    <result property="title" column="title"/>
    <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="bio" column="bio"/>
    </association>
</resultMap>
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
	{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

还有一种更加奇葩的,多 resultSet、多 resultMap,如下。这种我暂时也不清楚怎么用。

<select id="selectBlogs" resultSets="blogs01,blogs02" resultMap="blogResult01,blogResult02">
	{call getTwoBlogs(#{id,jdbcType=INTEGER,mode=IN})}
</select>

接下来只考虑第一种情况。另外两种感兴趣的自己研究吧。

进入到DefaultResultSetHandler.handleResultSets(Statement)方法。

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
	// 用于存放最终对象的集合
    final List<Object> multipleResults = new ArrayList<>();
	// resultSet索引
    int resultSetCount = 0;
    // 获取第一个结果集
    ResultSetWrapper rsw = getFirstResultSet(stmt);
	
    // 获取当前语句对应的所有ResultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    // resultMap总数
    int resultMapCount = resultMaps.size();
    // 校验结果集非空时resultMapCount是否为空
    validateResultMapsCount(rsw, resultMapCount);
    // 接下来结果集和resultMap会根据索引一对一地映射
    while (rsw != null && resultMapCount > resultSetCount) {
        // 获取与当前结果集映射的resultMap
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // 映射结果集,并将生成的对象放入multipleResults
        handleResultSet(rsw, resultMap, multipleResults, null);
        // 获取下一个结果集
        rsw = getNextResultSet(stmt);
        // TODO
        cleanUpAfterHandlingResultSet();
        // resultSet索引+1
        resultSetCount++;
    }
	
    // 如果当前resultSet的索引小于resultSets中配置的resultSet数量,将继续映射
    // 这就是前面说的第二种情况了,这个不讲
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
        while (rsw != null && resultSetCount < resultSets.length) {
            // 获取指定resultSet对应的ResultMap
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
                // 获取嵌套ResultMap进行映射
                String nestedResultMapId = parentMapping.getNestedResultMapId();
                ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
                handleResultSet(rsw, resultMap, null, parentMapping);
            }
            // 获取下一个结果集
            rsw = getNextResultSet(stmt);
            // TODO
            cleanUpAfterHandlingResultSet();
            // resultSet索引+1
            resultSetCount++;
        }
    }
	// 如果multipleResults只有一个,返回multipleResults.get(0),否则整个multipleResults一起返回
    return collapseSingleResultList(multipleResults);
}

进入DefaultResultSetHandler.handleResultSet(ResultSetWrapper, ResultMap, List<Object>, ResultMapping)。这里的入参 parentMapping 一般为空,除非在语句中设置了多个 resultSet;

属性 resultHandler 一般为空,除非在 Mapper 方法的入参中传入,这个对象可以由用户自己实现,通过它我们可以对结果进行操作。在实际项目中,我们往往是拿到实体对象后才到 Web 层完成 VO 对象的转换,通过ResultHandler ,我们在 DAO 层就能完成 VO 对象的转换,相比传统方式,这里可以减少一次集合遍历,而且,因为可以直接传入ResultHandler ,而不是具体实现,所以转换过程不会渗透到 DAO层。注意,采用这种方式时,Mapper 的返回类型必须为 void。

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        // 如果不是设置了多个resultSets,parentMapping一般为空
        // 所以,这种情况不关注
        if (parentMapping != null) {
            // 映射结果集
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            // resultHandler一般为空
            if (resultHandler == null) {
                // 创建defaultResultHandler
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                // 映射结果集
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                // 将对象放入集合
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                // 映射结果集,如果传入了自定义的ResultHandler,则由用户自己处理映射好的对象
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // issue #228 (close resultsets)
        closeResultSet(rsw.getResultSet());
    }
}

进入到DefaultResultSetHandler.handleRowValues(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)。这里会根据是否包含嵌套结果映射来判断调用哪个方法,如果是嵌套结果映射,需要判断是否允许分页,以及是否允许使用自定义ResultHandler

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    // 如果resultMap存在嵌套结果映射
    if (resultMap.hasNestedResultMaps()) {
        // 如果设置了safeRowBoundsEnabled=true,需校验在嵌套语句中使用分页时抛出异常
        ensureNoRowBounds();
        // 如果设置了safeResultHandlerEnabled=false,需校验在嵌套语句中使用自定义结果处理器时抛出异常
        checkResultHandler();
        // 映射结果集
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    // 如果resultMap不存在嵌套结果映射
    } else {
        // 映射结果集
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

这里我就不搞那么复杂了,就只看非嵌套结果的情况。进入DefaultResultSetHandler.handleRowValuesForSimpleResultMap(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)。 在这个方法中可以看到,使用RowBounds进行分页时,Mybatis 会查出所有数据到内存中,然后再分页,所以,不建议单独使用。

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
    throws SQLException {
    // 创建DefaultResultContext对象,这个用来做标志判断使用,还可以作为ResultHandler处理结果的入参
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    // 获取当前结果集
    ResultSet resultSet = rsw.getResultSet();
    // 剔除分页offset以下数据
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        // 如果存在discriminator,则根据结果集选择匹配的resultMap,否则直接返回当前resultMap
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        // 创建实体对象,并完成结果映射
        Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
        // 一般会回调ResultHandler的handleResult方法,让用户可以对映射好的结果进行处理
        // 如果配置了resultSets的话,且当前在映射子结果集,那么会将子结果集映射到的对象设置到父对象的属性中
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}

进入DefaultResultSetHandler.getRowValue(ResultSetWrapper, ResultMap, String)方法。这个方法将创建对象,并完成结果集的映射。点到为止,有空再做补充了。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 创建实体对象,这里会完成构造方法中参数的映射,以及完成懒加载的代理
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        // MetaObject可以方便完成实体对象的获取和设置属性
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        // foundValues用于标识当前对象是否还有未映射完的属性
        boolean foundValues = this.useConstructorMappings;
        // 映射列名和属性名一致的属性,如果设置了驼峰规则,那么这部分也会映射
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        // 映射property的RsultMapping
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        // 返回映射好的对象
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

以上,基本讲完 Mapper 的获取及方法执行相关源码的分析。

通过分析 Mybatis 的源码,希望读者能够更了解 Mybatis,从而在实际工作和学习中更好地使用,以及避免“被坑”。针对结果缓存、参数设置以及其他细节问题,本文没有继续展开,后续有空再补充吧。

参考资料

Mybatis官方中文文档

相关源码请移步:mybatis-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12761376.html

posted @ 2020-04-23 15:54  子月生  阅读(1843)  评论(0编辑  收藏  举报