MyBatis源码解读3(2.2、MyBatis动态代理)

378 阅读5分钟

2.2、MyBatis动态代理

我们在写MyBatis的时候会想一个问题,为什么xxxDao接口没有实现类却可以实现对应的操作?其实答案很简单,因为在MyBatis的内部采用了动态代理的技术(类在JVM运行时创建的,字节码文件自始至终就没有存在过,在JVM结束的时候类就消失了),在JVM运行时那么此时有两个问题:

  1. 如何创建Dao接口的实现类?
  2. 实现类是如何进行实现的?

一般来说需要实现动态代理有以下的几种场景:

  1. 为原始对象(目标)增加一些额外功能
  2. 远程代理
  3. 接口实现类,我们看不见的实实在在的类文件,但是在运行中却可以体现出来,典型的无中生有。

我们来看看MyBatis动态代理的源码,他有两个核心的类:

  1. MapperProxy
  2. MapperProxyFactory

2.2.1、MapperProxyFactory

他是一个创造MapperProxy的工厂,完成的事情就是创建Map(perProxy对象,通过newInstance()方法

image-20230622220014721

我们可以看到MapperProxyFactory中有一个属性是mapperInterface,MapperProxyFactory实际上对应的就是我们自己写的xxxDao接口,他肯定会调用的方法是newInstance方法。

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

我们可以看到new MapperProxy的时候,他穿了三个参数,我们来想一下为什么需要传这三个参数:

  1. sqlSession:底层需要调用sqlSession的增删改查的方法
  2. mapperInterface:需要接口名来拼接namespace
  3. methodCache:通过方法名完成namespace最后一步的拼接

2.2.2、MapperProxy

MapperProxy实现了InvocationHandler这个类,他的核心作用是通过动态代理创建了DAO的实现类,通过invoke方法去调用底层的SqlSession的增删改查操作,从而实现数据库的操作。我们来看了一下这个类的基本属性和构造方法。

image-20240103221636688

接着我们就去看MapperProxy这个类,根据以前学习的动态代理我们可以知道,MapperProxy一定会实现InvocationHandler

image-20230622220559705

我们找到对应的invoke方法去看看。

image-20240103222758196

我们可以看到,MyBatis想的比我们更细致,我们看到这里if,他做了一个判断。

image-20240103223337716

如果你的类是Object类的话,就不用交给SqlSession去执行,直接原样交给invoke去执行即可,如果是其他类的话,就去执行cachedInvoker方法,我们再追进去看。

image-20240103223201441

里面有一个很重要的对象,那就是MapperMethod这个对象。我们点进去MapperMethod这个类看看。可以发现他有两个很重要的成员变量。

image-20240103223628423

为了搞清楚这两个属性的作用,我们来看看SqlCommand的构造方法。

image-20240103225304357

我们可以注意到,name这个属性被赋值成了id,而这个id就是namespace命名空间的id。而他的type表明的是这条sql是insert还是delete还是update还是select,以此来对应不同的SqlSession方法。

image-20230623124522149

我们再来看他的第二个属性MethodSignature对象。

image-20240103231707273

我们来把成员变量一个个来解析。

    private final boolean returnsMany; // 返回值是否是多个
    private final boolean returnsMap;  // 你返回的是否是一个map
    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; // 你的参数是什么

我们可以发现MethodSignature主要针对的是返回值、分页和参数。我们接下来看看参数名的解析器ParamNameResolver,看看他是如何解析参数的。

在看这个代码的源码的时候,我们发现了一个老朋友,那就是@Param注解,如果有@Param注解的话,就可以通过注解来获取注解参数的值。

image-20230623142910207

看完这个注解后,我们再回到MapperProxy这个类里面,有一个invoke重载的方法。

image-20240103232250822

我们来看看是如何执行sql语句的,这下就要去点进去execute方法。当你点进去这个方法以后就真的一目了然了。

image-20240103232350001

image-20240103232411248

在execute方法中,根据不同的case对不同的操作来作区分。我们拿insert来距离,如果操作是insert的话,就可以开始准备参数了。

Object param = method.convertArgsToSqlCommandParam(args);

然后就可以通过调用sqlSession.insert方法来执行insert插入操作。而sqlSession.insert这个方法还需要两个参数,第一个参数是通过command.getName()来获取namespaceId,第二个参数是插入的时候传进来的参数。

result = rowCountResult(sqlSession.insert(command.getName(), param));

最后来看一下比较复杂的查询操作。

      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;

他分了好多种情况:

  1. 如果方法的返回值为空同时他又有返回值的话就执行executeWithResultHandler(sqlSession, args);
  2. 如果方法的返回值是一个list即表示她会返回很多个的时候的话,就会执行result = executeForMany(sqlSession, args);
  3. 如果返回值是一个map的话,他会执行result = executeForMap(sqlSession, args);
  4. 如果都不满足的话,最终会执行sqlSession.selectOne(command.getName(), param);

我们可以发现,无论是什么操作,最终的落脚点一定都是sqlSession的相关操作。