coding……
但行好事 莫问前程

Mybatis源码解读『select查询——使用Mapper接口』

上篇文章我们介绍了SQL映射文件的解析,关于Mybatis使用的准备工作算是分析的差不多了,本篇文章我们就来分析一下Mybatis是如何实现select查询的。一般我们使用Mybatis查询可能通过如下两种方式:

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {

    UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
    System.out.println("query all user");
    List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
    userInfoList.forEach(System.out::println);
    System.out.println("---------------------");

    System.out.println("queryUserInfoById id = 1");
    System.out.println(userInfoMapper.queryUserInfoById(1L));
    System.out.println("---------------------");
} finally {
    sqlSession.close();
}

这种方式,使用看上去,好像在直接使用Mapper接口查询。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
    String nameSpace = "com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper" + ".";

    System.out.println("query all user");
    List<UserInfo> userInfos = sqlSession.selectList(nameSpace + "queryAllUser");
    userInfos.forEach(System.out::println);
    System.out.println("---------------------");

    System.out.println("queryUserInfoById id = 1");
    UserInfo userInfo = sqlSession.selectOne(nameSpace + "queryUserInfoById", 1L);
    System.out.println(userInfo);
    System.out.println("---------------------");
} finally {
    sqlSession.close();
}

这种方式,我们直接使用了SqlSession中提供的接口查询。

上述这两种方式有什么区别,接下来我们就来详细介绍一下。第一种使用”Mapper接口 “查询的方式,本质上其实也是通过SqlSession中的接口(第二种)实现的。另外需要注意的是,无论使用哪种方式从查询,都不可避免地先创建SqlSession(update、insert、delete也是一样,也需要提前构建SqlSession),所以在介绍如何实现查询之前,我们先来看一下SqlSession的构建,由于篇幅原因,本篇文章介绍一下使用Mybatis使用Mapper接口的查询,后面的文章再介绍使用SqlSession的查询。

1. SqlSession构建

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

SqlSession构建需要两步:

  • 通过Mybatis配置文件文件流创建SqlSessionFactory
  • 调用SqlSessionFactory的openSession方法构建SqlSession
public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

关于SqlSessionFactory的构建,其实就是解析Mybatis配置文件获取Configuration对象,然后调用DefaultSqlSessionFactory的构造函数,返回一个DefaultSqlSessionFactory对象。

