likes
comments
collection
share

Mybatis流程分析(六): Mybatis中方法和sql语句的桥梁——MapperProxy

作者站长头像
站长
· 阅读数 25

本系列文章皆在从细节着手,由浅入深的分析Mybatis框架内部的处理逻辑,带你从一个全新的角度来认识Mybatis的工作原理。

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜


前言

(注:不熟悉Mybatissql语句和接口方法绑定细节的小伙伴可翻阅专栏之前的文章进行了解~~)

本章我们将主要介绍Mybatis中为什么操纵口实例对象方法就可以完成对数据库操纵。即分析如图所示的<3>执行接口方法,就能执行方法所绑定的sql语句的背后逻辑。

Mybatis流程分析(六): Mybatis中方法和sql语句的桥梁——MapperProxy

在开始分析之前,我们先来看看动态代理相关的知识。此时,可能你可能会疑惑,我想知道的是Mybatis内部为什么通过调用接口中的方法,就能完成对数据库操作的,怎么现在又开始分析动态代理了?

如果你有这样的疑惑,先别急,等我慢慢来给你分析。

动态代理

代理模式主要用于完成低侵⼊式的功能的扩展。进一步,实现代理的方式又可以分为:静态代理和动态代理两种类型。 其中静态代理的实现相对简单,大致逻辑如下:

  1. 编写⼀个代理类实现与⽬标对象相同的接⼝
  2. 在该代理类内部维护⼀个⽬标对象的引⽤。代理类通常会通过构造器来塞⼊⽬标对象
  3. 代理对象中调⽤与⽬标对象的同名⽅法,并方法执行前后添加前拦截,后拦截等所需的业务功能。

而对于动态的代理通常也有两种方式:

  1. 基于接口的动态代理(使用Jdk中的Proxy :这种方式要求目标对象必须实现一个或多个接口,代理对象则是通过 Proxy.newProxyInstance(...) 方法创建。

  2. 基于类的动态代理(使用CGLIB库) :这种方式可以代理没有实现任何接口的类。它通过继承来实现代理。

(注:因为Mybatis中是的Java中基于接口形式的动态代理,所以我们主要介绍Java中动态代理的相关内容)。

  1. 创建实现 InvocationHandler 接口的代理处理类: 首先,你需要创建一个类,实现 InvocationHandler 接口。这个类将定义代理对象的行为,包括在原始方法执行前后插入的逻辑。这个类的 invoke 方法会在代理对象的方法被调用时被触发,你可以在其中编写自定义的逻辑。
  2. 创建代理对象: 使用 Proxy.newProxyInstance(...) 方法来创建代理对象。该方法需要传入目标类的类加载器、目标类实现的接口列表以及之前创建的 InvocationHandler 实例。
  3. 调用代理对象的方法: 创建代理对象后,通过调用代理对象的方法来触发代理处理类的 invoke 方法。在 invoke 方法中,你可以根据需要在方法调用前后添加自定义逻辑。

事实上,所谓的动态代理类就是在运⾏时⽣成指定接⼝的代理类

Jdk中动态代理的实现有两个核心要素: InvocationHandler公共接⼝。 具体来看,每个代理实例(即实现需要代理的接⼝)都有⼀个关联的调⽤处理程序对象,此对象会实现InvocationHandler接口,并将相关的增强逻辑都定义在InvocationHandler类中的invoke⽅法之内。

MapperProxy 相关的逻辑

public class MapperProxyFactory<T> {

  // 待实现的接口信息
  private final Class<T> mapperInterface;
  
  // ....省略其他无关代码


  @SuppressWarnings("unchecked")
  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);
  }

}

进一步,其整个调用过程如下所示:

Mybatis流程分析(六): Mybatis中方法和sql语句的桥梁——MapperProxy

接下来,我们便看看MapperProxyFacory中的newInstance方法内部到底做了哪些工作。其内部代码如下:

MapperProxyFacory # newInstance()

protected T newInstance(MapperProxy<T> mapperProxy) {
    // 动态代理的逻辑
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

可以看到,newInstance构建对象的方式使用了我们之前介绍的的Jdk中的动态代理进行实现。此外,我们还注意到在使用Proxy构建代理对象时,其中方法newProxyInstance需要如下三个参数:

  1. 类加载器ClassLoader
  2. 接口数组Class[]{}
  3. MapperProxy

不难发现,MapperProxy在上述使用过程中会作为第三个参数进行传入,根据我们之前对于Jdk动态代理机制的分析,此时我们有理由猜测,不管MapperProxy内部逻辑如何复杂,其一定会实现InvocationHandler接口,并同时实现InvocationHandler中的invoke方法。

InvocationHandler中的invoke方法其实相当于逻辑的增强处,代理类的增强逻辑基本都会在此进行实现。

至此,虽然我们还没有分析MapperProxy的相关内容,但通过我们对于Jdk动态代理机制的理解,其实我们已经知道了对于MapperProxy类我们应该关注的重点——invoke方法。

进一步,MapperProxy类的相关代码如下:

MapperProxy


public class MapperProxy<T> 
            implements InvocationHandler, Serializable {
  // sqlSession会话信息
  private final SqlSession sqlSession;
  // getMapper使用时传入的Mapper接口信息
  private final Class<T> mapperInterface;

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
      if (Object.class.equals(method.getDeclaringClass())) {
        // 如果getDeclaringClass方法信息则直接进行调用
        // 例如:toString,equals等
        return method.invoke(this, args);
      } else {
        // 执行Mapper接口中定义的方法
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
   }
}

可以看到,MapperProxyinvoke方法的逻辑大致如下:

  1. 如果执行方法为Object类型中的方法,则无任何增强逻辑,直接执行;
  2. 如果方法为Mapper接口中所定义的方法,则执行逻辑又委托给cacheInvoker进行执行。

相信读到此处的你一定会有一种恍然大悟的感觉。原来在Mybatis中,我们通过getMapper返回一个实例对象,调用其内部的方法。进而调用到方法所对应的sql的语句。这背后的一切原因都依赖于MapperProxy中的invoke方法。更具体一点,其调用过程其实逻辑是委托给方法cachedInvoker来完成的。

cachedInvoker又会执行哪些逻辑呢?接下来,我们便进入到cachedInvoker中,看看其相应的逻辑。

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    
      // 此处是一个lambda表示
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          }  else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    
  }

