Mybatis執行流程,源碼解析

簡單使用Mybatis

在看Mybatis的內部執行原理之前,先簡單看一下我們要怎麼樣配置好然後進行使用:

先看一下整個結構:

在這裏插入圖片描述

mybatis.xml:

在這裏插入圖片描述

StudentMapper2.xml:

在這裏插入圖片描述

StudentDao2:

在這裏插入圖片描述

基本的配置大概就是這樣了,然後就是使用了:

在這裏插入圖片描述

順序大概是這樣的:

1.獲取配置文件的輸入流對象

1.以輸入流對象作爲參數,獲取SqlSessionFactory對象

2.通過其工廠類對象,創建一個SqlSession

3.通過SqlSession對象獲取接口對象

4.調用接口對象的方法,完成數據庫操作。

源碼解析

獲取配置文件的輸入流

OK,那一切都是從獲取mybatis配置文件的輸入流對象開始的,我們點進getResourceAsStream()方法裏面康康到底發生了什麼

public static InputStream getResourceAsStream(String resource) throws IOException {
    //把ClassLoader作爲參數作爲null,直接調用了重載函數
    return getResourceAsStream((ClassLoader)null, resource);
}

點進這個重載函數看看:

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    } else {
        return in;
    }
}

噢~原來需要返回的輸入流對象就是在這裏創建好的,點進去classLoaderWrapper.getResourceAsStream()看看是怎麼創建的?它最終會調用這個方法:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    ClassLoader[] var3 = classLoader;
    int var4 = classLoader.length;

    for(int var5 = 0; var5 < var4; ++var5) {
        ClassLoader cl = var3[var5];
        if (null != cl) {
            //用類加載器來加載資源
            InputStream returnValue = cl.getResourceAsStream(resource);
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }

            if (null != returnValue) {
                return returnValue;
            }
        }
    }

    return null;
}

噢~這個輸入流是通過類加載器來加載的啊,那看看cl.getResourceAsStream(resource),最終會到這裏:

public URL getResource(String name) {
    Objects.requireNonNull(name);
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = BootLoader.findResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}

這不就是一個雙親委派模型嗎?沒錯,就是類加載器通過雙親委派模型加載出對應的URL類對象:

在這裏插入圖片描述

然後再由URL類對象創建配置文件的輸入流對象,然後再返回,至此,**Resources.getResourceAsStream(“mybatis.xml”)**這個函數就執行完成啦!

獲取SqlSessionFactory對象

獲取到了配置文件的輸入流對象,接下來就根據這個對象,獲取SqlSessionFactory這個工廠類對象,也就是:

SqlSessionFactory sessionFactory= new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis.xml"));

的build方法。build()方法最終就調用它的這個重載方法:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        //主要是這一行
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();

        try {
            inputStream.close();
        } catch (IOException var13) {
            ;
        }

    }

    return var5;
}

這一行執行起來其實很複雜。但總的來說,就是把配置文件的信息加載進來一個叫做Configuration對象實例中了。

parser.parse()其實就是獲取了這個Configuration對象,也就是說,我們寫的xml配置文件的內容,還有數據庫相關信息,其實都儲存在其中了,然後調用另一個重載方法,傳入這個Configuration對象

var5 = this.build(parser.parse());

該方法就是

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

然後就獲得一個DefaultSqlSessionFactory(SqlSessionFactory的子類)對象實例了。

至此,我們的SqlSessionFactory對象就創建完成了,其中就包括Configuratin對象,也就是配置文件的信息都存在裏面啦~

創建SqlSession

SqlSession sqlSession=sessionFactory.openSession();

也就是這句

先來看一下openSession()方法:

public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}

調用了openSessionFromDataSource方法,那再看看這個方法吧:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }

    return var8;
}

其實就是對Configuration裏面的東西一頓加載,然後逐個賦值給DefaultSqlSession對象,然後進行返回。

那看看返回的SqlSession對象裏面有什麼東西:

在這裏插入圖片描述

裏面同樣有一個Configuration對面,裏面同樣存有數據庫相關信息。

那SqlSession也成功獲得了。

獲取Mapper接口對象

獲取了SqlSession之後,

StudentDao2 studentDao2=sqlSession.getMapper(StudentDao2.class);

就可以獲取對應的接口對象(也就是和Mapper文件對應的那些接口),之後便可以調用接口中的方法了。

這個地方很神奇,爲什麼能直接調用接口的方法?其實是調用了動態代理的設計模式來做到的,具體的代理模式介紹呢,可以看一下這篇文章:

設計模式(十二)代理模式.

接下來繼續看代碼吧:

getMapper()函數最終會調用這個方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //這裏是一個map,根據傳入的Class對象獲取對應的代理類
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            //這個工廠類去創建代理類
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}

這裏的關鍵就是這個knownMappers了,這裏面存的是啥呢?請看:

在這裏插入圖片描述

