likes
comments
collection
share

MyBatis源码解读(三)

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

三、MyBatis的核心对象

3.1、核心对象

3.1.1、MappedStatement

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

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

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

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

3.1.2、Executor

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

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

MyBatis源码解读(三)

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

  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。

MyBatis源码解读(三)

3.1.3、StatementHandler

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

MyBatis源码解读(三)

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

3.1.4、ParamentHandler

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

3.1.5、ResultSetHandler

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

MyBatis源码解读(三)

3.1.6、TypeHandler

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

3.1.7、总结

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

SqlSession的insert方法。

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

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

MyBatis源码解读(三)

3.2、MyBatis动态代理

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

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

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

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

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

  1. MapperProxy
  2. MapperProxyFactory

3.2.1、MapperProxyFactory

MyBatis源码解读(三)

我们可以看到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);
  }

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

MyBatis源码解读(三)

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

  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方法,我们再追进去看。

  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这个类看看。可以发现他有两个很重要的属性。

  private final SqlCommand command;
  private final MethodSignature method;

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

    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方法。

MyBatis源码解读(三)

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

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

MyBatis源码解读(三)

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

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

我们来看看是如何执行sql语句的,这下就要去点进去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;
  }

在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的相关操作。