关于Mybatis配置文件解析,前两篇文章已经介绍过。接下来,我们重点来看一下DefaultSqlSessionFactory的openSession方法,是如何构建SqlSession的。

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
/**
 * ExecutorType:枚举(SIMPLE, REUSE, BATCH),用于指定Executor的类型,默认类型为SIMPLE
 * TransactionIsolationLevel:事务隔离级别,传参null,则表示使用数据库默认的事务隔离界别
 * autoCommit:自动提交,true-自动提交,false-不自动提交
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 获取配置中的环境信息,环境中可以获取Mybatis配置文件中配置的数据源,事务等信息
    final Environment environment = configuration.getEnvironment();
    // 从环境中获取事务工厂TransactionFactory
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 创建事务,配置事务属性(数据源、事务隔离级别、事务是否自动提交)
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 创建Executor,即执行器,执行器用于执行sql(事务 + 执行器类型)
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建DefaultSqlSession对象,构造函数中传参(Configuration + 执行器 + 是否自动提交)
    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();
  }
}

上述openSessionFromDataSource方法主要包含如下几个核心步骤:

  1. 从configuration中获取Environment对象,Environment对象中主要包含了DataSource和TransactionFactory
  2. 通过TransactionFactory创建Transaction
  3. 通过Transaction和ExecutorType创建执行器Executor
  4. 创建DefaultSqlSession对象

1.1 获取Environment对象

这里需要使用到我们之前Mybatis配置文件解析得到的Environment,我们回顾一下Environment的配置,如下:

<!-- 环境配置 -->
<environments default="default">
    <environment id="default">
        <transactionManager type="JDBC"></transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${dataSource.driver}"/>
            <property name="url" value="${dataSource.url}"/>
            <property name="username" value="${dataSource.username}"/>
            <property name="password" value="${dataSource.password}"/>
        </dataSource>
    </environment>
</environments>

可以看到,我们Environment中配置了<transactionManager>和<dataSource>,我们之前的文章介绍过我们会根据配置,得到TransactionFactory和DataSource并设置到Configuration中。

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    // 1. 获取默认的JDBC环境名称
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    // 2. 遍历<environments>标签下的每一个<environment>标签
    for (XNode child : context.getChildren()) {
      // 2.1 获取<environment>标签id属性
      String id = child.getStringAttribute("id");
      // 2.2 判断当前的<environment>是不是默认的JDBC环境,如果是则解析该环境下的TransactionFactory和DataSource
      if (isSpecifiedEnvironment(id)) {
        // 解析当前环境下配置的TransactionFactory
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        // 解析当前环境下配置的DataSource
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

这里我们<transactionManager>类型配置的是”JDBC”,<dataSource>类型配置的是”POOLED”,所以最终会解析得到JdbcTransactionFactoryPooledDataSource

1.2 创建Transaction

Transaction对象时候通过TransactionFactory调用newTransaction方法获取的,我们上面介绍过,TransactionFactory是在Environment中配置的,类型为JdbcTransactionFactory,所以我们来看一下JdbcTransactionFactory的newTransaction方法。

public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
  return new JdbcTransaction(ds, level, autoCommit);
}

JdbcTransaction继承自Transaction接口,用于获取\关闭Sql连接(Connection),提交\回滚事务。

1.3 创建Executor执行器

Executor执行器通过调用Configuration类的newExecutor方法生成,该方法中会根据参数ExecutorType确定生成的Executor执行器类型,当参数ExecutorType为null时,默认生成SimpleExecutor。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  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;
}

如果Mybatis配置文件<settings>中配置了”cacheEnabled”为true,则生成执行器时,会使用装饰器模式生成CachingExecutor执行器。

最后调用InterceptorChain的pluginAll方法,非常有意思。Mybatis配置文件中配置的所有的plugin,解析后会生成一个InterceptorChain(内部有一个Interceptor集合)。

<!-- 插件 -->
<plugins>
    <!-- 可以通过plugin标签,添加拦截器,比如分页拦截器 -->
   <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 分页合理化参数,默认文false;pageNum <= 0,查询第一页;pageNum > 总页数,查询最后一页-->
        <property name="reasonable" value="true"/>
    </plugin>
</plugins>

在pluginAll方法中,每个Interceptor,会通过代理模式生成一个代理对象。并且一个代理对象会作为下一个Interceptor处理生成代理对象场景下的委托类,所以InterceptorChain中所有的Interceptor处理结束,所有的Interceptor都会被织入代理链中,最原始的委托类对象其实就是我们生成的SimpleExecutor(如果应用了缓存就是CachingExecutor)。那么之后SimpleExecutor执行sql方法时,我们配置的Interceptor就可以生效了。我们之前使用的PageHelper就是一种Interceptor。

1.4 创建DefaultSqlSession对象

最后创建DefaultSqlSession,是通过调用DefaultSqlSession构造函数完成的。

new DefaultSqlSession(configuration, executor, autoCommit);

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
  this.configuration = configuration;
  this.executor = executor;
  this.dirty = false;
  this.autoCommit = autoCommit;
}

可以看到DefaultSqlSession构造函数中,我们传入了Configuration配置类和执行器Executor以及autoCommit(这里是false)。

可以看到DefaultSqlSession中定义了很多sql操作的方法。完成了SqlSession构建后,接下来来看一下是如何通过Mybatis完成查询操作的。

2. 使用”Mapper接口”查询

这里我们首先来看一下,使用Mybatis如何通过Mapper接口完成查询操作。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {

    UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
    System.out.println("query all user");
    List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
    userInfoList.forEach(System.out::println);
    System.out.println("---------------------");

    System.out.println("queryUserInfoById id = 1");
    System.out.println(userInfoMapper.queryUserInfoById(1L));
    System.out.println("---------------------");
} finally {
    sqlSession.close();
}

这里我们通过SqlSession的getMapper接口,获取了一个UserInfoMapper实例。然后就使用该实例中定义的方法,完成了查询操作,到这里我们肯定有很多疑问,比如:

  • 我们并没有对Mapper接口定义实现类,那么这个getMapper返回的实例实际类型是什么?
  • Mybatis如何实现通过Mapper接口查询的

接下来我们就来看分析一下Mapper接口的查询。不难发现,核心逻辑肯定在getMapper中。

2.1 SqlSession.getMapper

public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}

DefaultSqlSession的getMapper方法比较简答,其实就是调用Configuration的getMapper方法。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

Configuration类中getMapper方法的实现是调用MapperRegistry的getMapper方法。

关于MapperRegistry是不是比较熟悉,好像在之前的文章中介绍过。对,其实就是在上篇文章Mybatis源码解读『SQL映射文件解析』中,SQL映射文件解析完成后,为nameSpace绑定Mapper接口,在bindMapperForNamespace方法中,会调用MapperRegistry的addMapper方法。

private void bindMapperForNamespace() {
    // 获取映射文件的命名空间
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 根据命名空间解析 mapper 类型
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
        }
        if (boundType != null) {
            // 检测当前 mapper 类是否被绑定过
            if (!configuration.hasMapper(boundType)) {
                configuration.addLoadedResource("namespace:" + namespace);
                // 绑定 mapper 类
                configuration.addMapper(boundType);
            }
        }
    }
}

// Configuration
public <T> void addMapper(Class<T> type) {
    // 通过 MapperRegistry 绑定 mapper 类
    mapperRegistry.addMapper(type);
}

// MapperRegistry
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 {
            /*
             * 将 type 和 MapperProxyFactory 进行绑定,MapperProxyFactory 可为 mapper 接口生成代理类
             */
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // 解析注解中的信息
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

