MyBatis查询全流程源码分析

本文详细剖析了MyBatis查询数据库的全过程,包括配置文件解析、SqlSessionFactory构建、SqlSession操作、Mapper代理对象生成及结果集映射等关键步骤。

MyBatis作为一款优秀的持久层框架,现在已经被越来越多的公司在用了。面对这个我们每天都在使用的框架,不好好读读它的源码怎么行呢?笔者花了几天的时间阅读和调试MyBatis源码,现在把我的一些理解分享给大家,如有错误,还望指正。

MyBatis源码版本:3.5.8-SNAPSHOT,源码地址:https://github.com/mybatis/mybatis-3.git

1. 前言

MyBatis整体框架大致如下图所示,其中每个模块都可以单拎出来写一篇文章。篇幅原因,本篇文章只会介绍【查询】的全流程,且不会对细节介绍太多。例如:xml解析、参数映射、缓存等会一笔带过,其中的细节会在后面的文章单独介绍。
未命名文件 (1).jpg
本篇文章只讨论MyBatis,也只分析MyBatis的源码,因此不会和Spring做集成。

2. 示例程序

【需求】
使用MyBatis完成一次数据库查询,根据主键ID查询用户记录。

1、编写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>
  <!--环境配置-->
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>

  <!--Mapper配置-->
  <mappers>
    <mapper resource="mappers/UserMapper.xml"/>
  </mappers>
</configuration>

2、编写用户表对应的DO类。

public class User {
  private Long id;
  private String userName;
  private String pwd;
  private String nickName;
  private String phone;
  private LocalDateTime createTime;
  // 省略 Getter,Setter方法
}

3、编写UserMapper接口。

public interface UserMapper {

  // 根据ID查询用户
  User getById(@Param("id") Long id);
}

4、编写UserMapper.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.mapper.UserMapper">

  <select id="getById" useCache="true" flushCache="false" resultType="org.apache.domain.User">
    select
    	id,user_name as userName,pwd,nick_name as nickName,phone,create_time as createTime
    from user
    where id = #{id}
  </select>
</mapper>

5、编写测试程序。

public class Demo {
  public static void main(String[] args) throws Exception {
    String resource = "mybatis-config.xml";
    // 读取配置文件,获取输入流
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 从输入流中构建回话工厂
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 打开一个新的回话
    SqlSession session = sessionFactory.openSession(true);
    // 获取UserMapper代理类对象
    UserMapper mapper = session.getMapper(UserMapper.class);
    // 查询ID=1的用户信息,并输出到控制台
    System.err.println(mapper.getById(1L));
  }
}

至此,示例程序全部结束,运行示例程序,控制台会输出ID=1的用户信息。基于此程序,我们来一步步分析MyBatis是如何仅凭一个接口和xml文件就实现数据库查询到Java Bean的映射。

3. 源码分析

3.1 SqlSessionFactory的构建

SqlSessionFactory是MyBatis管理回话的工厂,它的职责就是打开一个SqlSession,有了SqlSession我们就可以对数据库进行增删改查的操作了。

SqlSessionFactory是一个接口,默认的实现类是DefaultSqlSessionFactory。

这里贴出部分代码:

public interface SqlSessionFactory {

  // 打开一个新的回话
  SqlSession openSession()

  /**
   * 打开一个新的回话
   * @param autoCommit 是否自动提交
   * @return
   */
  SqlSession openSession(boolean autoCommit);

  /**
   * 以指定的事务隔离级别 打开一个新的回话
   * @param level 事务隔离级别
   * @return
   */
  SqlSession openSession(TransactionIsolationLevel level);
    
  // 获取全局配置
  Configuration getConfiguration();
}

SqlSessionFactory采用【建造者模式】进行构建,对应的类为SqlSessionFactoryBuilder,build()构建方法有多个重载,你可以通过字符流Reader来构建,也可以通过字节流InputStream来构建,也可以通过全局配置对象Configuration来构建。其实不论是哪种方式,最终都是通过Configuration对象构建,前两种构建方式MyBatis只不过是从字符流/字节流中将config.xml配置文件解析成Configuration对象了。

public class SqlSessionFactoryBuilder {
  // 通过字符流构建
  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }
  
  // 通过字节流构建
  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }
  
  // 通过配置对象来构建,最终都是通过该方法构建
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }    
}

3.2 Configuration的构建

Configuration是MyBatis的全局配置类,它可以看作是mybatis-config.xml配置文件在Java中的描述对象。这个类非常重要,也非常庞大,这里不展开细说,后面会专门写文章记录。

构建SqlSessionFactory需要Configuration,那Configuration对象是怎么来的呢?其实就两种方式,一种是你手动创建Configuration对象,还有一种就是你编写好xml文件,让MyBatis自己去解析。

一般都是采用配置文件的方式,因此我们重点看XMLConfigBuilder.parse()

