Mybatis映射接口的动态代理实现原理

news2024/11/13 9:07:21

Mybatis映射接口的动态代理实现原理

在上一节中,我们介绍了MyBatis的核心配置文件加载流程,Mybatis核心配置文件加载流程详解
在文中,我们介绍了MyBatis在加载配置文件的过程中会针对每个接口类都生成一个相应的MapperProxyFactory动态代理工厂类。

在MapperRegistry类中有一个叫做knownMappers的map缓存,其键为映射接口的Class对象,值为MapperProxyFactory对象,其有一个mapperInterface属性用来保存需要创建代理对象的接口类。

在MyBatis中,我们通过调用sqlSession.getMapper方法可以获取映射接口的动态代理对象,然后调用该映射接口定义的方法执行数据库操作时,就会去映射配置文件中找到该方法对应的SQL语句进行执行。

那么,MyBatis是如何实现上述流程的呢?接下来,我们就从源码中来一探究竟。

下面列出了测试主方法:

public class MybatisDemo {
    private static UserMapper userMapper;

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        userMapper = sqlSession.getMapper(UserMapper.class);

        User u1 = userMapper.selectUserById(1);
        System.out.println(u1);
    }
}

因为动态代理的生成步骤是在sqlSession.getMapper方法中,因此,我们先进入DefaultSqlSession类下的到getMapper方法的源码中来看一下:

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

这里会调用Configuration类下的getMapper方法,type即为映射接口类。然后我们进入到Configuration类下的getMapper方法:

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

这里调用了本文初所提及的MapperRegistry类下的getMapper方法,再进入:

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);
  }
}

这里,我们就看到了本文初介绍的,MyBatis在MapperRegistry类中会维护一个knownMappers的Map缓存,用来存储在解析核心配置文件的过程中,为每个映射接口生成的代理工厂类对象。
因此,这里就是根据被调用的映射接口,直接从该缓存中获取相应的MapperProxyFactory动态代理工厂类对象,如果获取不到则直接报错提示。

在获取到mapperProxyFactory后,调用newInstance方法来创建一个mapperProxy动态代理对象。接下来,我们进入到MapperProxyFactory类中观察一下它的实现逻辑,以及newInstance方法:

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

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

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

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

  @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);
  }

}

MapperProxyFactory中包含了两个属性:

  • mapperInterface:需要创建代理对象的映射接口
  • methodCache:是一个map缓存,其键为映射接口的方法对象,值为这个方法对应的MapperMethodInvoker。实际上,SQL的执行最终会由MapperMethodInvoker方法来完成,后面会详细说明。

再观察MapperProxyFactory中的两个重载的newInstance方法,可以看到最终重载方法会通过执行Proxy.newProxyInstance来创建映射接口的动态代理对象。而基于JDK动态代理的原理我们知道,代理对象方法的执行最终是在其实现的InvocationHandler接口的invoke方法中被执行的。而观察上面的代码不难发现,MapperProxy类必定是InvocationHandler接口的实现类:
在这里插入图片描述

果然,MapeprProxy类实现了InvocationHandler接口,并在创建MapperProxy时,MapperProxyFactory会将其持有的methodCache传递给MapperProxy,因此methodCache的实际读写是由MapperProxy来完成的。

下面来看一下MapperProxy实现的invoke方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (method.isDefault()) {
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  // 从methodCache中根据方法对象获取MapperMethodInvoker来执行sql
  // 如果获取不到,则创建一个MapperMethodInvoker并添加到methodCache中,再执行Sql
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
  return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

通过上面的代码我们可以知道之前提及的methodCache属性的作用,其实就是缓存了每个方法相应的MapperMethod对象,而MapperMethod对象是做什么用的呢?接下来,我们继续进入到execute方法:

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()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } 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;
    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;
}

喔!这里我们看到几个case标签不就是对应到我们执行SQL的几种类别的操作吗,看来代理对象方法的执行最终会进入到MapperMethodexecute方法里,并根据方法对应SQL的类别执行相应的处理逻辑。那么是如何根据映射接口的方法来得到SQL的执行类别呢?

