MyBatis源码的学习(12)---Mybatis是如何从Mapper.xml中的select到sqlSession.selectList的?

这个问题:估计大家都知道是动态代理。可是除了动态代理这之间还有哪些设计模式呢?

    //3、获取mapper
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    //4、执行数据库操作,并处理结果集
    return userMapper.selectUser("10");

先看我们的getMapper方法,是如何返回一个代理对象的。

  @Override
  public  T getMapper(Class type) {
    return configuration.getMapper(type, this);
  }

  public  T getMapper(Class type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }


public  T getMapper(Class type, SqlSession sqlSession) {
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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);
    }
  }

先从配置对象configuration中获取,最后委托给mapperRegistry对象。

在mapperRegistry中,有个缓存(knownMappers)对象Map,我们根据key(接口的全限定类名,比如:learn.UserMapper)去获取我们的MapperProxyFactory对象,MapperProxyFactory负责每次创建我们的代理对象的实例。

从这里,我们可以学到缓存和工厂的使用,这样做的好处,很明显,避免了资源的重复加载,可以重复利用我们的资源。

我们的MapperProxyFactory对象是在我们,创建configuration对象的过程中,解析xml生成并缓存起来的。


package org.apache.ibatis.binding;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.binding.MapperProxy.MapperMethodInvoker;
import org.apache.ibatis.session.SqlSession;

/**
 * @author Lasse Voss
 */
public class MapperProxyFactory {
  //就是我们的接口 比如:learn.UserMapper
  private final Class mapperInterface;
 //用于缓存我们的方法调用,我们的同一个方法每次调用时,只是参数的不一样,其他一样,所以
//可以缓存起来,这样可以高效利用,和前面将我们的factory缓存起来是一样的道理
//此外,我们每次调用方法时,需要新new一个MapperProxy对象,而这个对象的最终的invoke方法中
//执行具体方法的逻辑的MapperMethodInvoker调用者对象是可以重复使用的,所以很有必要缓存起来
  private final Map methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class getMapperInterface() {
    return mapperInterface;
  }

  public Map getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy mapperProxy) {
    //这里是JDK动态代理,返回代理类的对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    //mapperProxy实现了InvocationHandler接口,可以详细看我关于动态代理的文章。
   //简单理解,实现这个接口的类,代表了我们的行为的抽象,里面的invoke方法就是最终的调用方法
    final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

上面的MapperProxy类中的MethodCache中,实际上缓存的就是我们的接口中定义的那些方法。

接下来我们分析:

userMapper.selectUser("10");

因为是动态代理,所以这里的方法,会执行到MapperProxy中的invoke方法:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
//这里是类似 toString,equals这样的方法,就是我们未代理的方法
        return method.invoke(this, args);
      } else {//代理的方法,比如:selectUser
        return cachedInvoker(proxy, method, args).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

如果是我们进行设计:

//如何从 selectUser()方法,让它直接执行下面的方法
result = sqlSession.selectOne(command.getName(), param);
//首先,我们要找入参,入参需要进行转换,从java类型  ---> sql类型
//然后,我们需要知道这个java方法,对应的是xml中的哪个sqlid,也就是我们需要有个映射表可以查。
//其次,我们sql中常规的有 增删改查 四种类型的方法 

然后,我们看源码:

public MapperMethod(Class mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }


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

 MapperMetod这个类,解决了我们之前提到的问题。sql和java方法的映射,方法的执行时,几种不同类型的不同处理逻辑。

所以,我们在MapperProxy的invoke方法中可以直接调用下面的方法,就达到了从java方法到执行sql的转换。

mapperMethod.execute(sqlSession, args);

可是源码,并不是这样实现的,这是为甚么?

查看源码,我们发现,先从缓存中获取执行器,然后再用执行器去执行invoke方法。而执行器接口有俩个实现类。

主要的原因应该是,为了处理接口中的default方法。(经过查看旧版源码,发现以前只能支持JDK8的default方法,为了扩展性

,所以新版都是让default方法和我们的自定义方法都是实现同一个接口)

如果我们按常规逻辑MapperProxy的invoke方法的else中的代码类似下面:

if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {// jdk 1.8
              //调用 default方法
            } else {//jdk 1.9
               //调用default方法
            }
}else {//我们的逻辑
        mapperMethod.execute(sqlSession, args);
        }

这样的代码,一看就后期扩展不好,以后jdk到15,16怎么办?难道一直修改这个方法体吗?

根据设计原则,我们可以把变化的部分抽取,我们可以将其抽象为接口(接口更多的是行为的抽象)。这样,我们的代码可以优雅的写作下面:

接口.invoke()

然后我们查看源代码发现确实,定义一个接口

MapperMethodInvoker

然后接口里定义了方法:invoke

接下来就是:我们的接口MapperMethodInvoker的invoke方法执行的时候,它实际执行的应该是我们的mapperMethod.execute(sqlSession, args)

我们知道有一种设计模式作用就是兼容原本接口不匹配的两个类,起到桥梁的作用,所以查看源码果然是使用了对象型的适配器模式。

private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;

    public PlainMethodInvoker(MapperMethod mapperMethod) {
      super();
      this.mapperMethod = mapperMethod;
    }

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

对于每一次调用 userMapper.selectUser("10"),都是同一个调用者,只是参数不一样而已,所以我们完全可以使用缓存,这样不用每次都新new一个调用者对象。于是,就有了

我们的invoke方法中的,先从缓存中获取调用者,然后再用调用者去执行invoke方法。

cachedInvoker(proxy, method, args).invoke(proxy, method, args, sqlSession);

回到一开始,为何我们不直接new一个MapperProxy对象,而是,通过工厂来new实例呢?

我们将我们的MapperMethodInvoker调用者缓存起来了,放到了methodCache这个map中,而我们每次新new一个MapperProxy对象的时候,对于同一个接口而言,它的methodCache(简单理解为,里面就是存放这个接口下的所有method)是不变的。

所以,我们完全可以把这个methodCache属性托付给第三方,等每次new我们的MapperProxy对象的时候,属性值直接去第三方拿。更进一步,我们第三方负责生产我们的实例,于是乎,就有了MapperProxyFactory这个类。

同一个接口,同一个MapperProxyFactory对象,同一个methodCache,每次只是生产不同的MapperProxy对象就行。

final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);

总结:

1.我们的java方法如何映射为一个sql命令,于是有了MapperMethod这个类,将二者映射起来。

2.如何兼容我们的default方法和正常的sql代理方法,同时易于以后的扩展,于是我们抽象了行为,定义了接口MapperMethodInvoker。

3.如何兼容我们接口MapperMethodInvoker中的invoke方法和MapperMethod类中execute方法,于是我们使用了对象型的适配器设计模式。

4.同一个方法的每一次调用,除了入参不一样其他都是一样的,为了节约成本,于是我们使用缓存的概念,将我们的调用者缓存起来。

Map methodCache

5.使用工厂模式,MapperProxyFactory进行我们的对象的创建,同时不变的属性值methodCache,交由这个MapperProxyFactory对象管理。像这样的方式,也可以用于build模式中,每次构建不同的对象,但是不变的属性托付给builder

 

 

 

 

 

你可能感兴趣的:(Mybatis源码学习)