// 从xml配置文件中解析Configuration
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 从根节点<configuration>开始解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

// 从根节点开始依次解析
private void parseConfiguration(XNode root) {
  try {
    /**
     * 下面就是依次解析各类标签了
     * 1.解析properties标签,读取属性
     * 2.解析settings标签,读取设置项
     * 3.解析类的别名
     * 4.解析插件配置
     * 5.解析对象工厂配置
     * 6.解析对象包装工厂配置
     * 7.解析运行环境,多数据源配置
     * 8.解析databaseIdProvider,多数据库支持
     * 9.解析typeHandlers,类型处理器
     * 10.解析mappers,注册Mapper接口
     */
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

3.3 SqlSession

解析配置文件生成Configuration对象,有了Configuration就可以构建SqlSessionFactory,有了SqlSessionFactory就可以获得SqlSession对象。

SqlSession是MyBatis最重要的接口之一,它是MyBatis提供给开发者操作数据库的唯一接口,极大的简化了数据库的操作。

先来看看接口定义,我们就知道它具有哪些能力,我只贴部分代码。

public interface SqlSession extends Closeable {
    
    // 查询一条记录
    <T> T selectOne(String statement);
    
    // 查询多条记录
    <E> List<E> selectList(String statement);
    
    // 查询Map
    <K, V> Map<K, V> selectMap(String statement, String mapKey);
    
    // 游标查询
    <T> Cursor<T> selectCursor(String statement);
    
    // 插入数据
    int insert(String statement);
    
    // 修改数据
    int update(String statement);
    
    // 删除数据
    int delete(String statement);
    
    // 提交事务
    void commit();
    
    // 回滚事务
    void rollback();
    
    // 获取Mapper接口的代理类
    <T> T getMapper(Class<T> type);
    
    // 获取SqlSession关联的数据库连接
    Connection getConnection();
}

通过接口就可以看出来,只要有了SqlSession对象,我们就可以对数据库进行【增删改查】以及对事务的操作,而且它还会自动帮我们出来结果集和Java对象的映射,非常的方便。

3.4 生成Mapper代理对象

一般我们很少会直接通过SqlSession去操作数据库,而是会新建一个接口,通过MyBatis生成的代理对象去操作,因此我们重点关注getMapper()方法。

SqlSession的默认实现类是DefaultSqlSession,直接看它就好了。

/**
 * 获取Mapper接口代理对象:MapperProxy
 * @see MapperProxy
 * @param type Mapper接口类
 * @param <T>
 * @return
 */
@Override
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}

当我们通过SqlSession去获取Mapper的代理对象时,它会转交给Configuration对象去完成。因为MyBatis解析配置文件时,会一同解析Mapper.xml文件,并把解析结果注册到MapperRegistry中。MapperRegistry是Mapper接口的注册器,它将Mapper接口类Class和对应的MapperProxyFactory放到一个Map容器中,你可以向里面注册Mapper接口,以及获取Mapper接口的代理对象。

public class MapperRegistry {
  // 全局配置
  private final Configuration config;
  // Mapper接口和MapperProxyFactory的映射关系
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

  // 获取Mapper接口的代理对象
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  // 注册Mapper接口
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}

向MapperRegistry注册Mapper接口时,它会将Mapper接口封装成MapperProxyFactory,因为需要依赖它来为Mapper接口创建代理对象。创建代理对象的逻辑也很简单,因为Mapper都是接口,因此直接使用JDK动态代理就可以了,生成的代理对象的MapperProxy

/**
 * Mapper接口代理类工厂
 * 作用:生成Mapper接口代理对象
 * @param <T>
 */
public class MapperProxyFactory<T> {

  // Mapper接口类
  private final Class<T> mapperInterface;
  // 方法缓存,通过代理对象调用方法时,会先将Method解析成MapperMethodInvoker
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  @SuppressWarnings("unchecked")
  // 创建新实例,通过JDK动态代理创建代理对象
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
  // 省略部分代码...
}

3.5 MapperProxy

UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getById(1L);

UserMapper是接口,接口不能实例化,那mapper对象是哪里来的呢?
当我们调用第一行代码时,实际上MyBatis通过JDK动态代理为我们生成了一个代理对象MapperProxy。当我们调用第二行代码时,实际上执行的是MapperProxy.invoke()方法。

代理对象的invoke()如下,如果调用的是Object类的方法,如:hashCode、equals等,则直接通过代理对象本身调用即可,不需要操作数据库。否则为其生成MapperMethodInvoker对象,调用其invoke方法。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // 如果是继承自Object类的方法,则直接通过代理对象本身调用即可
      return method.invoke(this, args);
    } else {
      /**
      自定义方法才操作数据库
      1.先从缓存中获取方法对应的MapperMethodInvoker
      2.执行
      非default犯法,直接看:org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker.invoke()
      @see MapperMethod#execute(org.apache.ibatis.session.SqlSession, java.lang.Object[])
       */
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

MapperMethodInvoker是MyBatis对Mapper接口中Method的包装类,它有两个实现类:DefaultMethodInvoker和PlainMethodInvoker。

DefaultMethodInvoker是对Mapper接口中default方法的包装,也是不需要操作数据库的,直接获取方法的句柄对象MethodHandle调用即可。

如果是用户自定义的非default方法,如getById(),这种需要去操作数据库的,才会为Method生成PlainMethodInvoker对象。

// 从缓存中获取Method对应的MapperMethodInvoker对象
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (m.isDefault()) {
        // 如果调用的是Interface中的default方法,找到方法句柄MethodHandle,通过反射调用。
        // 不是重点,了解即可。
        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方法,需要操作数据库的,生成PlainMethodInvoker对象。
        // 它会通过sqlSession去操作数据库。
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

PlainMethodInvoker需要一个MapperMethod对象,对于需要操作数据库的方法,它会转交给MapperMethod完成,它会去调用MapperMethod.execute()方法。

3.6 MapperMethod

MapperMethod是MyBatis对Mapper接口中需要操作数据库的方法的包装类,先看下它的属性:

/**
 * 执行的SQL命令的封装类
 * name:接口全限定名+方法名,定位要执行的唯一的sql
 * type:SQL命令类型(增删改查)
 */
private final SqlCommand command;
/**
 * 方法签名的描述类
 * 方法的返回值的什么?
 * 返回单个对象还是集合对象?
 * 是否返回Map?
 * 等等...
 */
private final MethodSignature method;

SqlCommand是对方法要执行的SQL命令的描述,它记录了要执行SQL的唯一标识Statement,以及SQL命令的类型。

public static class SqlCommand {
    // 接口全限定名+方法名,定位要执行的唯一的sql
    private final String name;
    // SQL命令类型:增删改查等
    private final SqlCommandType type;
}

MethodSignature是对方法签名的描述,它记录了方法的参数、返回值、分页、ResultHandler等信息。

public static class MethodSignature {

    // 是否返回多个对象?如List
    private final boolean returnsMany;
    // 是否返回Map?
    private final boolean returnsMap;
    // 是否返回Void?
    private final boolean returnsVoid;
    // 是否返回Cursor游标对象?
    private final boolean returnsCursor;
    // 是否返回Optional对象?
    private final boolean returnsOptional;
    // 返回类型Class
    private final Class<?> returnType;
    // 解析方法上@MapKey注解的值
    private final String mapKey;
    // ResultHandler所在参数列表的下标(对查询结果做处理)
    private final Integer resultHandlerIndex;
    // RowBounds所在参数列表的下标(限制返回的结果数量)
    private final Integer rowBoundsIndex;
    // 参数名称解析器,对于加了@Param注解的参数,xml中可以直接使用
    private final ParamNameResolver paramNameResolver;
    // 省略部分代码... ...
}

当我们执行Mapper接口去操作数据库时,MapperProxy代理对象会执行MapperMethod.execute()方法,这里会完成数据库的【增删改查】操作,需要重点关注。

篇幅原因,这里只看查询。当调用userMapper.getById(1L)时,它首先会判断方法是否返回void且参数中包含ResultHandler,如果是就不返回结果了,将查询结果交给ResultHandler去处理。
然后会判断是否返回多条结果?是否返回Map?是否返回游标?很明显,这里都不满足,因此直接进最后一个else。

执行普通查询时,它首先需要将方法参数解析为ParamMap,本质还是一个HashMap,Key是参数Name,Value是参数值,参数解析的目的是你可以在xml中使用#{id}/${id}来使用参数,从而实现动态SQL。

参数解析完成后,它会调用SqlSession.selectOne()查询单条结果。看到没,还是通过SqlSession去操作数据库,只是我们很少直接用它,都是通过代理对象去操作的。

查询完成后,判断返回类型是否是Optional,如果是它会将结果使用Optional进行包装,最后返回结果。

/**
 * 执行Mapper方法:执行SQL
 * @param sqlSession
 * @param args
 * @return
 */
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  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()) {
        /**
        方法返回值为Void,且参数中有ResultHandler,将查询结果交给ResultHandler处理,直接返回null
         @see ResultHandler
         */
        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 {
        /*
        将方法实参转换为SQL语句需要用到的参数映射,这样你就可以在xml中通过#{param}来使用参数了。
        一般返回的是一个Map结构:例如
          "id":1,
          "param1":1
         */
        Object param = method.convertArgsToSqlCommandParam(args);
        /*
        执行SQL查询
        1.根据statement定位到MappedStatement
        2.SQL语句的处理,参数绑定
        3.StatementHandler创建对应的Statement
        4.执行SQL
        5.结果集处理,ResultSet转换成Java Bean
         */
        result = sqlSession.selectOne(command.getName(), param);
        // 如果返回结果为Optional,则自动将结果进行包装。
        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.7 ParamNameResolver

ParamNameResolver是MyBatis提供的【参数名称解析器】,它会处理方法的形参,以及加了@Param注解的参数,将它们转换成ParamMap。

先看属性:

public class ParamNameResolver {

  // 生成的Name前缀
  public static final String GENERIC_NAME_PREFIX = "param";

  // 是否用实际的参数名,可能得到arg0,arg1这种无意义的名称
  private final boolean useActualParamName;
    
  // 参数下标-对应的参数名
  private final SortedMap<Integer, String> names;

  // 是否存在@Param注解
  private boolean hasParamAnnotation;
}

MethodSignature对象被创建时,就会创建对应的ParamNameResolver。

ParamNameResolver会在构造函数中解析好参数名称及其对应的下标,并存放到names中。解析的逻辑是:优先取@Param注解指定的名称,如果没有注解则判断是否取实际的参数名,否则以参数下标当做Name。

这里补充一下,Java反射是可以获取参数名称的,前提是必须JDK8版本,且编译时加了–parameters参数才行,否则得到的是arg0,arg1这种无意义的参数名。

/**
 * 1.优先获取@Param注解的值
 * 2.通过反射获取参数名
 * 3.使用参数下标
 */
public ParamNameResolver(Configuration config, Method method) {
  this.useActualParamName = config.isUseActualParamName();
  final Class<?>[] paramTypes = method.getParameterTypes();
  final Annotation[][] paramAnnotations = method.getParameterAnnotations();
  final SortedMap<Integer, String> map = new TreeMap<>();
  int paramCount = paramAnnotations.length;
  // get names from @Param annotations
  for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
    if (isSpecialParameter(paramTypes[paramIndex])) {
      // 跳过RowBounds和ResultHandler参数
      continue;
    }
    String name = null;
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      if (annotation instanceof Param) {
        // 如果加了@Param注解,则取注解的值
        hasParamAnnotation = true;
        name = ((Param) annotation).value();
        break;
      }
    }
    if (name == null) {
      // @Param was not specified.
      if (useActualParamName) {
        // 如果使用实际的参数名,则通过反射获取参数名。
        // JDK8编译类加–parameters参数可以保留参数名称,否则得到的是arg0,arg1这种无意义的参数名
        name = getActualParamName(method, paramIndex);
      }
      if (name == null) {
        // 如果名称还是为空,则可以使用下标来获取参数:#{param1},#{param2}...
        name = String.valueOf(map.size());
      }
    }
    map.put(paramIndex, name);
  }
  // 使其不可变
  names = Collections.unmodifiableSortedMap(map);
}

构造函数只负责解析参数下标对应的参数名称,要想获取参数名对应的参数值,还需要调用getNamedParams()方法。
因为开发者可能没加注解,反射可能获取不到参数名,由于存在种种不确定性,因此MyBatis额外提供了一种确定性方案。它会在解析参数时,除了使用已知的参数名,还会自动根据参数下标按照#{param下标}自动生成记录。

例如示例程序中的参数id,会解析成如下:

"id" 	 > 1
"param1" > 1

代码如下:

/**
 * 获取参数名对应的参数值
 * 一般是Map结构,当参数只有1个时,直接返回,xml中写任意值都可以匹配到
 * @param args
 * @return
 */
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    // 无参情况
    return null;
  } else if (!hasParamAnnotation && paramCount == 1) {
    // 没有@Param注解,且参数只有一个的情况
    Object value = args[names.firstKey()];
    return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
  } else {
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    for (Map.Entry<Integer, String> entry : names.entrySet()) {
      param.put(entry.getValue(), args[entry.getKey()]);
      final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
      // 还会额外生成 param1,param2...的映射关系,你也可以在xml中使用#{param1}
      if (!names.containsValue(genericParamName)) {
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    }
    return param;
  }
}

