MyBatis源码解读(三)

三、MyBatis的核心对象

3.1、核心对象

3.1.1、MappedStatement

MyBatis其实是对JDBC的进一步封装,我们都知道JDBC有几个重要的对象:

  1. Statement
  2. Prepared Statement
  3. Callable Statement
  4. ResultSet

Statement、Prepared Statement、Callable Statement分别与数据与进行交互,最终执行返回的结果由ResultSet进行封装。而SqlSession是对上述步骤进行进一步的封装。

mybatis-config.xml最终被封装成了Configuration对象。我们点啊看iabatis的Configuration类可以发现,environment标签都封装在了Configuration这个类的Environment属性当中,而mapper.xml文件中的一个一个的标签一般会被封装在MappedStatement这个对象中,这就注定了一个Mybatis应用中会有N个MappedStatement对象。

还有一个我们很常见的二级缓存的配置在Configuration类中也有对应的属性。我们可以发现他的默认值为true,所以这个属性我们写不写都可以,因为是默认开启的。

而我们写别名的<typeAlioases>标签被mybatis封装成的对象是TypeAliasRegistry

对于mapper.xml文件的注册,我们可以封装在<mappers>这个标签里面,而被mybatis封装的对象是loadedResources。

而最重要的是写sql语句的mapper.xml文件,在Configuration对象中也做了汇总进行封装。

那么此时问题来了,我们写在标签里面的sql语句最终会被封装到哪里了呢?因为sql语句是写在标签里面的,每一个标签都被封装成了一个个的MappedStatement对象,所以我们需要在MappedStatement对象里面去找。我们往下翻会发现一个getBoundSql方法。

我们会发现他的返回值是叫一个BoundSql的对象,这个对象其实就是MyBatis对sql语句的封装。我们点进去看看这个对象,他有这么几个属性。我们来挨个分析一下。

  1. private final String sql:用于封装我们写的sql
  2. 其他的用于封装各种参数。

3.1.2、Executor

Executor是MyBatis中处理功能的核心,对应增删改Executor提供了对应的方法来执行这些操作。

我们打开源码可以发现,Executor是一个接口(一般涉及到操作相关的类型,尽量设计成接口),我们点住alt+7可以打开大纲,看看所有的方法。

我们可以看到他主要有两类方法:

  1. update:对应的是sql中的增删改,只要是对于数据库有改动的操作都统一归为update
  2. query:对应的是sql中的查询操作。
  3. commit、rollback、getTransaction:与事务相关的操作,包括提交、回滚等。
  4. createCacheKey、isCached:与缓存相关的操作。

Executor接口有3个比较重要的实现:

  1. BatchExecutor:批处理操作,一次链接,执行多条sql。
  2. ReuseExecutor:复用Statement,只要你的sql一样,用的Statement就是一样的。这个Executor比较少用,因为我们很少执行同样的sql,同类型的sql哪怕参数不同都不叫一样的sql
  3. SimpleExecutor:最常用的Executor,也是MyBatis默认的Executor。

3.1.3、StatementHandler

StatementHandler是MyBatis封装的JDBC的Statement,MyBatis访问数据库操作真正的核心。我们来看一下StatementHandler的源码。

可以看到都是一些简单的增删改查操作。

3.1.4、ParamentHandler

ParamentHandlerd的作用是把处理参数,把MyBatis的参数替换成底层JDBC的参数。

3.1.5、ResultSetHandler

ResultSetHandler封装的是JDBC的ResultSet。他的作用是对JDBC中查询结果集ResultSet进行封装。

3.1.6、TypeHandler

用于处理数据库类型与Java类型之间转换的过程。

3.1.7、总结

作为总结,我们就来简单的跟一下insert的源码。

SqlSession的insert方法。

DefaultSqlSessionSession的insert方法,我们可以发现insert方法调用的其实是update方法。

还是在DefaultSqlSessionSession同一个类中的update方法。

接着走到了Executor的update方法,注意这个Executor是默认的SimpleExecutor

3.2、MyBatis动态代理

我们在写MyBatis的时候会想一个问题,为什么xxxDao接口没有实现类却可以实现对应的操作?其实答案很简单,因为在MyBatis的内部采用了动态代理的技术,在JVM运行时那么此时有两个问题:

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

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

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

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

  1. MapperProxy
  2. MapperProxyFactory

3.2.1、MapperProxyFactory

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

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

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

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

vbnet 复制代码
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      }
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

看到有一个cachedInvoker方法,我们再追进去看。

php 复制代码
  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        if (!m.isDefault()) {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
        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);
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }
​

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

arduino 复制代码
  private final SqlCommand command;
  private final MethodSignature method;

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

ini 复制代码
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) == null) {
          throw new BindingException(
              "Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
        }
        name = null;
        type = SqlCommandType.FLUSH;
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

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

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

arduino 复制代码
    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注解的话,就可以通过注解来获取注解参数的值。

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

typescript 复制代码
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

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

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

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

ini 复制代码
Object param = method.convertArgsToSqlCommandParam(args);

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

ini 复制代码
result = rowCountResult(sqlSession.insert(command.getName(), param));

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

ini 复制代码
      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的相关操作。

相关推荐
qianshanxue112 分钟前
0-3论软件设计模式及其应用、2016-已写(观察者通知,命令-控制指令,适配器-兼容,工厂-多种数据库)
数据库·设计模式
code_std7 分钟前
保存文件到指定位置,读取/删除指定文件夹中文件
java·spring boot·后端
小许学java13 分钟前
Spring事务和事务传播机制
java·数据库·spring·事务
小无名呀15 分钟前
视图(View)
数据库·mysql
汤姆yu16 分钟前
基于springboot的热门文创内容推荐分享系统
java·spring boot·后端
lkbhua莱克瓦2419 分钟前
MySQL介绍
java·开发语言·数据库·笔记·mysql
武昌库里写JAVA22 分钟前
在iview中使用upload组件上传文件之前先做其他的处理
java·vue.js·spring boot·后端·sql
嘻哈baby35 分钟前
AI让我变强了还是变弱了?一个后端开发的年终自省
后端
teacher伟大光荣且正确37 分钟前
关于Qt QReadWriteLock(读写锁) 以及 QSettings 使用的问题
java·数据库·qt
舒一笑39 分钟前
2025:从“代码搬运”到“意图编织”,我在 AI 浪潮中找回了开发的“爽感”
后端·程序员·产品