mybatis解析和基本運行原理


        Mybatis的運行過程分為兩大步:

  • 第1步,讀取配置文件緩存到Configuration對象,用於創建SqlSessionFactory;
  • 第2步,SqlSession的執行過程。相對而言,SqlSessionFactory的創建還算比較容易理解,而SqlSession的執行過程就不那么簡單了,它包括許多復雜的技術,要先掌握反射技術和動態代理,這里主要用到的是JDK動態代理.

一個簡單使用的例子

public class TestMybatis {
    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlSessionFactory.openSession();
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.findById(2);
            System.out.println("name:" + user.getName());
            session.close();
            SqlSession session1 = sqlSessionFactory.openSession();
            List<User> users = session1.selectList("findAll");
            session1.commit();
            System.out.println("allSize:" + users.size());
            session1.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

MyBatis的主要構件及其相互關系

從MyBatis代碼實現的角度來看,MyBatis的主要的核心部件有以下幾個:

  • SqlSession              作為MyBatis工作的主要頂層API,表示和數據庫交互的會話,完成必要數據庫增刪改查功能
  • Executor                  MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護
  • StatementHandler   封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數、將Statement結果集轉換成List集合。
  • ParameterHandler   負責對用戶傳遞的參數轉換成JDBC Statement 所需要的參數,
  • ResultSetHandler    負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合;
  • TypeHandler            負責java數據類型和jdbc數據類型之間的映射和轉換
  • MappedStatement   MappedStatement維護了一條<select|update|delete|insert>節點的封裝,
  • SqlSource                負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,並返回
  • BoundSql                 表示動態生成的SQL語句以及相應的參數信息
  • Configuration            MyBatis所有的配置信息都維持在Configuration對象之中。

構建SqlSessionFactory過程

構建主要分為2步:

  • 通過XMLConfigBuilder解析配置的XML文件,讀出配置參數,包括基礎配置XML文件和映射器XML文件;
  • 使用Configuration對象創建SqlSessionFactory,SqlSessionFactory是一個接口,提供了一個默認的實現類DefaultSqlSessionFactory。

說白了,就是將我們的所有配置解析為Configuration對象,在整個生命周期內,可以通過該對象獲取需要的配置。

SqlSession運行過程

        我們與mybatis交互主要是通過配置文件或者配置對象,但是我們最終的目的是要操作數據庫的,所以mybatis為我們提供了sqlSession這個對象來進行所有的操作,也就是說我們真正通過mybatis操作數據庫只要對接sqlSession這個對象就可以了。那么問題來了,我們怎么樣通過sqlSession來了操作數據庫的呢?

問題1:如何獲取sqlSession?

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

由上面代碼我們可知我們可以通過SqlSessionFactory的openSession去獲取我們的sqlSession,只要我們實例化了一個SqlSessionFactory,就能得到一個DefaultSqlSession對象。

問題2:Mapper對象怎么來的?

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

通過調用DefaultSqlSession的getMapper方法並且傳入一個類型對象獲取,底層呢調用的是配置對象configuration的getMapper方法,configuration對象是我們在加載DefaultSqlSessionFactory時傳入的。
然后我們再來看下這個配置對象的getMapper,傳入的是類型對象(補充一點這個類型對象就是我們平時寫的DAO層接口,里面是一些數據庫操作的接口方法。),和自身也就是DefaultSqlSession。

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

我們看到這個configuration的getMapper方法里調用的是mapperRegistry的getMapper方法,參數依然是類型對象和sqlSession。這里呢,我們要先來看下這個MapperRegistry即所謂Mapper注冊器是什么。

public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

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

從這里我們可以知道其實啊這個MapperRegistry就是保持了一個Configuration對象和一個HashMap,而這個HashMap的key是類型對象,value呢是MapperProxyFactory。我們這里先不管MapperProxyFactory是什么東西,我們現在只需要知道MapperRegistry是這么一個東西就可以了。這里有人會問MapperRegistry對象是怎么來的,這里呢是在初始化Configuration對象時初始化了這個MapperRegistry對象的,代碼大家可以去看,為了避免混亂,保持貼出來的代碼是一條線走下來的,這里就不貼出來了。接下來我們繼續看下這個MapperRegistry的getMapper方法。

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

這里我們可以看到從knownMappers中獲取key為類型對象的MapperProxyFactory對象。然后調用MapperProxyFactory對象的newInstance方法返回,newInstance方法傳入sqlSession對象。到這里我們可能看不出什么端倪,那我們就繼續往下看這個newInstance方法做的什么事情吧。

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

這里我們可以看到MapperProxyFactory直接new了一個MapperProxy對象,然后調用另外一重載的newInstance方法傳入MapperProxy對象。這里我們可以看出一些東西了,通過調用Proxy.newProxyInstance動態代理了我們的mapperProxy對象!這里的mapperInterface即我們的dao層(持久層)接口的類型對象。
所以總結下就是我們通過sqlSesssion.getMapper(clazz)得到的Mapper對象是一個mapperProxy的代理類!
所以也就引出下面的問題。

問題3:為什么我調用mapper對象方法就能發出sql操作數據庫?

通過上面的講解,我們知道了這個mapper對象其實是一個一個mapperProxy的代理類!所以呢這個mapperProxy必然實現了InvocationHandler接口。所以當我們調用我們的持久層接口的方法時必然就會調用到這個MapperProxy對象的invoke方法,所以接下來我們進入這個方法看看具體mybatis為我們做了什么。