3.8 selectOne

参数解析完成后,又回到了SqlSession的查询操作了。先说一下SqlSession为什么可以操作数据库,要想操作数据库,首先得拿到数据库连接Connection。

DefaultSqlSession是SqlSession的默认实现类,我们来看下它的创建过程。

当我们从SqlSessionFactory中打开一个新的回话时,会调用openSessionFromDataSource()方法。它首先会从配置对象中获取运行环境,再从环境中获取事务工厂TransactionFactory。
TransactionFactory对应的就是配置文件中如下配置项,在解析配置文件时就已经创建好了,一般是JdbcTransactionFactory。

<transactionManager type="JDBC"/>

JdbcTransactionFactory会打开一个新的事务JdbcTransaction,JdbcTransaction中就包含数据源DataSource和数据库连接Connection。

public class JdbcTransaction implements Transaction {
  // 数据库连接
  protected Connection connection;
  // 数据源
  protected DataSource dataSource;
  // 事务隔离级别
  protected TransactionIsolationLevel level;
  // 是否自动提交
  protected boolean autoCommit;
}

有了JdbcTransaction,再根据ExecutorType去创建对应的Executor,Executor是MyBatis操作数据库的执行器接口。
有了Executor就可以创建DefaultSqlSession了,源码如下:

/**
 * 从DataSource打开一个Session
 * @param execType 执行器类型:简单、重用、批量
 * @param level 事务隔离级别
 * @param autoCommit 是否自动提交
 * @return
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 获取运行环境:解析xml时创建
    final Environment environment = configuration.getEnvironment();
    // 获取Environment关联的事务工厂
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 使用工厂类创建一个事务管理器,一般是 JdbcTransaction
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 根据事务管理器和执行器类型创建执行器,一般是 SimpleExecutor
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建一个SqlSession
    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();
  }
}

有了SqlSession就可以操作数据库了,回到selectOne()方法,它其实还是会执行selectList(),只不过会自动取第0条结果而已,因此我们重点关注selectList()

statement是要执行的SQL的唯一标识,由【接口全限定名+方法名】组成,因此MyBatis不支持接口方法的重载。根据statement就可以定位到xml中唯一的一个SQL节点,这个SQL节点在Java中用MappedStatement类表示。

MappedStatement这个类也很重要,下节会详细说明,现在你只需要知道它代表SQL标签节点,有了它就知道要执行的SQL语句是什么,返回结果是什么,就行了。

定位到MappedStatement后,就是调用Executor.query()进行查询操作了。

/**
 * 查询多条结果
 * @param statement 要执行SQL的唯一标识:接口全限定名+方法名
 * @param parameter 参数列表
 * @param rowBounds 分页条件
 * @param handler 结果处理器,如果参数中没有ResultHandler,则默认为null
 * @param <E>
 * @return
 */
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 从Map缓存中获取MappedStatement(解析xml里的SQL标签时创建)。
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

