阿里面試:Mybatis中方法和SQL是怎麼關聯起來的呢?

本文:3126字 | 閱讀時長:4分10秒mysql

今天是Mybatis源碼分析第四篇,也是最後一篇。sql

老規矩,先上案例代碼:數據庫

public class MybatisApplication {
  public static final String URL = "jdbc:mysql://localhost:3306/mblog";
  public static final String USER = "root";
  public static final String PASSWORD = "123456";
    
  public static void main(String[] args) {
    String resource = "mybatis-config.xml";
    InputStream inputStream = null;
    SqlSession sqlSession = null;
    try {
        inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //下面這行代碼就是今天的重點
        User user = userMapper.selectById(1));
        System.out.println(user);
    
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
      try {
        inputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
      sqlSession.close();
    }
 }

已經分享了三篇Mybatis源碼分析文章,從 Mybatis的配置文件解析 到 獲取SqlSession,再到 獲取UserMapper接口的代理對象設計模式

今天咱們來分析,userMapper中的方法和UserMapper.xml中的SQL是怎麼關聯的,以及怎麼執行SQL的。緩存

咱們的故事從這一行代碼開始:數據結構

User user = userMapper.selectById(1));

這一行代碼背後源碼搞完,也就表明着咱們Mybatis源碼搞完(主幹部分)。mybatis

下面咱們繼續開擼。app

圖片

在上一篇中咱們知道了userMapper是JDK動態代理對象,因此調用這個代理對象的任意方法都是執行觸發管理類MapperProxy的invoke()方法。ide

因爲篇幅較長,爲了更好閱讀,這裏把文章分紅兩個部分:源碼分析

  • 第一部分:MapperProxy.invoke()到Executor.query。
  • 第二部分:Executor.query到JDBC中的SQL執行。

第一部分流程圖:

圖片

MapperProxy.invoke()

開篇已經說過了,調用userMapper的方法就是調用MapperProxy的invoke()方法,因此咱們就從這invoke()方法開始。

若是對於Mybatis源碼不是很熟悉的話,建議先看看前面的文章。