也就是说每个SQL映射XML文件解析结束,会调用一次Configuration.addMapper方法,进而向Configuration的成员mapperRegistry中添加一个type(等价于向MapperRegistry的knownMappers添加一个type -> MapperProxyFactory对)。

介绍完MapperRegistry中添加Mapper,我们来看一下getMapper方法,其实就是addMapper方法的反操作。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 获取Mapper接口type对应的MapperProxyFactory(MapperProxyFactory是在SQL映射文件解析结束后添加的)
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 调用MapperProxyFactory的newInstance方法生成Mapper接口实例对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

可以看到getMapper方法本质其实是调用MapperProxyFactory的newInstance方法,返回了一个Mapper接口的实例对象。

2.2 MapperProxyFactory

public class MapperProxyFactory<T> {

  // Mapper接口Interface
  private final Class<T> mapperInterface;
  // Mapper接口内方法缓存
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  // 生成Mapper接口代理对象
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

到这,我们大致可以明白,为什么通过Mapper接口,不定义实现类,就可以使用接口中定义的方法,因为Mybatis帮我们定义的Mapper接口生成了代理对象,至于代理的实现细节,我们接下来再介绍。

2.2.1 JDK动态代理

上面Mapper接口的代理对象生成,使用了JDK动态代理。为了更方便理解,我们再回顾一下JDK动态代理,更详细的介绍可以去看之前的文章彻底搞懂动态代理。我们知道,JDK动态代理需要如下几步:

  1. 创建接口Subject及委托类RealSubject(实现了Subject接口)
  2. 创建一个实现InvocationHandler接口的类,实现invoke方法(定义代理逻辑)
  3. 通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)创建一个代理对象
  4. 使用代理对象调用方法
  • 定义一个Subject接口和委托类RealSubject
public interface Subject {
    String SayHello(String name);

    String SayGoodBye();
}
public class RealSubject implements Subject {
    @Override
    public String SayHello(String name) {
        return "hello " + name;
    }

    @Override
    public String SayGoodBye() {
        return "good bye!";
    }
}
  • 创建一个实现InvocationHandler接口的类
public class DynamicProxy implements InvocationHandler {

    private Object object;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在代理方法执行前添加一些自己的操作
        System.out.print("在调用之前,打印方法名称:");
        System.out.println("Method:" + method);

        //委托类方法执行
        Object obj = method.invoke(object, args);

        //在代理方法执行后添加一些自己的操作
        System.out.println("方法执行结束");

        return obj;
    }

    public DynamicProxy(Object object) {
        this.object = object;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
                object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                this

        );
    }
}

实现的invoke方法,其实就是代理方法的执行逻辑。我们这invoke方法中对于委托类方法进行了“加强”,在委托类方法执行前打印方法名称,在委托类方法执行结束,打印一段文字”good bye!”。另外我们可以看到,委托类方法的执行是通过反射完成的,就是invoke方法中的method.invoke(object, args)。

这里我们再添加一个工厂类,来获取代理对象:

public class SubjectProxyFactory {
    public static Subject newJdkProxy() {
        //代理RealSubject
        DynamicProxy dynamicProxy = new DynamicProxy(new RealSubject());
        return dynamicProxy.getProxy();
    }
}
  • 测试代理功能
public static void main(String[] args) {
    //获取代理对象
    Subject subject = SubjectProxyFactory.newJdkProxy();

    System.out.println("动态代理对象的类型:" + subject.getClass().getName());

    String hello = subject.SayHello("zhuoli");
    System.out.println(hello);
    System.out.println("-------------------------");
    String goodbye = subject.SayGoodBye();
    System.out.println(goodbye);
}

输出结果:

动态代理对象的类型:com.sun.proxy.$Proxy0
在调用之前,打印方法名称:Method:public abstract java.lang.String com.zhuoli.service.thinking.java.proxy.jdkproxy.Subject.SayHello(java.lang.String)
方法执行结束
hello zhuoli
-------------------------
在调用之前,打印方法名称:Method:public abstract java.lang.String com.zhuoli.service.thinking.java.proxy.jdkproxy.Subject.SayGoodBye()
方法执行结束
good bye!

使用起来很简单,同时可以发现,我们上面Mapper接口使用JDK动态代理,有一点不符合常规JDK动态代理的要求,那就是——委托类没有实现类,只是个接口。那么在InvocationHandler中invoke方法,委托类接口的方法调用(Mapper接口中的定义的方法)如何调用?

其实仔细想想,也是可以走得通的,我们Mapper接口中定义的方法实现在什么地方?其实就是我们SQL映射文件中通过<select>|<update>|<insert>|<delete>的一些列sql语句。通过之前的文章,我们知道这些标签已经被我们解析为MappedStatement,并存入到Configuration中。只要这些MappedStatement能生效,那么其实Mapper接口中的方法就有”实现”了。

2.2.1 MapperProxy

接下来我们来看一下动态代理中,非常重要的一个模块InvocationHandler,这里其实是MapperProxy。

// 生成Mapper接口代理对象
public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

由于MapperProxy类中,并没有一个成员变量是我们的Mapper接口实现类(实际上也没有实现类),那么代理逻辑是如何实现的,核心就在invoke方法中。

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 {
      // 如果非Object类定义的方法(Mapper接口中定义的方法),则通过如下方式实现调用
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

如果调用的委托类的方法是Object类中的方法,则直接反射调用,不用做特殊处理。否则,则需要通过cachedInvoker方法获取MapperMethodInvoker,然后调用MapperMethodInvoker的invoke方法实现对Mapper接口中某个方法的调用。

// private final Map<Method, MapperMethodInvoker> methodCache;

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    // A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
    // It should be removed once the fix is backported to Java 8 or
    // MyBatis drops Java 8 support. See gh-1929
    // 尝试从缓存中获取方法对应的MapperMethodInvoker
    MapperMethodInvoker invoker = methodCache.get(method);
    // 如果缓存命中,直接返回
    if (invoker != null) {
      return invoker;
    }

    // 如果缓存未命中,则构建MapperMethodInvoker并添加到缓存中
    return methodCache.computeIfAbsent(method, m -> {
      // 如果方法是default方法
      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方法,生成PlainMethodInvoker
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

cachedInvoker方法,就跟它的方法名一样,就是从缓存中获取MapperMethodInvoker对象,这个缓存其实是MapperProxy类的成员变量methodCache,类型为Map(Method -> MapperMethodInvoker)。如果缓存未命中,会构建一个MapperMethodInvoker对象,并将Method -> MapperMethodInvoker对添加到缓存methodCache中

关于构建MapperMethodInvoker对象的逻辑,分为两个分支,即方法是否为default方法,当方法不是default方法是,其实就会构建一个PlainMethodInvoker对象。

private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;

  public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}

可以看到PlainMethodInvoker的核心其实是其成员变量MapperMethod,其invoke方法也是调用MapperMethod的invoke方法实现的。那么MapperMethod又是什么?

我们上面再生成PlainMethodInvoker时,通过调用MapperMethod的构造函数方法,生成了一个MapperMethod对象。

public class MapperMethod {

  // Mapper接口的某个方法对应的sql信息,比如id,sql类型(INSERT, UPDATE, DELETE, SELECT等)
  private final SqlCommand command;
  // Mapper接口的某个方法签名,比如方法的返回值类型,以及参数解析器等
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
}

所以MapperMethod包含了两部分信息:

  • Mapper接口方法,底层sql的基本信息(SqlCommand)
  • Mapper接口方法,方法签名(MethodSignature)

接下来分别看一下这两个类,首先是SqlCommand,它包含了Mapper接口底层方法的基础信息,即id + SqlCommandType,我们来看一下其构造函数。

public static class SqlCommand {
    //name为MappedStatement的id,也就是namespace.methodName,
    //也是我们之前SQL映射文件解析过程中获取的
    // 比如com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper.queryAllUser
    private final String name;
    //SQL的类型,如INSERT, UPDATE, DELETE, SELECT等
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
        //拼接Mapper接口名和方法名,nameSpace(等价于mapperInterface) + "." + methodName
        String statementName = mapperInterface.getName() + "." + method.getName();
        MappedStatement ms = null;
        //检测configuration是否有key为com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper.queryAllUser的MappedStatement
        if (configuration.hasStatement(statementName)) {
            //获取MappedStatement
            ms = configuration.getMappedStatement(statementName);
        } else if (!mapperInterface.equals(method.getDeclaringClass())) {
            String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
            if (configuration.hasStatement(parentStatementName)) {
                ms = configuration.getMappedStatement(parentStatementName);
            }
        }
        
        // 检测当前方法是否有对应的MappedStatement
        if (ms == null) {
            if (method.getAnnotation(Flush.class) != null) {
                name = null;
                type = SqlCommandType.FLUSH;
            } else {
                throw new BindingException("Invalid bound statement (not found): "
                    + mapperInterface.getName() + "." + methodName);
            }
        } else {
            // 设置name和type 变量
            name = ms.getId();
            type = ms.getSqlCommandType();
            if (type == SqlCommandType.UNKNOWN) {
                throw new BindingException("Unknown execution method for: " + name);
            }
        }
    }
}

逻辑也比较简单,其实就是根据接口和方法信息,从Configuration中获取方法对应的MappedStatement(非常重要,因为MappedStatement其实等价于我们Mapper接口的“实现类”),通过MappedStatement初始化SqlCommand。从这个方法,我们也可以得出一个结论,SQL映射文件中nameSpace必须为Mapper接口的interfaceName

然后来看一下方法签名MethodSignature,它包含了Mapper接口某个方法的返回值类型,返回值信息,以及参数解析器等,来看一下其构造函数。

public static class MethodSignature {

  private final boolean returnsMany;
  private final boolean returnsMap;
  private final boolean returnsVoid;
  private final boolean returnsCursor;
  private final boolean returnsOptional;
  private final Class<?> returnType;
  private final String mapKey;
  private final Integer resultHandlerIndex;
  private final Integer rowBoundsIndex;
  private final ParamNameResolver paramNameResolver;

  public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 通过反射解析方法返回类型
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
    if (resolvedReturnType instanceof Class<?>) {
      this.returnType = (Class<?>) resolvedReturnType;
    } else if (resolvedReturnType instanceof ParameterizedType) {
      this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
    } else {
      this.returnType = method.getReturnType();
    }

    // 检测返回值类型是否是 void、集合或数组、Cursor、Optional、Map等
    this.returnsVoid = void.class.equals(this.returnType);
    this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
    this.returnsCursor = Cursor.class.equals(this.returnType);
    this.returnsOptional = Optional.class.equals(this.returnType);
    // 解析@MapKey注解,获取注解内容
    this.mapKey = getMapKey(method);
    this.returnsMap = this.mapKey != null;

    // 获取RowBounds参数在参数列表中的位置,如果参数列表中包含多个RowBounds参数,会抛出异常
    this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
    // 获取ResultHandler参数在参数列表中的位置
    this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
    // 获取参数解析器
    this.paramNameResolver = new ParamNameResolver(configuration, method);
  }
}