3.9 MappedStatement

MappedStatement可以理解为是MyBatis对xml文件中【增删改查】SQL节点的描述,MyBatis在解析xml文件时会自动生成该对象,详细代码在XMLStatementBuilder.parseStatementNode(),这里不详细介绍。

MappedStatement对象记录了SQL节点来自哪个xml文件,SQL语句是什么,返回结果类型是什么等等信息。有了它就知道该执行什么SQL语句,以及如何对结果集做数据映射了,因此这个类也十分重要。

类比较庞大,篇幅原因,只作属性的说明:

public final class MappedStatement {

  // 来自哪个xml文件
  private String resource;
  // 全局配置
  private Configuration configuration;
  // 唯一标识:接口全限定名+方法名
  private String id;
  // 限制SQL执行返回的最大行数,避免大数据量的查询导致OOM
  private Integer fetchSize;
  // SQL执行的超时时间
  private Integer timeout;
  // 执行SQL所使用的Statement类型
  private StatementType statementType;
  // 返回的结果集类型
  private ResultSetType resultSetType;
  // 执行的SQL源,通过它来获取执行的SQL语句
  private SqlSource sqlSource;
  // 二级缓存
  private Cache cache;
  // 标签中parameterMap属性的封装
  private ParameterMap parameterMap;
  // <resultMap>标签的封装,配置数据库字段和Java类属性的映射关系
  private List<ResultMap> resultMaps;
  // 是否需要刷新缓存
  private boolean flushCacheRequired;
  // 是否使用缓存
  private boolean useCache;
  private boolean resultOrdered;
  // SQL命令的类型(增删改查)
  private SqlCommandType sqlCommandType;
  // 主键生成器,insert数据时返回主键
  private KeyGenerator keyGenerator;
  // 主键应用的属性
  private String[] keyProperties;
  // 主键应用的列
  private String[] keyColumns;
  // 是否存在嵌套的结果
  private boolean hasNestedResultMaps;
  // 数据源ID,区分多数据源
  private String databaseId;
  // 日志
  private Log statementLog;
  // 不同语言驱动
  private LanguageDriver lang;
  // 返回多结果集时使用
  private String[] resultSets;
}