  @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);
    return mapperMethod.execute(sqlSession, args);
  }

從代碼中我們可以看到前面做了一個判斷,這個判斷主要是防止我們調用像toString方法或者equals方法時也能正常調用。然后我們可以看到它調用cachedMapperMethod返回MapperMethod對象,接着就執行這個MapperMethod對象的execute方法。這個cachedMapperMethod方法主要是能緩存我們使用過的一些mapperMethod對象,方便下次使用。這個MapperMethod對象主要是獲取方法對應的sql命令和執行相應SQL操作等的處理,具體細節同學們可以抽空研究。

 繼續跟蹤MapperMethod.execute

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

我們可以清晰的看到這里針對數據庫的增刪改查做了對應的操作,這里我們可以看下查詢操作。我們可以看到這里針對方法的不同返回值作了不同的處理,我們看下其中一種情況。

          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);

這里我們可以看到它將方法參數類型轉換成數據庫層面上的參數類型,最后調用sqlSession對象的selectOne方法執行。所以我們看到最后還是回到sqlSession對象上來,也就是前面所說的sqlSession是mybatis提供的與數據庫交互的唯一對象。

接下來我們看下這個selectOne方法做了什么事,這里我們看的是defaultSqlSession的selectOne方法。

  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

我們看到它調用selectList方法,通過去返回值的第一個值作為結果返回。那么我們來看下這個selectList方法。

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我們可以看到這里調用了executor的query方法,我們再進入到query里看看。這里我們看的是BaseExecutor的query方法

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 1.根據具體傳入的參數,動態地生成需要執行的SQL語句,用BoundSql對象表示 
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 2.為當前的查詢創建一個緩存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }
 
  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 3.緩存中沒有值,直接從數據庫中讀取數據 
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //4. 執行查詢,返回List 結果,然后    將查詢的結果放入緩存之中    
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

我們看到有個一個方法doQuery,進入方法看看做了什么。點進去后我們發現是抽象方法,我們選擇simpleExecutor子類查看實現。

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      //5. 根據既有的參數,創建StatementHandler對象來執行查詢操作
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //6. 創建java.Sql.Statement對象,傳遞給StatementHandler對象
      stmt = prepareStatement(handler, ms.getStatementLog());
      //7. 調用StatementHandler.query()方法,返回List結果集
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    //對創建的Statement對象設置參數,即設置SQL 語句中 ? 設置為指定的參數
    handler.parameterize(stmt);
    return stmt;
  }

我們可以看到通過configuration對象的newStatementHandler方法構建了一個StatementHandler,然后在調用prepareStatement方法中獲取連接對象,通過StatementHandler得到Statement對象。另外我們注意到在獲取了Statement對象后調用了parameterize方法。繼續跟蹤下去(自行跟蹤哈)我們可以發現會調用到ParameterHandler對象的setParameters去處理我們的參數。所以這里的prepareStatement方法主要使用了StatementHandler和ParameterHandler對象幫助我們處理語句集和參數的處理。最后還調用了StatementHandler的query方法,我們繼續跟蹤下去。

這里我們進入到PreparedStatementHandler這個handler查看代碼。這里我們進入到PreparedStatementHandler這個handler查看代碼。

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    statement.execute(sql);
    //使用ResultHandler來處理ResultSet
    return resultSetHandler.<E>handleResultSets(statement);
  }

看到這里,我們終於找到了操作數據庫的地方了,就是ps.execute()這句代碼。底層我們可以發現就是我們平時寫的JDBC!然后將這個執行后的PreparedStatement交給resultSetHandler處理結果集,最后返回我們需要的結果集。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM