mybatis原始碼學習:基於動態代理實現查詢全過程

天喬巴夏丶發表於2020-04-26

前文傳送門:
mybatis原始碼學習:從SqlSessionFactory到代理物件的生成
mybatis原始碼學習:一級快取和二級快取分析

下面這條語句,將會呼叫代理物件的方法,並執行查詢過程,我們一起來看看它的內部是如何實現的。

User user1 = userDao1.findById(41);

一、動態代理:執行代理物件的方法時攔截,進行方法增強。

 /**
 * 作用:執行被代理物件的任何介面方法都會經過該方法
 * @param proxy : 代理物件的引用
 * @param method : 當前執行的方法
 * @param args : 當前執行方法所需的引數
 * @return : 和被代理物件有相同的返回值
 * @throws Throwable
 */
  @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)) {
        //判斷該方法是不是default方法
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //對msqlcommand和method進行封裝,並以method:mapperMethod的形式加入methodCache
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //返回mapperMethod的execute的返回結果
    return mapperMethod.execute(sqlSession, args);
  }

可以看看這個MapperMethod具體是個啥玩意兒:

  //快取思想的體現
  private MapperMethod cachedMapperMethod(Method method) {
    //從methodCache這個Map中取method對應的mapperMethod
    MapperMethod mapperMethod = methodCache.get(method);
    //如果裡面沒有,就建立一個
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      //以method:mapperMethod的形式加入methodCache
      methodCache.put(method, mapperMethod);
    }
    //如果有就直接返回
    return mapperMethod;
  }

MapperMethod的構造器,sqlCommand和methodSignature是他的兩個靜態內部類:

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

在這裡插入圖片描述

二、接著執行MapperMethod物件的execute方法,其實原始碼還是通俗易懂的,無非就是按照不同的sql語句的類別進行不同的資料結果的封裝,值得注意的是,insert,update和delete其實底層都是呼叫了update方法,但為了語義清晰,所以區分類別。

之前command封裝了sql語句的類別,我們這是SELECT對吧,

  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 {
          //將Args轉換為SqlCommand引數,簡單理解就是獲取了引數41,這裡就不深入了
          Object param = method.convertArgsToSqlCommandParam(args);
          //呼叫selectOne方法,這部分可以發現,無論是使用代理dao還是定義sqlsession實現類,本質上都呼叫了這些方法,因為這裡的command。getName就是具體定義的sql的namespace.id
          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;
  }

三、當然本例以findById為例,這裡呼叫的是SelectOne方法,接收com.smday.dao.IUserDao.findById41

  @Override
  public <T> T selectOne(String statement, Object parameter) {
   	//根據引數select List
    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 {
      //個數為0返回null
      return null;
    }
  }

四、呼叫selectList的方法,實現如下:

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //獲取MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      //wrapCollection方法是對集合型別或者陣列型別的引數做特殊處理
      //通過執行器呼叫query方法
      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();
    }
  }

五、獲取MappedStatement物件,該物件代表一個增刪改查標籤的詳細資訊。

在這裡插入圖片描述

六、預設執行CachingExecutor.query(ms,xxx,x)方法,獲取boundsql,該物件包含sql的具體資訊,建立快取key。

在這裡插入圖片描述

七、先去二級快取中查詢資料,如果二級快取中沒有,則去一級快取(localCache)中查詢,接著資料庫(queryFromDatabase)一條龍服務,這部分就不贅述了。最終呼叫的是Executor的doQuery方法,list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

八、建立StatementHandler物件,預設為PreparedStatementHandler,用以操作statement執行操作。

ps:StatementHandler定義了一些主要的方法:預編譯相關prepare、查詢query、設定引數parameterize等等。

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      //從mappedStatement中獲取配置資訊物件
      Configuration configuration = ms.getConfiguration();
      //建立StatementHandler物件,處理sql語句的物件,預設為PreparedStatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //建立prepareStatement物件
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
      //RoutingStatementHandler並不是真實的服務物件,將會通過介面卡模式找到對應的Statementhandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
      //攔截鏈對方法進行攔截
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

Executor和Statement分為三種:Simple、Prepared、Callable。

SqlSession四大物件在建立的時候都會被攔截器進行攔截,我們之後再做學習。

九、在建立StatementHandler的時候,我們會發現,它還初始化建立了另外兩個重要的物件:

//用於引數處理 
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//用於封裝結果集
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

十、在建立prepareStatement物件的時候,其實還通過parameterHandler的prepare()對statement進行了引數的預編譯:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
      //預編譯(基礎配置)
    stmt = handler.prepare(connection, transaction.getTimeout());
      //設定引數
    handler.parameterize(stmt);
    return stmt;
  }

//statementhandler的方法
public Statement prepare(Connection connection, Integer transactionTimeout)
    Statement statement = null;
	//預編譯
    statement = instantiateStatement(connection);
	//設定超時
    setStatementTimeout(statement, transactionTimeout);
	//設定獲取最大行數
    setFetchSize(statement);
    return statement;

還通過handler.parameterize(stmt);對引數進行設定,最終通過parameterHandler的setParameters的方法實現了該操作,其中還建立TypeHandler物件完成資料庫型別和javaBean型別的對映。

  @Override
  public void setParameters(PreparedStatement ps) {
	  //。。。省略對value值的操作
      //建立TypeHandler物件完成資料庫型別和javaBean型別的對映
      TypeHandler typeHandler = parameterMapping.getTypeHandler();
      JdbcType jdbcType = parameterMapping.getJdbcType();
      if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
      }
      //設定引數
      typeHandler.setParameter(ps, i + 1, value, jdbcType);
  }

十一、獲取了ps引數之後,就可以執行statementHandler的query方法進行查詢了

  //PreparedStatementHandler.java  
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //轉為PreparedStatement物件
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    //利用結果集處理物件對結果集進行處理:封裝並返回。
    return resultSetHandler.<E> handleResultSets(ps);
  }

總結:

反射技術運用廣泛,基於反射的動態代理模式使我們操作的不再是真實的服務,而是代理物件,正是基於動態代理,mybatis可以在真實物件的基礎上,提供額外的服務,我們也可以利用這一特性去自定義一些類,滿足我們的需求。

  • 通過動態代理呼叫代理物件的方法。

  • 通過sqlSession執行sql操作的方法:insert|delete|select|update

  • 利用Executor物件對其他三大物件進行排程。

  • PreparedStatementHandler對sql進行預編譯,並進行了基礎配置,接著設定引數,並執行sql語句。

  • ParameterHandler負責對引數進行設定,其中TypeHandler負責資料庫型別和javabean型別的對映。

  • 最後查詢結果由ResultHandler封裝。

相關文章