3.10 Executor

Executor接口是MyBatis提供的操作数据库的执行器接口,SqlSession操作数据库就是委托Executor去执行的。

贴出部分Executor定义,看看它具有的能力,主要是操作数据库、事务的管理、缓存的管理等。

public interface Executor {

  /**
   * 执行SQL查询
   * @param ms 执行的的Statement,理解为:执行哪个xml的哪个sql标签节点?
   * @param parameter 参数
   * @param rowBounds 分页数据
   * @param resultHandler 结果处理器
   * @param <E>
   * @return
   * @throws SQLException
   */
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  // 游标查询
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  // 批量执行
  List<BatchResult> flushStatements() throws SQLException;

  // 事务提交
  void commit(boolean required) throws SQLException;

  // 事务回滚
  void rollback(boolean required) throws SQLException;

  // 创建缓存键,用于一级缓存
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  // 是否命中缓存
  boolean isCached(MappedStatement ms, CacheKey key);

  // 清除本地缓存
  void clearLocalCache();
  // 省略部分代码......
}

Executor有个抽象子类BaseExecutor,它是其他实现类的父类,采用的是【模板方法】模式,实现了基本功能。

子类如下:

实现类说明
SimpleExecutor简单执行器,每次执行都创建新的Statement
ReuseExecutor重用执行器,缓存同一个SQL的Statement,避免Statement频繁创建,优化性能
BatchExecutor批量操作执行器
CachingExecutor支持二级缓存的执行器,装饰者模式

默认是开启缓存的,因此我们直接看CachingExecutor.query()。它首先会调用ms.getBoundSql()完成SQL的解析,这一步会完成SQL的动态拼接,完成${}/#{} 参数的替换。
然后创建缓存键CacheKey,这是MyBatis自带的基于SqlSession的一级缓存,根据【StatementID+SQL+参数+分页】的规则创建,只有这些数据完全相同才会命中缓存。
然后就是调用query()进行查询了。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 根据实参完成SQL的解析,替换#{}和${},得到可以直接执行的SQL
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //System.err.println("###SQL:" + boundSql.getSql());
  // 根据执行的 [StatementID+SQL+参数+分页] 来创建缓存键,只有这些数据全部相同才能命中缓存。
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

CachingExecutor是支持二级缓存的,使用【装饰者模式】,内部依赖一个Executor,CachingExecutor本身不会处理数据库操作,它的作用就是判断查询是否命中二级缓存,没有命中则委派delegate去查询,然后将结果缓存起来。
如下是CachingExecutor重写的query()方法。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  if (cache != null) { // Mapper开启二级缓存的情况
    // 判断是否需要清空缓存
    flushCacheIfRequired(ms);
    // 当前Statement是否使用缓存
    if (ms.isUseCache() && resultHandler == null) {
      // 如果调用的是存储过程,二级缓存不支持保存输出类型的参数,会抛异常
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 从缓存中获取数据
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 缓存中没有,则查询数据库,并缓存结果
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托delegate去查询
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

如果没有命中二级缓存,会调用BaseExecutor.query()进行查询。BaseExecutor会先判断是否命中一级缓存,如果没命中才会真的去查询数据库。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 向ErrorContext报告自己正在做查询
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  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();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

在没有命中任何缓存的情况下,会调用doQuery()去真正的查询数据库。我们直接看SimpleExecutor.doQuery()
它首先会创建StatementHandler,然后准备好JDBC原生的Statement,最后调用JDBC原生的Statement.execute()去执行SQL,然后对结果集进行映射处理,得到最终的结果。

@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();
  /*
  创建RoutingStatementHandler,装饰者模式
  根据StatementType创建[Simple/Prepared/Callable]StatementHandler
   */
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    /**
     准备Statement
     1.创建对应的Statement
     2.设置好Timeout、FetchSize
     3.设置好参数
     */
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 执行查询,并完成结果集映射
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

3.11 StatementHandler

StatementHandler是MyBatis中对Statement的处理器描述。当Executor要执行SQL时,会通过Configuration.newStatementHandler()先创建StatementHandler,默认创建的是RoutingStatementHandler。

RoutingStatementHandler使用的【委派模式】,它本身不干活,只是根据StatementType创建对应的StatementHandler,然后委托它去干活。

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

  // 根据StatementType创建委托对象
  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());
  }

}

StatementType枚举有三个实例,分别对应JDBC三种Statement,如下:

public enum StatementType {
  STATEMENT, // Statement 普通SQL,无法设置参数
  PREPARED,  // PreparedStatement 预编译的,防止SQL注入
  CALLABLE   // CallableStatement 支持调用存储过程
}

StatementHandler和JDBCStatement的对应关系:

StatementHandlerJDBC Statement
SimpleStatementHandlerStatement
PreparedStatementHandlerPreparedStatement
CallableStatementHandlerCallableStatement

我们先看下准备Statement的流程:先调用子类的instantiateStatement()方法创建对应的Statement,再给Statement设置好Timeout和FetchSize。

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    /*
    根据StatementType创建JDBC原生的Statement
    SimpleStatementHandler    > connection.createStatement()
    PreparedStatementHandler  > connection.prepareStatement()
    CallableStatementHandler  > connection.prepareCall()
     */
    statement = instantiateStatement(connection);
    // 设置超时时间
    setStatementTimeout(statement, transactionTimeout);
    // 设置FetchSize,xml没有配置,则取
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
  }
}

