Mybatis源码分析:Mapper接口的方法调用与SQL的执行

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

Mybatis源码分析:Mapper接口的方法调用与SQL的执行


概述


在应用代码中,如果不结合spring来使用mybatis,则需要通过SqlSession获取mapper接口对应的代理对象MapperProxy,然后通过该代理对象来调用并执行mapper接口的方法。使用示例如下:


String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 通过SqlSessionFactory获取sqlSession
SqlSession session = sqlSessionFactory.openSession();
try {

  // 通过SqlSession对象获取BlogMapper接口的代理对象,
  // 然后赋值给BlogMapper接口
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 调用BlogMapper接口的方法来执行指定的数据库操作
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();
}


在mybatis内部是通过该MapperProxy代理对象来获取并执行该mapper接口方法所对应的SQL的。


Mapper接口代理对象MapperProxy的获取


由上面的例子可知,在应用代码中通过SqlSession来获取mapper接口BlogMapper的代理对象,并将该代理对象赋值到类型为BlogMapper的一个引用,通过该引用来调用BlogMapper接口的对应方法。


SqlSession获取mapper接口的代理对象的getMapper方法实现如下:调用configuration的getMapper方法,从configuration内部获取指定mapper接口类型的MapperProxy代理对象,其中mapper接口类型使用泛型T来表示。


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

// MapperRegistry类定义
public class MapperRegistry {
  // 配置Configuration引用
  private final Configuration config;
  
  // 应用代码中的mapper接口和代理对象MapperProxy的工厂类MapperProxyFactory的映射,
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 将mapper接口的类对象作为key
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 创建MapperProxy对象实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  ...
  
}


从内部维护的mapperRegistry中获取该mapper接口对应的mapperProxy代理对象,由之前的文章分析可知,每个mapper接口对应的mapperProxy代理对象是在应用程序中,使用SqlSessionFactoryBuilder创建SqlSessionFactory对象实例时,解析mapper.xml配置文件,然后保存到SqlSessionFactory对象关联的配置Configuration的mapperRegistry中的。


SQL的执行


由以上分析可知,应用代码通过SqlSession对象获取了mapper接口对应的代理对象MapperProxy,并在应用代码中赋值给了mapper接口的一个引用,然后通过该引用来调用mapper接口的方法,从而触发该代理对象MapperProxy来获取并执行该方法对应的SQL。MapperProxy的定义如下:


// 实现了InvocationHandler接口实现动态代理,目标类T每个方法的执行都会通过invoke来拦截
// 每个mapper对应一个MapperProxy,在spring中,针对每个mapper接口使用对应的MapperProxy对象来作为bean
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  
  // sqlSession引用,由该mapper的所有方法共享,在结合mybatis-spring中,是所有mapper的所有方法共享同一个sqlSessionTemplate引用
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;
  
  ...
  
}


MapperProxy实现了JDK提供的InvocationHandler接口,InvocationHandler是JDK提供的用来实现动态代理的,即被代理的类的所有方法的执行都通过InvocationHandler接口定义的invoke方法来拦截。


// args为mapper接口的代理对象的method方法被调用时,应用代码提供的参数值
@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);
    }
    
    // 根据目标类,即应用代码中的mapper接口的方法method,找到对应的mapperMethod
    // MapperProxy的MapperMethod是懒加载的,即使用的时候才创建MapperMethod,然后存放到该MapperProxy的methodCache中
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}


核心实现为通过cachedMapperMethod方法来创建该被调用的mapper接口的方法对应的MapperMethod,然后调用MapperMethod的execute方法来执行实际的SQL调用。


其中MapperMethod对象是在第一次调用该方法时创建,然后保存在MapperProxy,具体为MapperProxyFactory的一个类型为ConcurrentHashMap的methodCache缓存中,之后直接取出调用即可。


在mybatis内部设计当中mapper接口对应MapperProxy,mapper接口的方法对应MapperMethod。


mapper接口的方法映射MapperMethod的创建


MapperProxy的cachedMapperMethod的实现如下:


private MapperMethod cachedMapperMethod(Method method) {
    // methodCache具体在MapperProxyFactory定义,用于缓存该mapper接口的方法对应的MapperMethod对象
    // 这样由该MapperProxyFactory创建的MapperProxy对象共享这一个缓存,每个MapperProxy内部维护一个引用,
    // 这个实现基础是每个mapper接口都对应一个MapperProxyFactory。
    // 由于methodCache是ConcurrentHashMap,故是线程安全的。
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
}


先从方法缓存methodCache中获取,如果存在该方法对应的MapperMethod则直接返回,否则创建一个MapperMethod对象。


MapperMethod类定义与对象实例的创建如下:在执行时,即调用execute方法时,通过SqlCommand对象来获取mapper接口的该方法对应的MappedStatement对象。


public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 在SqlCommand构造方法中,从configuration的mappedStatements集合中,
    // 获取key为该mapper接口的方法method,value为对应的MappedStatement,即SQL语句包装器,

    // 这样在下面执行execute方法时,可以直接通过command的name作为key,
    // 从configuration的mappedStatements集合取出对应的mappedStatement对象即可
    this.command = new SqlCommand(config, mapperInterface, method);

    // 该mapper接口方法的签名
    this.method = new MethodSignature(config, mapperInterface, method);
  }
  
  ...
  
}


SqlCommand类定义:其中name为该mapper接口的方法的全限定名。通过SqlCommand的name属性作为key,可以从Configuration的mappedStatements字典中,取出该方法对应的MappedStatement对象。由之前的文章分析可知,Configuration的mappedStatements集合用于存放mapper.xml的增删查改对应的SQL语句。


SqlCommandType为SQL语句类型枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;


