5.[源码]揭示Mapper类背后的执行逻辑

在上一篇文章[源码]mybatis之JDBC处理器(一)StatementHandler与ParamHandler中,重点在于捋顺StatementHandler创建statement、设置参数,这两个步骤执行流程的逻辑,而忽略了其中参数处理的细节。而且由于我们之前的探究都是使用sqlSession直接调用的方式去做的,所以也没有涉及到全部的参数处理过程(缺少参数转换过程)。为了探究参数处理的整体流程,在这篇文章中,我们将先介绍另外一种执行sql的方式(涉及Mapper动态代理及参数转换)。为参数处理的整体逻辑梳理做准备。

当我们使用mybatis执行SQL的时候, 除了之前提及的直接调用,如下所示:

SqlSession session = sqlSessionFactory.openSession())
User user = new User();  
user.setId(1);  
user.setUsername("小张");  
session.selectList("com.swag.pojo.UserMapper.findUserById(user)

还有另外一种方式来执行SQL,如下所示:

SqlSession session = sqlSessionFactory.openSession())
User user = new User();  
user.setId(1);  
user.setUsername("小张");  
UserMapper mapper = session.getMapper(UserMapper.class);  
List<User> users = mapper.findUser(user);
@Mapper
public interface UserMapper {
    @Select("select id,username,age,phone,`desc`  from user where id=#{id}")  
    public List<User> findUserById(User user);
}

我们通过之前的学习已经知道,与数据库进行交互,mybatis会开启会话(sqlSession),有了会话就可以调用查询了。

第一种调用方式与sqlSession的关系一目了然,拿到sqlSession直接调用其selectList方法开始查询之旅。而第二种方式,调用sqlSession的方法相对来说比较隐晦,而且我们的UserMapper明明是一个接口,为何能直接调用它的方法得到结果,是谁在我们不知道的地方实现了它的方法?

不要急,先进行一波粗犷的debug,来看下从sqlSession中获得的对象UserMapper是什么?得到的结果如下图红框部分所示——MapperProxy@540335f…,显然,从sqlSession中获得的UserMapper的实现类,是一个代理对象。

在这里插入图片描述

NO.1|创建Mapper代理对象

为了探究这个过程,我们先要了解session.getMapper(UserMapper.class)里面做了哪些处理,以至于最终能够获得这个代理对象。

SqlSession是一个接口,所以我们进入到它的实现类—DefaultSqlSession中来查看这个方法。DefaultSqlSession.getMapper这个方法从Configuration对象中,根据Mapper接口,获取了Mapper代理对象 。

public class DefaultSqlSession implements SqlSession@Override
public <T> T getMapper(Class<T> type) {    
    return configuration.<T>getMapper(type, this);}
}

在Configuration的getMapper方法中,调用了其持有的MapperRegistry的getMapper方法。

Configuration:

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

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

我们进入到了MapperRegistry的getMapper方法中,可以看见,他首先会根据Mapper接口的类型,从Map集合—knownMappers【注1】中取出相应的Mapper代理对象工厂。随后调用拿到的Mapper代理对象工厂产生代理对象。而mapperProxyFactory.newInstance(sqlSession)这个方法又进行了怎样的操作,我们接着向下去看。

【注1】:在mybatis加载配置文件的过程中, 会拿到所有mapper接口以及它的代理对象存储到knownMappers这个Map集合中,key为mapper接口类型,value为代理对象工厂,以便此时使用。这里不是本次的重点,暂作了解。

 public class MapperRegistryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
 // 根据Mapper接口的类型,从Map集合中获取Mapper代理对象工厂
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

在mapperProxyFactory的 newInstance(sqlSession)方法中,创建了一个关键的对象MapperProxy,在newInstance的重载方法newInstance(mapperProxy)中传入了这个对象,newInstance(mapperProxy)这个方法里面,有着明显的JDK动态代理痕迹,代理对象在这里创建并返回。这与我们最开始debug得到的答案完全相符——从sqlSession中获得的UserMapper的实现类,确实是一个代理对象。

public class MapperProxyFactory<T>private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
 // 使用JDK动态代理方式,生成代理对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
 // 创建基于JDK实现的Mapper代理对象
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

接下来看下MapperProxy这个关键类,果然实现了InvocationHandler,完全符合动态代理api。

public class MapperProxy<T> implements InvocationHandler, Serializable

NO.2|通过Mapper代理对象调用方法

