mybatis模仿1之我先看看

用了挺久的mybatis,但一直停留在用的層面上,以爲不行的呀,得走出溫馨區。
因此想本身看看mybatis的實現,而後模仿着寫一個,哈哈,固然一開始不會要求完成度很高。
這一篇就先看下mybatis奧祕。

這裏參考的mybatis源碼版本是3.4.5。sql

首先,先寫一個mybatis簡單使用的例子。
clipboard.png數據庫

// 使用
public static void main(String[] args) throws IOException {
    
    //根據配置文件建立一個SqlSessionFactory對象
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory 
        = new SqlSessionFactoryBuilder().build(inputStream);
    
    // 獲取sqlSession對象
    SqlSession session = sqlSessionFactory.openSession();
    
    try{
        
        // 獲取接口的實現類實例
        IUserMapper mapper = session.getMapper(IUserMapper.class);
        
        // 調用方法
        User user = mapper.findById(1);
        System.out.println(user.getName());
        
    }finally{
        session.close();
    }
}

回憶一下,使用Mybatis的步驟就是apache

  1. 寫配置文件,配置鏈接數據庫的參數,mybatis的參數。
  2. 定義接口,而且經過註解或者xml文件的形式提供SQL語句。以後要在配置文件中註冊這個接口。
  3. 建立SqlSessionFactory,傳入配置文件。經過工廠得到SqlSession對象。
  4. 經過SqlSession對象獲取自定義的接口的實例,而後就是調用接口的方法。

整個過程當中,玩家就只參與了配置參數,還有提供SQL這兩步。因此這兩步就是看mybatis怎麼操做的入口,是進入mybatis地下城的大門。
配置參數這部分,使用框架時基本都有這個操做,比較常見。因此算是個分支劇情,而提供SQL算是mybatis的主線劇情,這裏先通關主線劇情。緩存


劇情1 之 發生了什麼

IUserMapper mapper = session.getMapper(IUserMapper.class);
User user = mapper.findById(1);

能夠看到,在使用時,咱們獲取到了咱們的接口的一個實現類實例,
燃鵝,咱們沒有寫這個接口的實現的呀。因此我以爲是魔法的緣由,在這裏要打個斷點。session

  1. 在getMapper的方法上斷點,咱們進入了DefaultSqlSession.getMapper(Class<T>),
    因此默認咱們從SqlSessionFactory拿到的是一個DefaultSqlSession的實例。mybatis

    /*
    經過configuration的getMapper方法,傳入咱們的接口類型以及SqlSession實例,返
    回一個泛型。這裏也就是咱們的IUserMapper接口的實現類的實例。*/
    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.<T>getMapper(type, this);
    }
  2. 再進去是Configuration.getMapper(Class<T>, SqlSession)。app

    /*
    這裏又從mapperRegistry裏拿到對象,
    mapperRegistry是Configuration類的一個屬性*/
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
  3. 看一下MapperRegistry的getMapper裏邊是什麼。
    這裏看到了使人激動的字眼,就是Proxy,
    猜想咱們最終拿到的IUserMapper的實例是個代理對象框架

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 看了下,knownMappers是個Map對象,Map<Class<?>, MapperProxyFactory<?>>
        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);
        }
    }
  4. MapperProxyFactory的newInstance 一探究竟
    其實名字叫xxFactory的確定是生產xx的,能夠猜到返回的是個MapperProxyide

    public T newInstance(SqlSession sqlSession) {
        /*這裏new了一個MapperProxy,而後調用newInstance*/
        final MapperProxy<T> mapperProxy 
            = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
  5. MapperProxy是個啥ui

    /* 這個類實現了InvocationHandler,動態代理的接口。*/
    public class MapperProxy<T> implements InvocationHandler, Serializable
  6. 看看newInstance(mapperProxy)作了啥。
    使用Proxy構造實現咱們IUserMapper接口的代理類的實例!

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
  7. 消化一下,開始的疑問是咱們沒有提供IUserMapper的實現,可是經過SqlSession的getMapper方法能拿到一個IUserMapper的實現類的對象。
    謎底就是最終返回了咱們接口的一個代理類的實例。
    而MapperProxy實現了InvocationHandler接口,在咱們構造代理對象時傳入了MapperProxy對象,
    所以在調IUserMapper的全部方法時,都會進入到MapperProxy類的invoke方法。
    其實不像上邊那樣操做,經過直接打印這個對象也能夠看出來..

    System.out.println(mapper);
    System.out.println(Proxy.isProxyClass(mapper.getClass()));
    // 打印結果,貼圖片太醜了,就不貼結果圖了。
    org.apache.ibatis.binding.MapperProxy@e580929
    true

劇情2 之 MapperProxy你幹了啥

通常使用動態代理,實現了InvocationHandler接口的類中都會持有被代理類的引用,這裏也就是MapperProxy。而後在invoke方法裏邊先執行額外的操做,再調用被代理類的方法。在MapperProxy這個類裏卻沒找到被代理類的引用。

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    。。。
}

