MyBatis 源码解析-Mapper的执行流程

MyBatis官网:https://mybatis.org/mybatis-3/zh/index.html

MyBatis的测试代码如下:

        //解析mybatis-config.xml配置文件和Mapper文件,保存到Configuration对象中
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        //通过SqlSessionFactory创建SqlSession对象
        SqlSession session = sqlSessionFactory.openSession();
        //生成Mapper接口的代理类
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        //调用代理类的方法
        Blog blog = mapper.selectBlogById(2,"Bally Slog");

解析配置文件

执行第一行代码时,会去加载并解析 mybatis-config.xml 配置文件,配置文件的每一个节点在Configuration类里的都有一个属性与之对应,会把解析的值存放到对应的属性中,然后根据配置文件配置的Mapper,去解析Mapper文件。

SqlSession

执行第二行代码时,SqlSessionFactory当然是负责创建SqlSession了,具体就是DefaultSqlSessionFactory负责DefaultSqlSession的创建。最终也就是执行下面这段代码创建了DefaultSqlSession。

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //获取Environment对象,也就是mybatis-config.xml中的<environment>节点
      final Environment environment = configuration.getEnvironment();
      //获取TransactionFactory,创建 Transaction 对象
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      //上面这些都是为创建DefaultSqlSession而准备的
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

SqlSession是暴露给客户端操作数据库的接口,调用者无需了解内部复杂的执行流程,就可以轻松操作数据库执行CRUD操作。这应该就是门面模式了。

Mapper接口是如何被调用的?

执行第三行代码时,就会生成一个Mapper接口的代理类,为什么要生成代理类呢?当然是增强方法了。但由于Mapper接口并没有实现类,所以也谈不上增强,这里主要是为了调用Mapper接口后,接下来的流程能够继续执行。

动态代理

MyBatis 中大量使用了动态代理,先来看一个动态代理的例子 - 动态地给房东生成一个中介,代替房东出租房屋。

