MyBatis基础(二) 代理对象的生成--源码分析

系列内容回顾:

整体思路

这个部分的源码比较多, 先谈思路. 假设我们自己要实现一个和MyBatis相似的功能, 给一个接口和一个方法-sql语句映射关系的xml, 生成一个代理类完成dao的工作, 应该怎么做?

首先实现工具上, 有两种能用的工具, 一个是JDK动态代理, 另一个是CGLib动态代理, 两者的区别是JDK动态代理通过反射实现, 只能生成接口中声明的方法; CGLib通过操纵字节码实现, 其本质是继承了原有类的子类, 并进行加强, 所以不需要提供接口, 但是生成代理类的速度会比JDK的方式慢一些.

既然给定了接口, 这里可以采用JDK的动态代理来做, 首先回想一下动态代理类的生成, 我们通常采用java.lang.reflect.Proxy类的静态工厂方法来生成:

// 生成动态代理类的静态方法
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
// invocationHandler需要实现的方法
invoke(Object proxy, Method method, Object[] args)

loader通常和接口或实现类的classLoader保持一致, interface就是我们自己定义的dao接口, 最重要的是InvocationHandler, 因为我们事实上只有方法声明没有方法实现, 所以当调用例如findAll之类的方法时, 我们需要在InvocationHandler中给出具体的实现.

<mapper namespace="org.org.example.a_xml.dao.IUserDao">
    <!--配置查询所有-->
<!--    <select id="方法名称">-->
<!--        sql语句-->
<!--    </select>-->
    <select id="findAll" resultType="org.org.example.a_xml.domain.User">/*resultType 指定拿到的数据封装类*/
        select * from user;
    </select>
</mapper>

上面给出了一个非常简单的mapper.xml, InvocationHandler中需要完成的工作的整体思路如下:

  • 我们在mapper.xml中已经定义好了namespace="org.org.example.a_xml.dao.IUserDao", 这个是接口和xml之间的一一对应
  • 之后在<mapper>标签中定义了方法名和sql语句的一一映射, 我们可以构造这样一个类: 它持有一对接口名.方法名 - sql命令的映射
  • 在InvocationHandler中根据接口名+方法名找到对应的上述类的对象, 有对象执行以下工作:
  • 根据sql语句执行, 将sql语句返回的结果映射成实体类的对象, 这个映射关系由select标签中的resultType="org.org.example.a_xml.domain.User"定义.
  • 将映射的实体类对象从InvacationHandler的方法调用返回.

整体思路理完了我们来看源码, 首先看一下整体流程:

在这里插入图片描述

左边是我们在使用中的调用流程, 右边是batis帮我们做的事情,方括号内的是类实现的接口. 可以看到MapperProxy其实是一个InvocationHandler类, 在这里面定义的invoke方法,正是MyBatis帮我们生成的dao的各种方法的实现, 这些方法的类称为MapperMethod, 其中的excute方法返回的就是我们需要的dao方法返回的实体类结果.

源码分析

MapperRegistry.java

这个类里面重点关注getMapper方法

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // knownMappers 是个Map<Class<?>, MapperProxyFactory<?>>实例, 存着Dao接口类和mapperProxyFactory的映射
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 如果knownMapper中没有的话报错,  否则尝试返回一个新的MapperProxy
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

getMapper方法事实上是通过工厂方法返回一个MapperProxy<T>实例, 这个实例顾名思义就是Dao的代理类, 由它负责Dao的各个方法的实现. 代理类在执行每个方法时都会调用invoke方法, 一个传统的代理类中的invoke方法中大致是这么写的:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在真实的对象执行之前我们可以添加自己的操作
        System.out.println("before invoke。。。");
        Object invoke = method.invoke(obj, args);
        //在真实的对象执行之后我们可以添加自己的操作
        System.out.println("after invoke。。。");
        return invoke;
    }

问题是我们这里的method可能是接口的default方法, 也可能只声明了没有实现的, 如果是没有实现的方法, 就是mybatis需要帮忙实现的, 来看invoke的源码:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
    // 如果调用的是Object中的父类方法, 直接调用
    if (Object.class.equals(method.getDeclaringClass())) {
    return method.invoke(this, args);
    // 如果调用的是接口的default方法, 直接调用
    } else if (method.isDefault()) {
    return invokeDefaultMethod(proxy, method, args);
    }
} catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
}
// 按照Method实例查找一个mapperMethod并执行
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

所以一个代理类的方法的实现工作, 事实上是交给了一个MapperMethod实例去完成的, 所以我们接着来看MapperMethod里面干了什么?

public class MapperMethod {
  // 两个成员变量, 一个是sql语句的封装对象, 一个是调用者所调用的方法的对象
  // 就是我们之前整理思路时说的, 持有**一对接口名.方法名 - sql命令的映射**的类
  private final SqlCommand command;
  private final MethodSignature method;

  //...

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      /*
        所有case的流程:
        - 准备sql语句的参数
        - 执行sqlSession中提供的方法
        - 对插入, 删除, 更新结果, 返回的是影响了几行数据, 调用rowCountResult进行封装
        - 对查询操作分情况讨论
      */
      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:
      // select里面的花招比较多, 需要考虑到是否直接用回调函数处理, 返回多个值, 返回单个值, 返回map, 返回cursor的几种情况
        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;
  }
  //...
}

具体的select怎么处理, update, delete, insert怎么处理结果, 这些内容就比较简单, 可以直接查看下源码.

结论

Mybatis的动态生成Dao代理对象的原理, 就是用动态代理在InvocationHandler里面按照sql和方法的映射处理没有实现的方法,并封装结果.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章