前言
昨天,筆者在一篇面經中突然看到阿里的這樣一道面試題:
Mybatis中的Dao介面和XML檔案裡的SQL是如何建立關係的? 如果有兩個XML檔案和這個DAO建立關係,豈不是衝突了?
如果你看過筆者關於Mybatis原始碼分析的往期博文,相信你肯定可以給出一個不錯的答案。
但鑑於系列文章篇幅較大,而且重點是原始碼部分的解讀,所以筆者想再針對這個問題,再梳理下整個流程。
本文配合下列文章,食用更佳。
一、解析XML
首先,Mybatis在初始化SqlSessionFactoryBean
的時候,找到mapperLocations
路徑去解析裡面所有的XML檔案,這裡我們重點關注兩部分。
1、建立SqlSource
Mybatis會把每個SQL標籤封裝成SqlSource物件。然後根據SQL語句的不同,又分為動態SQL和靜態SQL。其中,靜態SQL包含一段String型別的sql語句;而動態SQL則是由一個個SqlNode組成。
假如我們有這樣一個SQL:
<select id="getUserById" resultType="user">
select * from user
<where>
<if test="uid!=null">
and uid=#{uid}
</if>
</where>
</select>
複製程式碼
它對應的SqlSource物件看起來應該是這樣的:
2、建立MappedStatement
XML檔案中的每一個SQL標籤就對應一個MappedStatement物件,這裡面有兩個屬性很重要。
- id
全限定類名+方法名組成的ID。
- sqlSource
當前SQL標籤對應的SqlSource物件。
建立完MappedStatement
物件,將它快取到Configuration#mappedStatements
中。
Configuration物件,我們知道它就是Mybatis中的大管家,基本所有的配置資訊都維護在這裡。把所有的XML都解析完成之後,Configuration就包含了所有的SQL資訊。
到目前為止,XML就解析完成了。看到上面的圖示,聰明如你,也許就大概知道了。當我們執行Mybatis方法的時候,就通過全限定類名+方法名
找到MappedStatement
物件,然後解析裡面的SQL內容,執行即可。
二、Dao介面代理
我們的Dao介面並沒有實現類,那麼,我們在呼叫它的時候,它是怎樣最終執行到我們的SQL語句的呢?
首先,我們在Spring配置檔案中,一般會這樣配置:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.viewscenes.netsupervisor.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
複製程式碼
或者你的專案是基於SpringBoot的,那麼肯定也見過這種:
@MapperScan("com.xxx.dao")
它們的作用是一樣的。將包路徑下的所有類註冊到Spring Bean中,並且將它們的beanClass設定為MapperFactoryBean
。有意思的是,MapperFactoryBean
實現了FactoryBean
介面,俗稱工廠Bean。那麼,當我們通過@Autowired
注入這個Dao介面的時候,返回的物件就是MapperFactoryBean
這個工廠Bean中的getObject()
方法物件。
那麼,這個方法幹了些什麼呢?
簡單來說,它就是通過JDK動態代理,返回了一個Dao介面的代理物件,這個代理物件的處理器是MapperProxy
物件。所有,我們通過@Autowired
注入Dao介面的時候,注入的就是這個代理物件,我們呼叫到Dao介面的方法時,則會呼叫到MapperProxy
物件的invoke方法。
對這塊內容不太能理解的朋友,可以先看看Spring中的FactoryBean 和 JDK動態代理相關知識
曾經有個朋友問過這樣一個問題:
對於有實現的dao介面,mapper還會用代理麼?
答案是肯定,只要你配置了MapperScan
,它就會去掃描,然後生成代理。但是,如果你的dao介面有實現類,並且這個實現類也是一個Spring Bean,那就要看你在Autowired
的時候,去注入哪一個了。
具體什麼意思呢?我們來到一個例子。
如果我們給userDao搞一個實現類,並且把它註冊到Spring。
@Component
public class UserDaoImpl implements UserDao{
public List<User> getUserList(Map<String,Object> map){
return new ArrayList<User>();
}
}
複製程式碼
然後我們在Service方法中,注入這個userDao。猜猜會發生什麼?
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userDao1;
public List<User> getUserList(Map<String,Object> map) {
return userDao1.getUserList(map);
}
}
複製程式碼
也許你已經猜到了,是的,它會啟動報錯。因為在注入的時候,找到了兩個UserMapper的例項物件。日誌是這樣的:
No qualifying bean of type [com.viewscenes.netsupervisor.dao.UserDao] is defined: expected single matching bean but found 2: userDaoImpl,userDao
當然了,也許我們的命名不太規範。其實我們通過名字注入就可以了,像這樣:
@Autowired UserMapper userDao;
或者 @Autowired UserMapper userDaoImpl;
再或者給你其中一個Bean加上@Primary註解。
具體原理,請參看筆者文章:徹底搞明白Spring中的自動裝配和Autowired
說著說著可能扯遠了,我們繼續回到Mybatis。那麼,目前為止,我們通過Dao介面也有了代理實現,所以就可以執行到它裡面的方法了。
三、執行
如上所述,當我們呼叫Dao介面方法的時候,實際呼叫到代理物件的invoke方法。 在這裡,實際上呼叫的就是SqlSession裡面的東西了。
public class DefaultSqlSession implements SqlSession {
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);
}
}
}
複製程式碼
看到以上程式碼,說明我們想的不錯。它就是通過statement全限定型別+方法名
拿到MappedStatement 物件,然後通過執行器Executor去執行具體SQL並返回。
四、總結
到這裡,再回到開頭我們提到的問題,也許你能更好的回答。同時筆者覺得,這道題目,如果你覆蓋到以下幾個關鍵詞,面試官可能會覺得很滿意。
- SqlSource以及動態標籤SqlNode
- MappedStatement物件
- Spring 工廠Bean 以及動態代理
- SqlSession以及執行器
那麼,針對第二個問題:如果有兩個XML檔案和這個Dao建立關係,豈不是衝突了?
答案也是顯而易見,不管有幾個XML和Dao建立關係,只要保證namespace+id
唯一即可。