我们这里是根据用户ID查询,且xml里使用的是#{id},所以用到的是PreparedStatementHandler。PreparedStatementHandler创建的Statement当然就是PreparedStatement了。

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = mappedStatement.getKeyColumns();
        if (keyColumnNames == null) {
            return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
        } else {
            return connection.prepareStatement(sql, keyColumnNames);
        }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        return connection.prepareStatement(sql);
    } else {
        return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
}

PreparedStatement创建好了,接下来就是设置参数了,代码在DefaultParameterHandler.setParameters()
它会根据参数类型找到对应的TypeHandler,TypeHandler是MyBatis提供的类型处理器接口,它的作用有两个:一是给Statement设置参数、二是从ResultSet中获取结果做类型转换。

@Override
public void setParameters(PreparedStatement ps) {
  // 向ErrorContext报告自己正在设置参数
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // 获取参数映射
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        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;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 获取TypeHandler,我们的参数是Long,所以就是LongTypeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          /*
          当参数不为空时,MyBatis可以根据Java类型推断出JdbcType。
          参数为空时,就无法推断了,此时会使用默认类型:JdbcType.OTHER
          Oracle数据库中,JdbcType.OTHER传入NULL会报异常:无效的列类型,此时必须指定JdbcType
           */
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          // 设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

我们的参数是Long id,所以对应的就是LongTypeHandler,看看它设置参数的过程,其实很简单。

@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType)
    throws SQLException {
    // 给第i号参数设置Long类型的值
    ps.setLong(i, parameter);
}

PreparedStatement创建好了,参数也设置完毕了,就可以直接执行了。执行后拿到结果集ResultSet,就剩下结果集映射,将ResultSet转换成Java Bean了。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 执行Statement
    ps.execute();
    // 结果集处理
    return resultSetHandler.handleResultSets(ps);
}

3.12 ResultSetHandler

ResultSetHandler是MyBatis提供的结果集处理器,它的作用是负责将数据库返回的结果集ResultSet转换成我们想要的Java Bean。

默认的实现类是DefaultResultSetHandler,它首先会将JDBC原生的ResultSet包装成MyBatis的ResultSetWrapper,然后调用handleResultSet()处理结果集,将单个ResultSet转换成JavaBean并存入list中,然后循环处理,最终返回list结果。

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
  // 向ErrorContext报告自己正在处理结果集
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  // 可能有多个结果集,所以用List存放
  final List<Object> multipleResults = new ArrayList<>();

  int resultSetCount = 0;
  // 如果结果集存在,就将其包装为MyBatis的ResultSetWrapper对象
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  // <insert>标签配置的resultMap属性对应的标签集
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  // 校验数量
  validateResultMapsCount(rsw, resultMapCount);
  // 处理
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 处理结果集,得到Java对象集合存入multipleResults
    handleResultSet(rsw, resultMap, multipleResults, null);
    // 获取下一个结果,继续循环处理
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  // 处理resultSets属性,多结果集
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
  }

  // 如果只有单个结果集,则返回multipleResults的第0个元素,否则直接返回multipleResults
  return collapseSingleResultList(multipleResults);
}

为什么要将ResultSet包装成ResultSetWrapper?是为了方便做结果映射和类型转换。
ResultSetWrapper记录了结果集返回的所有列,以及列对应的Java类型和JDBC类型,还有列对应的TypeHandler,需要它来做结果集的类型转换。篇幅原因,这里贴部分代码:

public class ResultSetWrapper {

  // JDBC原生结果集
  private final ResultSet resultSet;
  // TypeHandler注册器
  private final TypeHandlerRegistry typeHandlerRegistry;
  // 列名
  private final List<String> columnNames = new ArrayList<>();
  // 对应的Class名称
  private final List<String> classNames = new ArrayList<>();
  // 对应的JdbcType
  private final List<JdbcType> jdbcTypes = new ArrayList<>();
  private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>();
  private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();
  private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();

  public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
    super();
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    // 获取结果集元数据
    final ResultSetMetaData metaData = rs.getMetaData();
    // 结果集中列的数量
    final int columnCount = metaData.getColumnCount();
    // 解析所有列,得到其列名、JdbcType、以及对应的Java类
    for (int i = 1; i <= columnCount; i++) {
      columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
      jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
      classNames.add(metaData.getColumnClassName(i));
    }
  }
}

handleResultSet()用户处理结果集,它首先会创建一个DefaultResultHandler,内部会有一个List,然后调用handleRowValues()处理返回的行记录,并将处理结果添加到List中。