因此穿山甲說了什麼?
因此當咱們調用 IUserMapper 的 findById 時發生了什麼?
這裏就要看下MapperProxy的invoke方法了。

@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);
}
  1. 首先進行一個 if 判斷,邏輯是 若是調的這個方法的提供類是Object類,那個就直接執行這個方法。
    這裏容易想偏,哪一個類不是Object的子類呀..
    其實應該是 若是是Object中的方法,那就直接執行。
    Object有哪些方法呢?toString這些。調mapper.toString()時,就直接被執行,不走下邊的邏輯了。

    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
  2. 以後是第二個 if,邏輯是,若是這個方法的權限修飾符是public而且是由接口提供的,則執行invokeDefaultMethod方法。
    好比在IUserMapper寫了一個默認方法,執行這個方法isDefaultMethod就會返回true了。
    這裏咱們的方法的提供方是代理類,不是接口,因此返回了false。

    else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
    }
    
    // isDefaultMethod
    private boolean isDefaultMethod(Method method) {
    return 
        ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) 
            == Modifier.PUBLIC)
            && 
        method.getDeclaringClass().isInterface();
    }
  3. 前兩步都是過濾做用,下邊的纔是重點。
    能夠看到經過 cachedMapperMethod方法 拿到了一個 MapperMethod 對象。
    看名字是從緩存裏拿。而後就執行MapperMethod的execute方法。

    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  4. 緩一緩,小結一下。開始的疑問是,MapperProxy類裏邊居然沒有被代理類對象的引用。
    那它想幹什麼。在invoke方法中咱們找到答案。
    經過invoke方法的method參數,拿到了一個MapperMethod 對象,
    而後執行了這個對象的execute方法,就沒了。中間一些常規的方法就直接執行。
    因此純粹就是爲了進入invoke方法,拿到MapperMethod ,至始至終都不存在被代理類。
    哇,代理的神奇用法,小本本記起來。
  5. 接着咱們看看怎麼經過method參數拿到MapperMethod
    這裏就很簡單了,Map裏邊有就直接返回,沒有就新建。接口的一個方法就對應一個MapperMethod。
    so easy ~

    //cachedMapperMethod
    private MapperMethod cachedMapperMethod(Method method) {
        // methodCache 是個Map<Method, MapperMethod>
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod 
                = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
  6. 看下MapperMethod的構造過程,發現傳入了接口信息,方法信息,還有配置信息。
    主要工做是初始化 command 還有 method 字段。
    command裏邊就保存方法的名稱(com.mapper.IUserMapper.findById),還有對應的SQL類型(SELECT)。
    method裏邊保存了方法的返回類型,是不是集合,是不是遊標等信息。
    看到這裏,其實我一直在忽略Configuration這個類裏邊是什麼東西,等要模仿再去看。

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

劇情3 之 MapperMethod你跟着幹了啥

咱們調用 mapper.findByid, 最終是經過MapperMethod執行execute獲得結果。
因此接下來要看看execute方法中隱藏了什麼祕密。

  1. 下邊是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;
    }
  2. 能夠看到 INSERT UPDATE 這些熟悉的字眼的了。
    經過MapperMethod裏的command的屬性,進入不一樣分支。
    這裏調用的是findById,進入了SELECT分支,最終執行了下邊的語句,第一句是裝配參數,第二句是執行查詢。

    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
  3. 看下 sqlSession.selectOne(),裏邊調用了selectList的方法,而後將結果返回。

    @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;
        }
    }
  4. 進入到selectList瞧瞧,感受流程要走完了,都已經開始select了。
    這裏又看到了使人激動的字眼,statement。感受已經在靠近JDBC啦。
    有個MappedStatement的對象須要關注一下。

    @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();
        }
    }
  5. 回憶下基本JDBC是怎麼用的。

    /*
      1 加載對應數據庫驅動
      Class.forName(driverClass);   
      
      2 獲取數據庫鏈接
      Connection con = DriverManager.getConnection(jdbcUrl, user, password);
      
      3 準備SQL語句
      String sql = " ... ";
      
      4 執行操做
      Statement statement = con.createStatement();
      statement.executeUpdate(sql);
      
      5 釋放資源
      statement.close();
      con.close();*/
  6. MappedStatement是個什麼東西呢,它對應着咱們的一條SQL語句。
    是經過MapperMethod的command對象的name屬性,從configuration裏邊拿到的。
  7. 拿到MappedStatement以後調用 executor的query方法,這個方法是CachingExecutor提供的。
    能夠看到,這裏經過MappedStatement獲取了咱們的SQL,而後生成一個緩存key,想起我記憶深處的mybatis一級二級緩存。
    以後返回調用query方法的結果。

    @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);
    }
  8. 仍是進入CachingExecutor的query方法。看來Executor這樣的類就是真正執行數據庫操做的類了。
    看到先是從MappedStatement裏邊拿緩存,若是是空的,就調用delegate.query,
    delegate是SimpleExecutor類型,顧名思義CachingExecutor委派了SimpleExecutor來進行數據庫操做。

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
     throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, parameterObject, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
  9. 繼續看SimpleExecutor裏邊的query,看到SimpleExecutor裏邊並無query方法,
    而是SimpleExecutor繼承了BaseExecutor,query是BaseExecutor類提供的。
    第一句斷點進去以後,看到的是存起來的"executing a query",這是出異常時的堆棧信息。
    emm..而後就是不少是否存在緩存是否使用緩存的代碼。
    咱直接看queryFromDatabase(),這個命名明顯告訴玩家,BOSS就在前面了。

    @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 {
                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;
    }
  10. 一樣是BaseExecutor 提供的 queryFromDatabase()方法。
    首先put進去了一個緩存,key是咱們以前的緩存鍵,值是一個默認的值,感受是佔位的意思。
    而後執行doQuery方法,看到do開頭的方法,就知道不簡單。doGet doPost
    doQuery是個抽象方法,咱們得去SimpleExecutor看實現。

    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 {
            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;
    }
  11. SimpleExecutor.doQuery
    來啦! Statement!並且還有咱們熟悉的prepareStatement字眼。哈哈 都是JDBC呀
    最後看到是由一個Handler來執行的,看一看這個Handler。

    @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();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.<E>query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
  12. 先是進入到了RoutingStatementHandler,而後RoutingStatementHandler委託給了PreparedStatementHandler,下邊是PreparedStatementHandler的query。
    看到想看的東西了,ps.execute()
    以後將結果交給resultSetHandler處理。

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
    }
  13. 回顧一下,咱們的問題是MapperMethod對象的execute方法作了什麼,結論就是,
    咱們經過MapperMethod的command屬性和method屬性,知道了要執行的SQL的類型,
    這裏咱們走的是SELECT路線。知道類型以後,交由SqlSession執行selectOne方法。
    而後又調用了DefaultSqlSession的selectList方法,DefaultSqlSession表示不想幹活,
    就交給了勤勞的BaseExecutor,BaseExecutor的裏邊有query方法,query方法作一些通用操做,
    看一眼有沒有緩存呀這些。在沒有或不用緩存的狀況下,再去調doQuery方法,doQuery方法有不一樣的實現。
    在doQuery以及其要調用方法裏邊使用的就是咱們熟悉的JDBC。執行完操做以後,將結果交給resultSetHandler。