很明显,就是要根据映射接口的方法来获取到映射配置文件中相应的SQL标签,而这种关联就要利用到MappedStatement对象了。在前文MyBatis的核心配置文件加载过程中,我们详细分析了,在MyBatis加载的过程中,会对映射配置文件中的每一个<select><insert><update><delete>标签根据其namespace+id值生成一个MappedStatement对象,存储在Configuration中。因此,要实现上面的关联,只需要根据映射接口类名+方法名,从Configuration中获取到相应的MappedStatement对象即可。

那么MyBatis的实际实现是不是正如我们上面的分析所料呢?答案就在MapperMethod的构造方法中:

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

在该构造方法中,会根据传入的映射接口的Class对象,映射接口被调用方法以及配置类Configuration来创建SqlCommandMethodSignature对象。

  • SqlCommand主要是保存和映射接口被调用方法所关联的MappedStatement的信息;
  • MethodSignature主要是存储映射接口被调用方法的参数信息和返回值信息。

来看一下SqlCommand的构造方法:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  // 获取映射接口被调用方法的方法名
  final String methodName = method.getName();
  // 获取被调用方法的接口的Class对象
  final Class<?> declaringClass = method.getDeclaringClass();
  // 获取和映射接口被调用方法关联的MappedStatement对象
  MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
      configuration);
  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 {
    // 将MappedStatement的id值赋值给SqlCommand的name字段
    name = ms.getId();
    // 将MappedStatement的Sql命令类别赋值给SqlCommand的type字段
    // 比如SELECT、INSERT、UPDATE、DELETE
    type = ms.getSqlCommandType();
    if (type == SqlCommandType.UNKNOWN) {
      throw new BindingException("Unknown execution method for: " + name);
    }
  }
}

构造函数中主要做了这些事情:

  • 先获取和被调用方法关联的MappedStatement对象;
  • 然后将MappedStatementid字段赋值给SqlCommandname字段;
  • 最后将MappedStatementsqlCommandType字段赋值给SqlCommandtype字段。
    这样一来,SqlCommand就具备了和被调用方法关联的MappedStatement的信息。那么如何获取和被调用方法关联的MappedStatement对象呢,继续看resolveMappedStatement() 的实现,如下所示:
  private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
      Class<?> declaringClass, Configuration configuration) {
    // 根据接口全限定类名+"."+方法名拼接出MappedStatement的id
    String statementId = mapperInterface.getName() + "." + methodName;
    if (configuration.hasStatement(statementId)) {
      // 根据statementId获取configuration中保存的MappedStatement对象
      return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
      return null;
    }
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
      if (declaringClass.isAssignableFrom(superInterface)) {
        // 递归调用 
        MappedStatement ms = resolveMappedStatement(superInterface, methodName,
            declaringClass, configuration);
        if (ms != null) {
          return ms;
        }
      }
    }
    return null;
  }
}

终于在这里,我们就可以利用源码来佐证我们上面的分析了!实际上,MyBatis就是根据被调用映射接口的类名+方法名拼接成statementId,并利用该statementId从Configuration对象中获取到相应的MappedStatement对象。这也就是为什么在MyBatis中老生常谈的问题:映射配置文件中的<select>等标签id要跟映射接口的方法名保持一致,且映射配置文件的<mappers namespace=xxx>namespace属性要写成映射接口的全限定类名。

上面代码中有一处递归调用逻辑,可能比较难以理解。这里简单介绍下,实际上这么做的目的是:

MyBatis3.4.2及以前的版本,只会根据映射接口的全限定名 + “.” + 方法名声明被调用方法的接口的全限定名 + “.” + 方法名ConfigurationmappedStatements缓存中获取MappedStatement,那么按照这样的逻辑,如果是某些继承映射接口的子接口类是无法获取到MappedStatement的,因此在MyBatis3.4.3及之后的版本中,采用了resolveMappedStatement() 方法中的逻辑,以支持继承了映射接口的接口对应的SqlCommand也能和映射接口对应的MappedStatement相关联

对于SqlCommand的分析到此为止。

至此,回到上面定位到MapperMethod类中的execute方法中,我们就知道了MyBatis对于映射接口的动态代理实现原理,以及执行流程。最终在execute方法中,会根据被调用方法关联的SQL执行类别,将方法传入的参数进行转换,最终在SqlSession中调用相应的方法来执行SQL语句。