// 获取mapper接口的某个方法对应的mapper.xml中的id为该方法名的SQL操作节点对应的mappedStatement
// 使用name来关联这个mappedStatement对象在configuration的mappedStatements集合的key
public static class SqlCommand {
    // mapper接口的方法全限定名,便于从Configuration的mappedStatements集合获取对应的SQL包装对象MappedStatement对象
    private final String name;
    // SQL语句的类型枚举
    private final SqlCommandType type;
    
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName(); // mapper接口方法名
      final Class<?> declaringClass = method.getDeclaringClass();

      // 获取该方法对应的MapperStatement对象
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        // name为configuration中的mappedStatements集合的某个元素的key
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }
    
    ...
    
}

// MapperMethod的resolveMappedStatement方法
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
    Class<?> declaringClass, Configuration configuration) {
      // statementId为方法全限定名,即类全限定名+方法名
      String statementId = mapperInterface.getName() + "." + methodName;
      // 创建SqlSessionFactory时,已经将mapper接口的方法对应的SQL操作,
      // 生成了对应的MappedStatement对象存放到了configuration的mappedStatement集合中了
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }

      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
}


SQL的执行:调用MapperMethod的execute方法


SQL的执行主要是通过MapperMethod的execute来实现的。由以上分析可知,MapperMethod通过SqlCommand来获取mapper接口的该方法对应的mapper.xml的增删查改节点的SQL,具体为SQL对应的MappedStatement对象。具体实现如下:


public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 使用sqlSession来执行对应的SQL语句
    switch (command.getType()) {
      case INSERT: {
      // param为一个HashMap,SQL参数占位符名和mapper接口方法的参数值的映射
      Object param = method.convertArgsToSqlCommandParam(args);
        // command.getName为SQL语句的id,param为SQL的参数和参数值对
        // 交给sqlSession执行,其中sqlSession绑定一个数据库连接
        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);
        }
        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;
}


通过SqlSession内部的executor来执行MappedStatement封装的SQL语句


以insert插入数据为例:sqlSession.insert(command.getName(), param),以下为SqlSession的update方法定义,insert底层也是调用update方法来执行的。


具体为通过executor包的Executor来实现:使用SQL包装器MappedStatement和SQL参数parameter获取实际的执行SQL和完成动态SQL参数绑定,最后调用JDBC相关API来完成SQL的执行。


public int update(String statement, Object parameter) {
    try {
      dirty = true;
      // statement为mapper接口的方法全限定名,
      // 从configuration的mappedStatements中获取在创建sqlSessionFactory时填充进去的该statement对应的
      // SQL语句包装类MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }


每个SqlSession都包含一个独立的Executor,避免不同SqlSession的相互影响,通过该executor来完成SQL的执行。


Executor在内部解析MappedStatement获取SQL语句并完成动态SQL参数值的绑定,以下为Executor接口的其中一个实现类SimpleExecutor的update方法实现,具体实现为:通过StatementHandler对象来完成MappedStatement内部的SQL解析提取,SQL参数值绑定,生成JDBC的预处理语句,以及最后通过JDBC的API执行该SQL语句。


public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();

      // SQL语句处理器,将SQL语句对象ms和参数键值对映射parameter,赋值为其内部参数,以便之后的SQL语句参数赋值
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

      // 生成预处理语句,并且将参数值替换SQL的问号?参数,形成可执行的SQL
      // 内部调用handler.prepare方法
      stmt = prepareStatement(handler, ms.getStatementLog());

      // 使用SQL语句处理器执行SQL完成更新,返回更新的数据库行数
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
}


以下为SQL语句处理器StatementHandler接口的其中一个实现类PreparedStatementHandler的update方法实现:主要是JDBC的PrepareStatement的执行。


@Override
public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // SQL语句执行
    ps.execute();
    // 获取执行结果,即更新的数据库行数
    int rows = ps.getUpdateCount();

    Object parameterObject = boundSql.getParameterObject();
    // 生成key信息,如插入数据产生行的id
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    // 将key信息返回给应用代码
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    // 放回更新的行数
    return rows;
}


总结


通过以上分析可知,在应用代码中调用Mapper接口的某个方法来执行对应的数据库操作,在mybatis内部过程为:


首先通过mapper接口的代理对象MapperProxy的invoke方法拦截该mapper接口方法的执行,在invoke方法中获取该mapper接口方法对应的MapperMethod对象;

    

调用MapperMethod对象的execute方法,通过当前的SqlSession对象执行mapper接口的该方法对应的SQL语句;

    

SqlSession对象通过关联的Configuration引用获取该方法对应的MappedStatement对象;然后通过SqlSession绑定的SQL执行器Executor来解析MappedStatement对象获取实际的SQL,并完成该SQL的动态参数绑定;

    

Executor通过SQL语句处理器StatementHandler来从MappedStatement获取SQL语句,然后生成JDBC相关的statement语句并进行参数绑定,最后执行该statement语句,从而将该SQL语句发送到数据库执行并获取执行结果。


链接:https://blog.csdn.net/u010013573/article/details/87997635


精彩推荐

JDK1.8源码分析:Executors线程池创建工厂的用法和实现原理

JDK1.8源码分析:可重入锁ReentrantLock和Condition的实现原理

Tomcat源码分析:Socket网络通信和线程模型体系结构设计

Spring的三种依赖注入方式的使用和源码实现分析



扫下方二维码关注“程序员考拉”,每日推荐优秀好文!



Mybatis源码分析:Mapper接口的方法调用与SQL的执行


如果感觉推送内容不错,不妨右下角点个在看,感谢支持!

原文始发于微信公众号(程序员考拉):Mybatis源码分析:Mapper接口的方法调用与SQL的执行