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) {
        return configuration.<T>getMapper(type, this);
    }
    
  • Configuration的getMapper方法实现如下:

    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方法来拦截。

  • MapperProxy的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内部过程为:

  1. 首先通过mapper接口的代理对象MapperProxy的invoke方法拦截该mapper接口方法的执行,在invoke方法中获取该mapper接口方法对应的MapperMethod对象;
  2. 调用MapperMethod对象的execute方法,通过当前的SqlSession对象执行mapper接口的该方法对应的SQL语句;
  3. SqlSession对象通过关联的Configuration引用获取该方法对应的MappedStatement对象;然后通过SqlSession绑定的SQL执行器Executor来解析MappedStatement对象获取实际的SQL,并完成该SQL的动态参数绑定;
  4. Executor通过SQL语句处理器StatementHandler来从MappedStatement获取SQL语句,然后生成JDBC相关的statement语句并进行参数绑定,最后执行该statement语句,从而将该SQL语句发送到数据库执行并获取执行结果。

你可能感兴趣的:(Mybatis)