可以看到cachedInvoker在返回对象时,会使用到一个lambda表达式,相关逻辑无非就是根据条件返回不同的MapperMethodInvoker的实现。

看来如果我们要明白MapperProxyinvoke逻辑,我们便需要进入到 MapperMethodInvoker实现类中的invoke方法。进一步,对于MapperMethodInvoker而言,其主要有两个默认实现,一个为DefaultMethodInvokerPlainMethodInvoker。此处我们仅选取其中的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内部,invoke方法又会将逻辑委托给MapperMethodexecute方法。

MapperMethod

事实上,在 MyBatisMapperMethod 是一个重要的内部类。它负责将 Java 接口中的方法映射为实际的 Sql 操作。MapperMethod 的作用是解析接口方法的元数据,包括方法名、参数等信息,并根据这些信息生成对应的 Sql 语句。总结来看,有其内容如下:

  1. 作用MapperMethod 负责将接口中的方法转换为实际的 Sql操作。它根据方法的名称、参数类型等信息,动态生成执行的 Sql语句,并执行查询、更新等操作。

  2. 工作原理: 当你调用代理对象的接口方法时,代理会将方法调用传递给 MapperMethod,它会根据方法名和参数类型等信息,决定执行的 Sql 操作。MapperMethod 会构建一个 MappedStatement 对象,该对象包含了 Sql 语句以及其他执行相关的信息。

  3. 结构MapperMethod 主要包含以下属性:

    • SqlCommand: 表示 Sql 操作的类型,比如SELECT、INSERT、UPDATE、DELETE等。
    • MethodSignature: 用于描述接口方法的签名,包括方法名、参数类型等。
    • SqlSource: 用于生成 Sql 语句的源信息。

(注:MappedStatement 相关信息我们在前一章有过介绍)


public class MapperMethod {

// .... 省略其他无关代码

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
     
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
        }
      // ....省略其他相似逻辑的d代码
    return result;
  }


private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    // 相当于对sql内容进行封装
    MappedStatement ms = sqlSession.getConfiguration().
    Object param = method.convertArgsToSqlCommandParam(args);
    
    // 通过sqlSession中的select方法进行执行
    sqlSession.select(command.getName(), param, method.extractResultHandler(args));
  }

}

可以看到,当我们调用接口中相关方法时,其本质是从Configurtaion对象中获取缓存的MappedStatement对象,提取出其中的sql信息,然后将sql执行逻辑委托于SqlSession来进行执行。

至此,我们应该明白。在Mybatis中,传入一个Mapper接口,Mybatis内部就会通过代理的方式为我们生成一个该接口的代理对象——MapperProxy。进一步,当调用接口中方法时,会调用到对象中的方法所绑定的sql语句。

事实上,结合之前的文章:

再加上本章,我们已经利用三章的篇幅来叙述MybatisgetMapper的相关逻辑。虽然看着很多,但却可以通过如下的一张图来进行总结。 Mybatis流程分析(六): Mybatis中方法和sql语句的桥梁——MapperProxy

总结

事实上,MybatisgetMapper获取实例对象的背后的逻辑就是 通过动态代理的方式生成一个实现该接口的代理类。进一步,调用该实例对象执行对应sql的背后的逻辑也全部交给了SqlSession来处理。至于SqlSession中是如何执行sql的且听后续分解~~

读源码,分析源码本身就是一件枯燥的事情。作者在行文排版上已经尽可能减少代码的的排版,因为作者觉得大段的粘贴代码只会降低行文的可读性。事实上,作者更喜欢用图示的信息来展现代码间的调用逻辑。希望文章中的图能对你理解MyBatis有所帮助。

此外,读源码的本身并不是让我们再复现一个框架,读源码等多的是窥探源码的中的设计以及让我们可以更加深刻的认清楚框架的"底层"逻辑,好让你在工作中快速定位问题。

最后,还是希望文章能给你带来一点收获,毕竟花费时间来看文章本身就是一种对作者的信任,真的很感谢你们的信任。我所能做就是不断提升文章的质量,让读者能真正有所收获。🌹

我是毅航,一名练习时长两年半的六十九岁扶墙java开发。针对文章如果有疑问的话可以在评论区留言,作者抽空会逐一回复。🙋

共勉,一起成长。