写点什么

Mybatis 中方法和 sql 语句的桥梁——MapperProxy

作者:java易二三
  • 2023-09-02
    湖南
  • 本文字数:4743 字

    阅读完需:约 16 分钟

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



在开始分析之前,我们先来看看动态代理相关的知识。此时,可能你可能会疑惑,我想知道的是 Mybatis 内部为什么通过调用接口中的方法,就能完成对数据库操作的,怎么现在又开始分析动态代理了?如果你有这样的疑惑,先别急,等我慢慢来给你分析。我们在 Mybatis 流程分析(四):Mybatis 构建 Mapper 背后的故事中曾经强调过 Mybaits 内部之所以能根据传入的接口,返回一个实现该接口的对象的原理就在于动态代理。所以为了读者能更好的理解后续的内容,此处就有必要对 Java 中的动态代理进行一个简短的介绍。动态代理代理模式主要用于完成低侵⼊式的功能的扩展。进一步,实现代理的方式又可以分为:静态代理和动态代理两种类型。 其中静态代理的实现相对简单,大致逻辑如下:


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


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


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


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


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


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


事实上,所谓的动态代理类就是在运⾏时⽣成指定接⼝的代理类。而 Jdk 中动态代理的实现有两个核心要素: InvocationHandler 和公共接⼝。具体来看,每个代理实例(即实现需要代理的接⼝)都有⼀个关联的调⽤处理程序对象,此对象会实现 InvocationHandler 接口,并将相关的增强逻辑都定义在 InvocationHandler 类中的 invoke⽅法之内。MapperProxy 相关的逻辑在之前 Mybatis 流程分析(四):Mybatis 构建 Mapper 背后的故事中我们曾分析到 Mybatis 中的 MapperProxyFactory 内部的 newInstance 方法会根据我们传入的接口信息返回一个 Mapper 实例对象。java 复制代码 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);}


}


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



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


MapperProxyFacory # newInstance()


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


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


类加载器 ClassLoader 接口数组 Class[]{}MapperProxy


不难发现,MapperProxy 在上述使用过程中会作为第三个参数进行传入,根据我们之前对于 Jdk 动态代理机制的分析,此时我们有理由猜测,不管 MapperProxy 内部逻辑如何复杂,其一定会实现 InvocationHandler 接口,并同时实现 InvocationHandler 中的 invoke 方法。而 InvocationHandler 中的 invoke 方法其实相当于逻辑的增强处,代理类的增强逻辑基本都会在此进行实现。至此,虽然我们还没有分析 MapperProxy 的相关内容,但通过我们对于 Jdk 动态代理机制的理解,其实我们已经知道了对于 MapperProxy 类我们应该关注的重点——invoke 方法。进一步,MapperProxy 类的相关代码如下:


MapperProxy


java 复制代码 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);  }
复制代码


}}


可以看到,MapperProxy 中 invoke 方法的逻辑大致如下:


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


相信读到此处的你一定会有一种恍然大悟的感觉。原来在 Mybatis 中,我们通过 getMapper 返回一个实例对象,调用其内部的方法。进而调用到方法所对应的 sql 的语句。这背后的一切原因都依赖于 MapperProxy 中的 invoke 方法。更具体一点,其调用过程其实逻辑是委托给方法 cachedInvoker 来完成的。那 cachedInvoker 又会执行哪些逻辑呢?接下来,我们便进入到 cachedInvoker 中,看看其相应的逻辑。java 复制代码 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 的实现。看来如果我们要明白 MapperProxy 的 invoke 逻辑,我们便需要进入到 MapperMethodInvoker 实现类中的 invoke 方法。进一步,对于 MapperMethodInvoker 而言,其主要有两个默认实现,一个为 DefaultMethodInvoker 和 PlainMethodInvoker。此处我们仅选取其中的 PlainMethodInvoker 进行分析。java 复制代码 private static class PlainMethodInvoker implements MapperMethodInvoker {private final MapperMethod mapperMethod;


public PlainMethodInvoker(MapperMethod mapperMethod) {  super();  this.mapperMethod = mapperMethod;}
@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { return mapperMethod.execute(sqlSession, args);}
复制代码


}


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


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


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


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


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


(注:MappedStatement 相关信息我们在前一章有过介绍)java 复制代码 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 语句。事实上,结合之前的文章:


Mybatis 流程分析(四):Mybatis 构建 Mapper 背后的故事 Mybatis 流程分析(五): sql 语句与接口中方法绑定的"细节"


再加上本章,我们已经利用三章的篇幅来叙述 Mybatis 中 getMapper 的相关逻辑。虽然看着很多,但却可以通过如下的一张图来进行总结。



总结事实上,Mybatis 中 getMapper 获取实例对象的背后的逻辑就是 通过动态代理的方式生成一个实现该接口的代理类。进一步,调用该实例对象执行对应 sql 的背后的逻辑也全部交给了 SqlSession 来处理。至于 SqlSession 中是如何执行 sql 的且听后续分解~~读源码,分析源码本身就是一件枯燥的事情。作者在行文排版上已经尽可能减少代码的的排版,因为作者觉得大段的粘贴代码只会降低行文的可读性。事实上,作者更喜欢用图示的信息来展现代码间的调用逻辑。希望文章中的图能对你理解 MyBatis 有所帮助。此外,读源码的本身并不是让我们再复现一个框架,读源码等多的是窥探源码的中的设计以及让我们可以更加深刻的认清楚框架的"底层"逻辑,好让你在工作中快速定位问题。最后,还是希望文章能给你带来一点收获,毕竟花费时间来看文章本身就是一种对作者的信任,真的很感谢你们的信任。我所能做就是不断提升文章的质量,让读者能真正有所收获。

用户头像

java易二三

关注

还未添加个人签名 2021-11-23 加入

还未添加个人简介

评论

发布
暂无评论
Mybatis中方法和sql语句的桥梁——MapperProxy_Java_java易二三_InfoQ写作社区