了解了SqlCommand和MethodSignature后,我们对MapperMethod所有成员的介绍就结束了。接下来介绍MapperMethod的核心逻辑,如果通过MapperMethod实现代理逻辑的。我们上面介绍过MapperProxy(InvocationHandler)中,重写的invoke方法其实是调用PlainMethodInvoker的invoke方法实现的,而PlainMethodInvoker的invoke方法其实是调用的MapperMethod的execute方法,如下:

return cachedInvoker(method).invoke(proxy, method, args, sqlSession);

private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;

  public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}

所以接下来重点来看一下MapperMethod的execute方法。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 获取方法对应的SQL类型(INSERT, UPDATE, DELETE, SELECT等)
  switch (command.getType()) {
    case INSERT: {
      // 使用参数解析器paramNameResolver解析方法参数
      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()) {
        // 返回void的查询操作
        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);
        // 返回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;
}

在execute方法中,最后都会通过调用SqlSession的方法,而SqlSession中的方法其实最终就是我们在SQL映射文件中定义的sql(MappedStatement)起作用的。所以为什么我们不用定义实现类,就能通过Mapper接口实现查询?

  • Mapper接口中的任何方法,在SQL映射文件中都有定义其“实现”
  • Mybatis帮我们的Mapper接口生成了代理对象,而代理逻辑其实就是执行SQL映射文件中定义的SQL

关于SqlSession的方法,比如上述sqlSession.selectOne方法中,MappedStatement是如何生效的,我们接下来再详细介绍。在本篇文章最开始介绍的使用Mybatis执行查询操作的第二种方式,其实就是调用SqlSession中的方法。

参考链接:

1. Mybatis源码

2. Mybatis之Mapper接口如何执行SQL

赞(1) 打赏
Zhuoli's Blog » Mybatis源码解读『select查询——使用Mapper接口』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址