其實這就是一個HashMap,以接口的Class對象爲key,對應代理類的工廠類作爲value,這樣在sqlSession中傳入Class對象,其實就能直接獲取到代理類的工廠類了。然後再調用其newInstance()方法

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

this.newInstance(mapperProxy)其實就是:

protected T newInstance(MapperProxy<T> mapperProxy) {
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

mapperInterface的內容: interface com.jay.dao.StudentDao2。這就是一個很經典的動態代理模式了!

可以看到,我們獲得的接口對象,其實準確來說並不是接口對象,而是Proxy代理類對象:

在這裏插入圖片描述

那麼至此,就很明朗了,我們獲得了接口的代理類對象,接下來的方法的調用,其實都只是告訴代理對象:我用了xxx方法,你去調用對應的jdbc吧!

接口方法的調用

接下里就調用接口的方法了,以getAll2()爲例,當前是會進入到代理類的invoke()方法中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        //如果它是Object中的方法,比如hashCode(),equals()
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        //如果它是實現的方法,而不是抽象方法
        if (this.isDefaultMethod(method)) {
            return this.invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable var5) {
        throw ExceptionUtil.unwrapThrowable(var5);
    }
    //我們調用的是接口方法,所以就會走到這裏
    
    //這個就是查緩存
    MapperMethod mapperMethod = this.cachedMapperMethod(method);
    //這個就是真正執行sql語句,也就是調用jdbc
    return mapperMethod.execute(this.sqlSession, args);
}

調用完jdbc之後,把結果封裝成需要的數據結構,如果有需要的話,還要寫緩存,然後再進行返回。

這樣,Mybatis的一整個操作就完成啦!

所以當面試官問:你知道Mybatis的執行流程嗎?

要聊Mybatis的內部執行流程,我覺得應該要先從我們開發人員調用的Mybatis API開始說。

當一些基礎配置完成之後,我們想要使用Mybatis,以最初始的方法爲例,我們要先創建配置文件的InputStream輸入流對象,然後作爲參數,獲得SqlSessionFactory這個工廠類對象,之後調用其openSession()方法獲取一個SqlSession實例,sqlSession實例就是關鍵,我們調用它的getMapper()方法,傳入接口的Class對象作爲參數,就能獲得對應的接口對象了,這裏說的接口就是Dao層的那些和Mapper文件對應的接口。然後調用接口的方法就可以完成對應的數據庫操作。

那就先從獲取配置文件的輸入流對象開始說,這個過程概括起來其實很簡單,就是根據我們傳入的字符串,應用類加載器加載出一個對應URL類對象,使用的自然也是雙親委派模型。然後調用該url類的openStream()方法就可以返回對應的InputStream對象。

獲得對象之後,下一步就是要獲得SqlSessionFactory這個工廠類對象。其實這一步邏輯很簡單但是很重要,這一步做的事情就是對Mybatis配置文件,也就是那個xml文件進行解析,將解析得到的結果放在一個叫Configuration的類對象中,Configuration很重要,是Mybatis的核心組件,我們寫的配置文件的設置都存放在其中,包括使用哪個數據庫?是線上環境的還是測試環境的?數據庫賬號密碼;還有數據庫連接池的一些相關參數。然後將這個Configuration對象作爲參數賦值給DefaultSqlSessionFactory的構造函數中,生成工廠類對象然後返回。

工廠類對象獲得,下一個就是要獲得SqlSession對象,其實這一步也很簡單,其實就是創建出一個DefaultSqlSession實例,然後按照Configuration裏面設置的屬性對其進行初始化之後,將這個DefaultSqlSession對象返回。

獲得了SqlSession對象之後,最關鍵的一步來了,那就是調用它的getMapper()方法,傳入接口的Class對象,就會返回對應的接口實例了。那這時候很奇怪的一點是,爲什麼我們後面可以直接調用這個接口的方法呢?其實這裏用了動態代理的設計模式,準確來說,我們得到的接口對象並不是接口對象,而是Proxy這個代理類實例。所以接口的方法調用最終都會去到代理類對象的invoke()方法上。具體源碼上的實現上,就是在這個方法裏面,會去一個叫做knownMappers的結構中,這個結構其實就是一個HashMap,它的key爲接口的Class對象,而value就是對應代理類的工廠類,所以說,我們傳入Class對象,實際上就是來這個HashMap中獲取了對應的代理類的工廠類,然後用這個工程類創建出對應的代理類,然後進行返回。所以才說爲什麼我們獲得的其實是一個Proxy類對象。

那獲得代理對象之後,一切就明朗了,接口方法的調用其實都是去到了代理對象的invoke方法中,那invoke方法當然就是訪問數據庫的地方了,它會先去查詢緩存,如果緩存沒有或者,纔會去執行jdbc來訪問數據庫,將結果封裝爲需要返回的對象,有需要的話還要寫入緩存再返回結果。然後再執行結束。

Mybatis的一次完整執行的大概流程就是這樣!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章