那么在调用mapper.findUser(user)又会发生什么呢,相信你心里也有了一些猜想。首先,这里的mapper是一个代理对象,所以在通过代理对象调用方法的时候一定会调用MapperProxy的invoke方法,我们大胆猜测,mybatis在invoke方法中做了处理,调用了sqlSession的方法,从而可以进行SQL的执行。具体是不是这样呢,还要继续向下探究。

可以肯定的是通过mapper代理对象调用方法的时候,一定会走MapperProxy的invoke方法,所以这里是执行的入口,探究之旅也从这里开始。

可以看见在MapperProxy的invoke方法中,调用了MapperMethod的execute(sqlSession, args)方法。

public class MapperProxy<T> implements InvocationHandler, Serializable:

private final Map<Method, MapperMethod> methodCache;

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      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);
    // 执行sqlsession的方法
    return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
    //判断methodCache中以method为key有没有值存在, 如果没有创建一个MapperMethod对象放进去
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

MapperMethod是整个代理机制的核心类,我们一直心心念念的sqlSession的操作就是在这个类中进行了封装。所以有必要先来认识一下这个类以及它的成员变量,方便我们后续的理解。

它的成员变量有两个,一个是SqlCommand【注2】命令类型,还有一个是MethodSignature【注3】方法签名。

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

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

【注2】:SqlCommand命令类型,SqlCommand的构造方法中会根据mapperInterface.getName() + “.” + method.getName()生成statementId,然后根据statementId从configuration中取出对应的MappedStatement(每个sql语句会被封装成一个MappedStatement),进而通过ms.getSqlCommandType()获得此次SQL命令的类型—增、删、改、查。

【注3】:MethodSignature方法签名,里面定义了关于方法的相关信息,具体是什么可以看下面的注释部分。

 public MethodSignature(Configuration configuration, Method method) throws BindingException {
      this.returnType = method.getReturnType();      // 返回值类型
      this.returnsVoid = void.class.equals(this.returnType);      // 返回值是否为void
      this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());      // 是否返回多个结果
      this.mapKey = getMapKey(method);      // method是否使用了mapKey注解
      this.returnsMap = (this.mapKey != null);       // 返回值是否是Map类型
      this.hasNamedParameters = hasNamedParams(method); // 参数是否自定义了别名
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 是否使用mybatis的物理分页
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 是否有resutlHandler
      this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters)); // 请求参数解析
    }

认识了MapperMethod这个类之后,就要探究它的execute方法的执行逻辑了。在execute方法中,会先判断SQL语句的命令类型—增、删、改、查。选择不同的执行逻辑。

而增删改查执行逻辑大致相同,先是调用method.convertArgsToSqlCommandParam(args)进行参数转换(在介绍参数处理的整个流程的时候会详细介绍图片)然后就是调用sqlSession对应的增删改方法。

而当进行查询操作的时候,可以看到根据返回类型的不同,会执行不同的方法,然而殊途同归,最终都会先调用convertArgsToSqlCommandParam进行参数转化,然后调用sqlSession的不同的查询api。

我们这里仅以需要返回多个结果,也就是返回参数是List的情况为例,进行探究。当返回结果为List时,会调用executeForMany(sqlSession, args)方法。

public class MapperMethodpublic 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;
  }

可以清楚的看见在executeForMany方法中, 进行了参数转换后,调用了sqlSession.selectList()得到结果,最后根据method定义的返回类型进行数据封装。终于,我们在Mapper代理对象执行方法的过程中,成功的找到了sqlSession,可以用它与数据库交互了。

 public class MapperMethodprivate <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<E>selectList(command.getName(), param, rowBounds); 
    } else {
      result = sqlSession.<E>selectList(command.getName(), param);
    }
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

NO.3|小结

mybatis在session.getMapper(UserMapper.class);的时候获取代理对象工厂,使用代理对象工厂创建MapperProxy,并利用MapperProxy创建JDK动态代理对象,然后返回代理对象。

mybatis在mapper.findUser(user);执行的时候,其实是用代理对象来执行方法,在执行的过程中,会调用MapperProxy的invoke方法,invoke方法中会通过MapperMethod调用sqlSession的方法操作SQL。

截止到这里,当调用mapper.findUser(user)时mybatis在干什么?相信大家已经清楚了。

这里除了编程知识分享,同时也是我成长脚步的印记, 期待与您一起学习和进步,了解更多源码知识,可以扫描下方二维码关注我的公众号:程序媛swag。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值