//MapperProxy類
@Override
public Object invoke(....) throws Throwable {
  try {
    //首先判斷是否爲Object自己的方法,是則不須要去執行SQL,
    //好比:toString()、hashCode()、equals()等方法。
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (method.isDefault()) {
      //判斷是否JDK8及之後的接口默認實現方法。
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  //<3>  
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  //<4>
  return mapperMethod.execute(sqlSession, args);
}

<3>處是從緩存獲取MapperMethod,這裏加入了緩存主要是爲了提高MapperMethod的獲取速度。緩存的使用在Mybatis中也是很是之多。

private final Map<Method, MapperMethod> methodCache;
private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

Map的computeIfAbsent方法:根據key獲取值,若是值爲null,則把後面的Object的值付給key。

繼續看MapperMethod這個類,定義了兩個屬性command和method,兩個屬性與之相對應的兩個靜態內部類。

public class MapperMethod {
  private final SqlCommand command;
  private final MethodSignature method;
  public static class SqlCommand {
  private final String name;
  private final SqlCommandType type;
  public SqlCommand(...) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      //得到 MappedStatement 對象
      MappedStatement ms = resolveMappedStatement(...);
      // <2> 找不到 MappedStatement
      if (ms == null) {
        // 若是有 @Flush 註解,則標記爲 FLUSH 類型
        if (method.getAnnotation(Flush.class) !null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else { 
          // 拋出 BindingException 異常,若是找不到 MappedStatement
          //(開發中容易見到的錯誤)說明該方法上,沒有對應的 SQL 聲明。
          throw new BindingException("Invalid bound statement (not found): "+ mapperInterface.getName() + "." + methodName);
       }
      } else {
      // 得到 name
      //id=com.tian.mybatis.mapper.UserMapper.selectById
      name = ms.getId();
      // 得到 type=SELECT
      type = ms.getSqlCommandType();
      //若是type=UNKNOWN
      // 拋出 BindingException 異常,若是是 UNKNOWN 類型
      if (type == SqlCommandType.UNKNOWN) { 
           throw new BindingException("Unknown execution method for: " + name);
      }
      }
  }   
  private MappedStatement resolveMappedStatement(...) {
      // 得到編號
      //com.tian.mybatis.mapper.UserMapper.selectById
      String statementId = mapperInterface.getName() + "." + methodName;
      //若是有,得到 MappedStatement 對象,並返回
      if (configuration.hasStatement(statementId)) {
        //mappedStatements.get(statementId);  
        //解析配置文件時候建立並保存Map<String, MappedStatement> mappedStatements中
        return configuration.getMappedStatement(statementId);
        // 若是沒有,而且當前方法就是 declaringClass 聲明的,則說明真的找不到
        } else if (mapperInterface.equals(declaringClass)) {
          return null;
        }
        // 遍歷父接口,繼續得到 MappedStatement 對象
        for (Class<?> superInterface : mapperInterface.getInterfaces()) {
            if (declaringClass.isAssignableFrom(superInterface)) {
                MappedStatement ms = resolveMappedStatement(...);
            if (ms != null) {
                return ms;
            }
        }
      }
      // 真的找不到,返回 null
      return null;
    } 
    //....
}
public static class MethodSignature {
    private final boolean returnsMap;
    private final Class<?> returnType;
    private final Integer rowBoundsIndex;
    //....
}

圖片

SqlCommand封裝了statement ID,好比說:

com.tian.mybatis.mapper.UserMapper.selectById

和SQL類型。

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

另外,還有個屬性MethodSignature方法簽名,主要是封裝的是返回值的類型和參數處理。這裏咱們debug看看這個MapperMethod對象返回的內容和咱們案例中代碼的關聯。

圖片

妥妥的,故事繼續,咱們接着看MapperMethod中execute方法。

MapperMethod.execute

上面代碼中<4>處,先來看看這個方法的總體邏輯:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case SELECT:
         //部分代碼省略....
         Object param = method.convertArgsToSqlCommandParam(args);
          //本次是QUERY類型,因此這裏是重點  
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    } 
    return result;
  }

這個方法中,根據咱們上面得到的不一樣的type(INSERT、UPDATE、DELETE、SELECT)和返回類型:

(本文是查詢,因此這裏的type=SELECT)
  1. 調用convertArgsToSqlCommandParam()將方法參數轉換爲SQL的參數。

    圖片

2.調用selectOne()方法。這裏的sqlSession就是DefaultSqlSession,因此咱們繼續回到DefaultSqlSession中selectOne方法中。

SqlSession.selectOne方法

繼續DefaultSqlSession中的selectOne()方法:

//DefaultSqlSession中  
@Override
public <T> selectOne(String statement, Object parameter) {
    //這是一種好的設計方法
    //無論是執行多條查詢仍是單條查詢,都走selectList方法(重點)
    List<T> list = this.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 {
      //數據庫中沒有數據就返回null
      return null;
    }
  }

這裏調用的是本類中selectList方法。

@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //從configuration獲取MappedStatement
      //此時的statement=com.tian.mybatis.mapper.UserMapper.selectById
      MappedStatement ms = configuration.getMappedStatement(statement);
      //調用執行器中的query方法
      return executor.query(...);
    } catch (Exception e) {
     //.....
    } finally {
      ErrorContext.instance().reset();
    }
  }

在這個方法裏是根據statement從configuration對象中獲取MappedStatement對象。

MappedStatement ms = configuration.getMappedStatement(statement);

在configuration中getMappedStatement方法:

//存放在一個map中的
//key是statement=com.tian.mybatis.mapper.UserMapper.selectById,value是MappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>();  
  public MappedStatement getMappedStatement(String id) {
    return this.getMappedStatement(id, true);
  }
  public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) 
    return mappedStatements.get(id);
  }

而MappedStatement裏面有xml中增刪改查標籤配置的全部屬性,包括id、statementType、sqlSource、入參、返回值等。

圖片

到此,咱們已經將UserMapper類中的方法和UserMapper.xml中的sql給完全關聯起來了。繼續調用executor中query()方法:

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

這裏調用的是執行器Executor中的query()方法。

第二部分流程:

Executor.query()方法

這裏的Executor對象是在調用openSession()方法時建立的。關於這一點咱們在前面的文章已經說過,這裏就再也不贅述了。

下面來看看調用執行器的query()放的整個流程:

圖片

咱們股市繼續,看看具體源碼是如何實現的。

CachingExecutor.query()

在CachingExecutor中

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

BoundSql中主要是SQL和參數:

圖片

既然是緩存,咱們確定想到key-value數據結構。

下面來看看這個key生成規則:

這個二級緩存是怎麼構成的呢?而且還要保證在查詢的時候必須是惟一。

圖片

也就說,構成key主要有:

方法相同、翻頁偏移量相同、SQL相同、參數相同、數據源環境相同纔會被認爲是同一個查詢。

圖片


這裏能說到這個層面就已經闊以了。


