簡單使用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的一次完整執行的大概流程就是這樣!