// 1.定义接口,JDK动态代理支持接口类型,所以先得准备一个接口
public interface IHouse {
    //房屋出租
    void chuZuHouse();
}
// 2.准备一个实现类,也就是房东
public class HouseOwner implements IHouse{
    @Override
    public void chuZuHouse() {
        System.out.println("我有房子要出租");
    }
}
  // 3.动态生成一个代理类,也就是中介
  public class TestDynamicProxy {
    public static void main(String[] args) {
//        ClassLoader loader:类加载器,这个类加载器他是用于加载代理对象字节码的,写的是被代理对象的类加载器,也就是和被代理对象使用相同的类加载器,固定写法,代理谁用谁的
//        Class<?>[] interfaces:它是用于让代理对象和被代理对象有相同的方法。写被代理对象实现的接口的字节码,固定写法代理谁就获取谁实现的接口
//        InvocationHandler h:用于提供增强的代码。它是让我们写如何代理,写InvocationHandlder接口的实现类
        IHouse houseOwner = new HouseOwner();
        IHouse houseAgent = (IHouse)Proxy.newProxyInstance(HouseOwner.class.getClassLoader(), HouseOwner.class.getInterfaces(), new InvocationHandler() {
            /**
             * @param proxy 代理对象的引用,在方法中如果想使用代理对象,就可以使用它,一般不用
             * @param method 表示代理对象执行的被代理对象的方法
             * @param args 表示执行当前被代理对象的方法所需要的参数
             * @return 返回值: 和被代理对象有相同的返回值
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //method.invoke(proxy, args);不能这样写,否则会出现自己调自己
                Object invoke = method.invoke(houseOwner, args);
                System.out.println("我是房产中介,让我来替你租吧");
                return invoke;
            }
        });
        //调用代理类的方法,就能执行invoke方法里的代码逻辑了
        houseAgent.chuZuHouse();
    }
}

上面就是很简单的动态代理,通过中介调用chuZuHouse()方法时,就会执行增强的逻辑,动态代理的好处就是动态生成代理类,假如有大量的类的方法需要被增强,如果用静态代理,有多少个类就得写多少个代理类,而使用动态代理只需实现InvocationHandler接口,不需要自己动手写代理类。

那如果不需要调用被代理对象的方法,那就可以不要实现类了,这就是MyBatis动态代理Mapper接口的原理。比如:

public interface IHouse {
    //房屋出租
    void chuZuHouse();
}

public class Test2DynamicProxy {
    public static void main(String[] args) {
        //由于不需要实现类,所以只需要定义接口就可以了
        IHouse houseAgent = (IHouse)Proxy.newProxyInstance(IHouse.class.getClassLoader(), new Class[]{IHouse.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //此处不需要实现类(房东)
                System.out.println("我是房产中介,让我来替你租吧");
                return null;
            }
        });
        //调用代理类的方法,就能执行invoke方法里的代码逻辑了
        houseAgent.chuZuHouse();
    }
}

生成Mapper接口的代理类

有了动态代理的基础,接下来看看 BlogMapper mapper = session.getMapper(BlogMapper.class);做了什么?

   //Configuration.java
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //从Configuration中根据类的全类名获取
        return mapperRegistry.getMapper(type, sqlSession);
    }

  //MapperRegistry.java
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从knownMappers中根据类的全类名获取
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

  //MapperProxyFactory.java
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

 protected T newInstance(MapperProxy<T> mapperProxy) {
    //此处就是Mapper接口被动态代理的关键,返回代理类对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

从以上代码可以看到,首先会从Configuration中根据类的全类名获取,然后交给了MapperRegistry,该类中有一个名叫knownMappers的map,保存了需要的Mapper,那什么时候存进去的呢?答案是在解析Mapper文件之后,就会把当前Mapper文件对应的Mapper接口封装到MapperProxyFactory对象,存到名叫knownMappers的HashMap中。然后进入MapperProxyFactory类可以看到,最终是通过Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);生成了Mapper的代理类。

有了代理类对象,就可以调用Mapper接口中的方法了。

调用Mapper接口方法

接下来看看 Blog blog = mapper.selectBlogById(2,"Bally Slog");这句代码做了什么?我们先分析一下:
1、mapper xml文件中写的参数是#{XXX},执行SQL前,参数肯定得处理;
2、返回了最终的执行结果,说明对数据库进行了操作,肯定会有JDBC相应的代码,比如PreparedStatement对象;
3、在查询的时候,MyBatis是有缓存功能的,所以还会对一级二级缓存进行处理;
4、MyBatis直接返回了我们需要的对象,所以还会对数据库的返回结果进行解析;

  //MapperProxy.java
  //调用代理类的方法,也就会执行InvocationHandler接口的实现类(MapperProxy)的invoke方法
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果method.getDeclaringClass()是Object,则调用method.invoke(this, args);不知道干嘛的,不过对正常流程无影响
      //method.getDeclaringClass()正常情况下是Mapper接口的Class,这里是yyb.useful.start01.BlogMapper
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
       
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    //如果methodCache里面存在method对应的MapperMethod,则取出,否则会创建并存放到methodCache中,并返回
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

通过以上代码,又把接下来的流程交给了MapperMethod来处理了,接下来看看MapperMethod做了什么?

  //MapperMethod.java

  /**
   * 解析参数和返回值(此处的返回值已经是java类型了),具体执行交给sqlSession来做
   */
  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;
  }

可以看到,MapperMethod的功能有三个:
1、获取SQL的类型,用来判断接下来该调用CRUD中的哪一个,类型判断主要是借助于MappedStatement,MappedStatement里面封装了<select|update|insert|delete>标签对应的信息,其中有一个SqlCommandType枚举类型,就是用来保存SQL的类型的。
2、解析参数,该功能由ParamNameResolver类来完成,逻辑如下:

public class ParamNameResolver {

  private static final String GENERIC_NAME_PREFIX = "param";

  /**
   * key是参数的索引,值是参数的名称
   *
   * 如果用@Param指定了名称,名称就从@Param中获取
   * 如果未指定,使用参数的索引。注意,当方法具有特殊参数(RowBounds,ResultHandler)时,此索引可能与实际索引不同
   * 比如:
   * aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}
   * aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}
   * aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
   */
  private final SortedMap<Integer, String> names;

  private boolean hasParamAnnotation;

  public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      //如果参数是RowBounds,ResultHandler时,跳过
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          //如果用@Param指定了名称,名称就从@Param中获取
          name = ((Param) annotation).value();
          break;
        }
      }
      if (name == null) {
        // @Param was not specified.
        if (config.isUseActualParamName()) {
          //使用方法签名中的名称作为语句参数名称。 要使用该特性,项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)
          //编译不加上 -parameters,则获取的值是arg0
          name = getActualParamName(method, paramIndex);
        }
        //如果未指定@Param,使用参数的索引
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          name = String.valueOf(map.size());
        }
      }
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

  /**
   * 单个参数且没加@Param注解,直接返回值
   * 多个参数除了使用命名规则(可能是argX,真实的name,或者索引)来命名外,此方法还添加了通用名称(param1, param2,...)
   */
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    //没有参数,返回null
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      //只有一个参数,且没加@Param注解,直接返回值
      return args[names.firstKey()];
    } else {
      //只要参数个数大于1,或者参数只有1个但加了@Param,返回hashMap
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        //把names中的数据添加到map中(可能是argX,真实的name,或者索引)
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          //添加(param1, param2, ...)到map中
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }
}

3、解析返回值类型
解析返回值(这里只是简单的处理,比如把int转换Integer,Long,Boolean等,实际的ResultSet解析过程有相应的类进行处理),具体执行又交给SqlSession来做,所以本质上,以下两种调用方式其实没什么区别,只不过mapper的方式多了一层转换,但对开发中更友好,避免写错类名的情况。

  Blog blog = mapper.selectBlogById(2);
  Blog blog1 = session.selectOne("yyb.useful.start01.BlogMapper.selectBlogById", 2);
posted @ 2021-03-24 22:27  ~冰  阅读(406)  评论(0编辑  收藏  举报