总结一下本文的主要内容如下:

  1. 动态代理实现原理
    在MyBatis启动加载核心配置文件时,会对每个映射接口生成一个MapperProxyFactory对象,并保存在MapperRegistry对象的knownMappers缓存中,便于后续使用。当调用SqlSession.getMapper方法来获取映射接口的代理对象时,会从MapperRegistry对象的knownMappers缓存中获取该映射接口对应的动态代理工厂类,该工厂类会通过Proxy.newProxyInstance方法来创建一个代理对象。同时,会创建一个MapperProxy对象在invoke方法中执行真正的被调用方法执行逻辑。在invoke方法中,会获取到该调用方法对应的MapperMethod对象,并在其execute方法中结合SqlSession来执行具体的增删改查逻辑。

  2. MyBatis中的动态代理是对接口的代理
    MyBatisJDK动态代理中,是不存在被代理对象的,是对接口的代理。MapperProxy实现了InvocationHandler接口,因此MapperProxyMyBatisJDK动态代理中扮演调用处理器的角色,即调用映射接口的方法时,实际上是调用的MapperProxy实现的invoke() 方法,又因为不存在被代理对象,所以在MapperProxyinvoke() 方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成MapperMethod并执行MapperMethodexecute() 方法,即调用映射接口的方法的请求会发送到MapperMethod
    可以理解为映射接口的方法由MapperMethod代理

最后,用一张图归纳一下MyBatis中的动态代理执行流程,如下所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1305287.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【异常解决】SpringBoot + Maven 在 idea 下启动报错 Unable to start embedded Tomcat(已解决)

Unable to start embedded Tomcat&#xff08;已解决&#xff09; 一、背景介绍二、原因分析2.1 网络上整理2.2 其他原因 三、解决方案 一、背景介绍 spring boot(v2.5.14) maven idea 启动项目 之前项目一直启动的好好的&#xff0c;都能正常运行。重启的时候突然就不能启…

单元测试技术

文章目录 一、单元测试快速入门二、单元测试断言三、Junit框架的常用注解 一、单元测试快速入门 所谓单元测试&#xff0c;就是针对最小的功能单元&#xff0c;编写测试代码对其进行正确性测试。 常规的例如如果在main中测试&#xff0c;比如说我们写了一个学生管理系统&…

MSPM0L1306例程学习-ADC部分(1)

MSPM0L1306例程学习-ADC部分(1) MSPM0L1306例程学习 使用的TI的官方例程&#xff0c;即SDK里边包含的例程代码。 MCU使用的是MSPM0L1306, 对于ADC部分&#xff0c;有10个例程&#xff1a; 例程理解 ADC的转换有多种工作模式&#xff0c;从最简单的单通道单次转换开始入手…

C++共享和保护——(1)作用域

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 人生就像骑单车&#xff0c;要想平衡就…

Java - Spring中Bean的循环依赖问题

什么是Bean的循环依赖 A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你&#xff0c;你也依赖我。 比如&#xff1a;丈夫类Husband&#xff0c;妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。 Spring解决循环依赖的机理 Spring为什么可以解决set s…

ubuntu如何远程ssh登录Windows环境并执行测试命令

ubuntu如何远程ssh登录Windows环境并执行测试命令 1 paramiko模块简介1.1 安装paramiko1.2 paramiko基本用法1.2.1 创建SSHClient实例1.2.2 设置主机密钥策略1.2.3 连接SSH服务器1.2.4 执行命令1.2.5 关闭SSH连接1.2.6 异常处理 2 windows的配置2.1 启动OpenSSH服务2.2 配置防火…

【Qt开发流程】之2D绘图1:概述及基本绘制与填充和渐变填充

概述 Qt的绘图系统可以使用相同的API在屏幕和打印设备上进行绘图&#xff0c;并且主要基于QPainter, QPaintDevice和QPaintEngine类。 QPainter用于执行绘图操作&#xff0c;QPaintDevice是一个二维空间的抽象&#xff0c;可以使用QPainter在其上绘制&#xff0c;QPaintEngine…

Android : XUI- SimpleImageBanner+BannerItem带标题的轮播图-简单应用

示例图&#xff1a; 1.导入XUI http://t.csdnimg.cn/qgGaN 2.MainActivity.java package com.example.viewpagerbanne;import android.os.Bundle; import android.view.View; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.xu…