總結

  • 咱們使用的是什麼?
答:使用的是咱們的接口的代理類的實例。
在構造代理類的實例時,
咱們傳入了實現了InvocationHandler接口的MapperProxy實例,
當代理對象調用方法時,會進入MapperProxy的invoke方法。
在invoke方法中經過Method對象找MapperMethod,
而後執行MapperMethod對象的execute方法。
在這裏,代理的做用是,讓咱們知道哪一個接口的哪一個方法被使用了。

MapperProxy 對應了咱們的一個接口,
MapperMethod 對應接口裏的一個方法,
MappedStatement 對應一條SQL

  • 從上邊流程中,總結各個類的職責
MapperProxy: 定義代理對象調用方法時執行的動做。
即在invoke()裏拿到調用的方法對應的MapperMethod,而後調用MapperMethod的execute。

MapperMethod: 對應咱們接口裏的方法,持有SqlCommand(command)和MethodSignature(method),
能夠知道方法的全名以及對應的SQL的類型。

MappedStatement: 保存的SQL的信息。

SqlSession: 玩家獲取Mapper的地方。僞裝執行SQL,實際交給了Executor。

Executor: 真正執行數據庫操做。

大體知道流程是什麼樣的,接着就能夠模仿着寫一寫了...emm...感受沒這麼簡單。

相關文章
相關標籤/搜索