若是向更深刻的搞,就得把hashCode這些扯進來了,請看上面這個張圖裏前面的幾個屬性就知道和hashCode有關係了。

處理二級緩存

首先是從ms中取出cache對象,判斷cache對象是否爲null,若是爲null,則沒有查詢二級緩存和寫入二級緩存的流程。

有二級緩存,校驗是否使用此二級緩存,再從事務管理器中獲取二級緩存,存在緩存直接返回。不存在查數據庫,寫入二級緩存再返回。

@Override
public <E> List<E> query(....)
      throws SQLException 
{
    Cache cache = ms.getCache();
    //判斷是否有二級緩存
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 }

那麼這個Cache對象是什麼建立的呢?

在解析UserMapper.xml時候,在XMLMapperBuilder類中的cacheElement()方法裏。

圖片

關於二級緩存相關這一塊在前面文章已經說過,好比:

圖片

解析上面這些標籤

圖片

建立Cache對象:

圖片

二級緩存處理完了,就來到BaseExecutor的query方法中。

圖片


BaseExecutor,query()

第一步,清空緩存

if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
}

queryStack用於記錄查詢棧,防止地櫃查詢重複處理緩存。

flushCache=true的時候,會先清理本地緩存(一級緩存)。

若是沒有緩存會從數據庫中查詢

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

在看看這個方法的邏輯:

private <E> List<E> queryFromDatabase(...) throws SQLException {
    List<E> list;
    //使用佔位符的方式,先搶佔一級緩存。
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //刪除上面搶佔的佔位符  
      localCache.removeObject(key);
    }
    //放入一級緩存中
    localCache.putObject(key, list);
    return list;
 }

先在緩存使用佔位符佔位,而後查詢,移除佔位符,將數據放入一級緩存中。

執行Executor的doQuery()方法,默認使用SimpleExecutor。

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

下面就來到了SimpleExecutor中的doQuery方法。

SimpleExecutor.doQuery

@Override
 public <E> List<E> doQuery(....) throws SQLException {
    Statement stmt = null;
    try {
      //獲取配置文件信息  
      Configuration configuration = ms.getConfiguration();
      //獲取handler
      StatementHandler handler = configuration.newStatementHandler(....);
      //獲取Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      //執行RoutingStatementHandler的query方法  
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
 }

建立StatementHandler

在configuration中newStatementHandler()裏,建立了一個StatementHandler對象,先獲得RoutingStatementHandler(路由)。

圖片

public StatementHandler newStatementHandler() {
    StatementHandler statementHandler = new RoutingStatementHandler();
    //執行StatementHandler類型的插件    
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler建立的時候是就是建立基本的StatementHandler對象。

這裏會根據MapperStament裏面的statementType決定StatementHandler類型。默認是PREPARED

圖片

StatementHandler裏面包含了處理參數的ParameterHandler和處理結果集的ResultHandler。

圖片

上面說的這幾個對象正式被插件攔截的四大對象,因此在建立的時都要用攔截器進行包裝的方法。

圖片

對於插件相關的,請看以前已發的插件文章:插件原理分析

咱們故事繼續:

建立Statement

圖片


建立對象後就會執行RoutingStatementHandler的query方法。

//RoutingStatementHandler中 
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //委派delegate=PreparedStatementHandler
    return delegate.query(statement, resultHandler);
}

這裏設計頗有意思,全部的處理都要使用RoutingStatementHandler來路由,所有經過委託的方式進行調用。

執行SQL

而後執行到PreparedStatementHandler中的query方法。

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //JDBC的流程了  
    ps.execute();
    //處理結果集,若是有插件代理ResultHandler,會先走到被攔截的業務邏輯中
    return resultSetHandler.handleResultSets(ps);
  }

看到了ps.execute(); 表示已經到JDBC層面了,這時候SQL就已經執行了。後面就是調用DefaultResultSetHandler類進行結果集處理。

到這裏,SQL語句就執行完畢,並將結果集賦值並返回了。

總算搞完把Mybatis主幹源碼掠了一遍,鬆口氣~,能看到這裏,證實小夥伴也是蠻用心的,辛苦了。越努力越幸福!

建議抽時間,再次debug,還有就是畫畫類之間的關係圖,還有就是Mybatis中設計模式好好回味回味。

圖片

總結

從調用userMapper的selectById()方法開始,到方法和SQL關聯起來,參數處理,再到JDBC中SQL執行。

完整流程圖:

圖片

感興趣的小夥伴,能夠對照着這張流程圖就行一步一步的debug。

記着:努力的人,世界不會虧待你的。
相關文章
相關標籤/搜索