vue项目中 CDN 是vue本身的依赖可以按需加载还是项目中所有的第三方库都可以按需加载?

这是我看到CDN简介时产生的问题 相信很多小伙伴会有 和我一样的疑问 在这里 我也统一回答一下 CDN&#xff08;内容分发网络&#xff09;是一种通过将数据分发到全球各个节点&#xff0c;以提供快速、可靠的内容传输的技术。在Vue项目中&#xff0c;CDN可以用于按需加载Vue本…

Linux--操作系统

1. 常见的操作系统 Windowsmac OSLinuxiOSAndroid 2. 操作系统的定义 操作系统直接运行在计算机上的系统软件&#xff0c; 它是控制硬件和支持软件运行的计算机程序。 3. 操作系统的作用 向下控制硬件向上支持软件的运行&#xff0c;具有承上启下的作用。 4.总结 操作系统…

Server check fail, please check server xxx.xxx.xxx.xxx,port 9848 is available

记录一次服务调用中的错误 背景&#xff1a;我使用了nacos2.x的版本&#xff0c;同时在同一台服务器的三个docker容器中部署了nacos1、2、3&#xff0c;并将它们连接到了同一个docker网络 错误&#xff1a;Server check fail, please check server xxx.xxx.xxx.xxx,port 9848 …

事件驱动架构 vs. RESTful架构:通信模式对比与选择

1. 通信风格 事件驱动架构&#xff08;EDA&#xff09; 是一种异步通信风格&#xff0c;组件之间通过产生和消费事件进行通信。 事件是表示系统中重大变化或事件的消息&#xff0c;并分发给感兴趣的组件。这种通信模型允许系统的不同部分之间进行解耦和动态交互。 组件充当事件…

MTK Android P Sensor架构(一)

需求场景&#xff1a; 本来如果只是给传感器写个驱动并提供能读取温湿度数据的节点&#xff0c;是一件比较轻松的事情&#xff0c;但是最近上层应用的同事要求我们按照安卓标准的流程来&#xff0c;这样他们就能通过注册一个服务直接读取传感器事件数据了。这样做的好处就是第…

从手工测试进阶中高级测试?如何突破职业瓶颈...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、手工测试如何进…

【教3妹学编程-算法题】需要添加的硬币的最小数量

3妹&#xff1a;2哥2哥&#xff0c;你有没有看到新闻&#xff0c; 有人中了2.2亿彩票大奖&#xff01; 2哥 : 看到了&#xff0c;2.2亿啊&#xff0c; 一生一世也花不完。 3妹&#xff1a;为啥我就中不了呢&#xff0c;不开心呀不开心。 2哥 : 得了吧&#xff0c;你又不买彩票&…

Vue 双向绑定:让数据与视图互动的魔法!(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

人体关键点检测3:Android实现人体关键点检测(人体姿势估计)含源码 可实时检测

目录 1. 前言 2.人体关键点检测方法 (1)Top-Down(自上而下)方法 (2)Bottom-Up(自下而上)方法&#xff1a; 3.人体关键点检测模型训练 4.人体关键点检测模型Android部署 &#xff08;1&#xff09; 将Pytorch模型转换ONNX模型 &#xff08;2&#xff09; 将ONNX模型转换…

【JVM从入门到实战】(一) 字节码文件

一、什么是JVM JVM 全称是 Java Virtual Machine&#xff0c;中文译名 Java虚拟机。 JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 二、JVM的功能 解释和运行 对字节码文件中的指令&#xff0c;实时的解释成机器码&#xff0c;让计算机…

图论——二分图

图论——二分图 二分图通俗解释 有一个图&#xff0c;将顶点分成两类&#xff0c;边只存在不同类顶点之间&#xff0c;同类顶点之间设有边。称图 G 为二部图&#xff0c;或称二分图&#xff0c;也称欧图。 性质 二分图不含有奇数环图中没有奇数环&#xff0c;一定可以转换为二…

005、Softmax损失

之——softmax与交叉熵 杂谈 我们常用到softmax函数与交叉熵的结合作为损失函数以监督学习&#xff0c;这里做一个小小的总结。 正文 1.softmax的基本改进 所谓softmax就是在对接全连接层输出时候把输出概率归一化&#xff0c;最基础的就是这样&#xff1a; 效果就是这样&…