/**
 * 处理结果集,将结果集转换成Java对象并存入multipleResults
 * @param rsw             结果集包装对象
 * @param resultMap       结果映射,resultType属性会被转换成resultMap
 * @param multipleResults 最终的Java对象结果集
 * @param parentMapping   <resultMap>中配置的列和属性的映射关系
 * @throws SQLException
 */
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
  try {
    if (parentMapping != null) {
      handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
    } else { // 我们没有配,所以走这里
      /**
       没有ResultHandler,则返回结果。
       有ResultHandler,则将结果交给它处理,不返回List结果集。
       */
      if (resultHandler == null) {
        /**
         ObjectFactory:MyBatis依赖它反射创建对象、给对象赋值。
         DefaultResultHandler:默认的结果处理器。
         */
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        // 处理返回的数据行记录,转换成Java对象集合,存入DefaultResultHandler
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        // 将转换后的Java结果集存入multipleResults,并返回
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
      }
    }
  } finally {
    // issue #228 (close resultsets)
    closeResultSet(rsw.getResultSet());
  }
}

如果ResultMap不包含嵌套结果映射的话,会调用handleRowValuesForSimpleResultMap()方法,它会处理分页,跳过一部分数据,然后解析鉴别器Discriminator,然后调用getRowValue()获取行结果并转换成Java对象,最终将Java对象存入List。

/**
 * 处理结果结果映射,不包含嵌套
 * @param rsw
 * @param resultMap
 * @param resultHandler
 * @param rowBounds
 * @param parentMapping
 * @throws SQLException
 */
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
  throws SQLException {
  DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
  ResultSet resultSet = rsw.getResultSet();
  // 根据分页规则跳过一些数据
  skipRows(resultSet, rowBounds);
  // 如果有更多的数据,则处理它
  while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
    // 解析鉴别器Discriminator
    ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
    // 从结果集中获取Java对象
    Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
    // 将单个结果对象存入到list中
    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
  }
}

getRowValue()首先会调用createResultObject()利用反射创建结果对象,如果结果对象是复杂对象,此时它还是个空对象,还需要调用applyAutomaticMappings()进行属性的赋值,最终才可以返回。

/**
 * 从数据行中获取结果
 *
 * @param rsw          结果集包装对象
 * @param resultMap    结果集映射
 * @param columnPrefix 列名前缀
 * @return
 * @throws SQLException
 */
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
  final ResultLoaderMap lazyLoader = new ResultLoaderMap();
  /**
   创建结果对象
   1.如果是简单对象,如Long,此时已经得到结果了。
   2.如果是复杂对象,如自定义User,此时拿到的是空对象,需要下面的属性赋值。
   */
  Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
  if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    // 获取对象元数据
    final MetaObject metaObject = configuration.newMetaObject(rowValue);
    // 使用构造函数映射
    boolean foundValues = this.useConstructorMappings;
    // 反射给空对象的属性赋值
    if (shouldApplyAutomaticMappings(resultMap, false)) {
      foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
    }
    foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
    foundValues = lazyLoader.size() > 0 || foundValues;
    rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
  }
  // 返回赋值后的结果对象
  return rowValue;
}

createResultObject()根据构造函数来反射创建结果对象:

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
  this.useConstructorMappings = false; // reset previous mapping result
  // 构造函数形参类型列表
  final List<Class<?>> constructorArgTypes = new ArrayList<>();
  // 构造函数实参列表
  final List<Object> constructorArgs = new ArrayList<>();
  /**
   创建结果对象
   1.如果返回结果类存在对应的TypeHandler,则直接处理了,例如返回Long,这里会直接完成类型转换。
   2.如果返回结果为复杂类,例如自定义的User,则会调用构造函数创建空对象。
   */
  Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);

  // 存在结果,且为复杂对象
  if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    for (ResultMapping propertyMapping : propertyMappings) {
      // issue gcode #109 && issue #149
      if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
        resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
        break;
      }
    }
  }
  this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
  return resultObject;
}

applyAutomaticMappings()用来给结果对象属性赋值:

// 给结果对象赋值
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
  List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
  boolean foundValues = false;
  if (!autoMapping.isEmpty()) {
    for (UnMappedColumnAutoMapping mapping : autoMapping) {
      final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
      if (value != null) {
        foundValues = true;
      }
      if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
        // gcode issue #377, call setter on nulls (value is not 'found')
        metaObject.setValue(mapping.property, value);
      }
    }
  }
  return foundValues;
}

最终List里就会有一个User对象,然后selectOne()会取List中第0个元素返回,这样代理对象就能返回最终结果了。

4. 总结

MyBatis程序启动,首先会去解析配置文件创建Configuration对象,再利用Configuration去构建SqlSessionFactory,有了SqlSessionFactory就可以打开一个SqlSession。基于SqlSession我们可以拿到Mapper接口的代理对象MapperProxy,当我们调用Mapper的查询方法时,代理对象会自动帮我们调用SqlSession的select方法,SqlSession又会委托Executor去调用query方法,再根据StatementType创建对应的JDBC原生的Statement并完成参数的设置,最终执行SQL拿到结果集ResultSet,最后根据ResultMap完成结果映射,将ResultSet转换成目标结果Java Bean,最终通过代理对象返回。

东西太多了,有些可能三句两句讲不清我就跳过了,以后会针对核心类单独写文章记录,敬请期待吧~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长河0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值