KeyWords: Mybatis 原理,源碼,Mybatis Mapper 接口實現類,代理模式,動態代理,Java動態代理,Proxy.newProxyInstance,Mapper 映射,Mapper 實現
MyBatis 是一款優秀的持久層框架,它支持定制化 SQL、存儲過程以及高級映射。MyBatis 避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集。我們在使用 Mybaits 進行 ,通常只需要定義幾個 Mapper 接口,然后在編寫一個 xml 文件,我們在配置文件中寫好 sql , Mybatis 幫我們完成 Mapper 接口道具體實現的調用。以及將結果映射到 model bean 中。
我們在項目中所編寫的眾多的 Mapper 類只是一個接口(interface ),根據 Java 的多態性我們知道,可以使用接口接口作為形參,進而在運行時確定具體實現的對象是什么。但是,對於 Mapper 接口,我們並沒有編寫其實現類!Mybatis是如何找到其實現類,進而完成具體的 CRUD 方法調用的呢?原理何在?
Mapper 接口是怎么找到實現類的
為了弄清楚 Mapper 接口是如何找到實現類的,我們先回憶一下 Mybatis 是怎么使用的,根據實際的例子,進而一點點的去分析。這里的使用指的是Mybatis 單獨使用,而不是整合 spring , 因為整合 spring 的話,還需要涉及 Mapper dao 裝載到 spring 容器的問題,spring 幫忙創建數據源配置等問題。
通常我們使用 Mybatis 的主要步驟是:
- 構建 SqlSessionFactory ( 通過 xml 配置文件 , 或者直接編寫Java代碼)
- 從 SqlSessionFactory 中獲取 SqlSession
- 從SqlSession 中獲取 Mapper
- 調用 Mapper 的方法 ,例如:blogMapper.selectBlog(int blogId)
從一段代碼看起
上面我們概括了使用 Mybatis 的4個步驟。這4個步驟看起來很簡單,但是用代碼寫出來就很多。我們不妨先記着這4個步驟,再去看代碼,會容易點。
// 1.
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);// 添加Mapper接口
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 2.
SqlSession session = sqlSessionFactory.openSession();
try {
// 3.
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 4.
Blog blog = mapper.selectBlog(1);
} finally {
session.close();
}
在這塊代碼中,第 1 部分我們使用了 Java 編碼的形式來實現 SqlSessionFactory ,也可以使用 xml 。如果使用xml的話,上面的第一部分代碼就是這樣的:
String resource = "org/mybatis/example/mybatis-config.xml"; // xml內容就不貼了
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我們本次的目標是弄清楚 “ Mapper 是如何找到實現類的 ”,我們注意上面代碼 3 , 4 的位置:
// 3.
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 4.
Blog blog = mapper.selectBlog(1);
這里 mapper 可以調用selectBlog(1) 這個方法,說明 mapper 是個對象,因為對象才具有方法行為實現啊。BlogMapper接口是不能實例化的,更沒有具體方法實現。我們並沒有定義一個類,讓它實現BlogMapper接口,而在這里它只是通過調用session.getMapper() 所得到的。由此,我們可以推斷:肯定是session.getMapper() 方法內部產生了BlogMapper的實現類。有什么技術可以根據BlogMapper 接口生成了一個實現類呢?想到這里,對於有動態代理
使用經驗的程序員來說,很容易想到,這背后肯定是基於動態代理技術,具體怎么實現的呢?下面我們來根據源碼一探究竟。
Mapper 接口的注冊
從上面的代碼中,我們知道 BlogMapper 接口的實現類是從session.getMapper中得來的,大概是基於動態代理技術實現。我們既然能夠從SqlSession中得到BlogMapper接口的,那么我們肯定需要先在哪里把它放進去了,然后 SqlSession 才能生成我們想要的代理類啊。上面代碼中有這么一行:
configuration.addMapper(BlogMapper.class);
跟着這個 addMapper 方法的代碼實現是這樣的:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
我們看到這里 mapper 實際上被添加到 mapperRegissry 中。繼續跟進代碼:
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers
= new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) { // 只添加接口
if (hasMapper(type)) { // 不允許重復添加
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type)); // 注意這里
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
看到這里我們知道上面所執行的configuration.addMapper(BlogMapper.class);
其實最終被放到了HashMap中,其名為knownMappers
,knowMappers
是MapperRegistry 類的一個私有屬性,它是一個HashMap
。其Key
為當前Class對象,value
為一個MapperProxyFactory
實例。
這里我們總結一下: 諸如BlogMapper
之類的Mapper接口被添加到了MapperRegistry
中的一個HashMap中。並以 Mapper 接口的 Class 對象作為 Key , 以一個攜帶Mapper接口作為屬性的MapperProxyFactory
實例作為value 。MapperProxyFacory
從名字來看,好像是一個工廠,用來創建Mapper Proxy的工廠。我們繼續往下看。
Mapper接口的動態代理類的生成
上面我們已經知道,Mapper 接口被到注冊到了MapperRegistry
中——放在其名為knowMappers 的HashMap屬性中,我們在調用Mapper接口的方法的時候,是這樣的:
BlogMapper mapper = session.getMapper(BlogMapper.class);
這里,我們跟蹤一下session.getMapper() 方法的代碼實現,這里 SqlSession 是一個接口,他有兩個實現類,一個是DefaultSqlSession
,另外一個是SqlSessionManager
,這里我們用的是DefaultSqlSession
. 為什么是DefaultSqlSession
呢?因為我們在初始化SqlSessionFactory的時候所調用的SqlSessionFactoryBuilder
的build()方法里邊配置的就是DefaultSqlSession
, 所以,我們進入到DefaultSession類中,看看它對session.getMapper(BlogMapper.class)
是怎么實現的:
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this); //最后會去調用MapperRegistry.getMapper
}
}
如代碼所示,這里的 getMapper 調用了 configuration.
MapperRegistry
,而此前我們已經知道,
MapperRegistry
是存放了一個HashMap的,我們繼續跟蹤進去看看,那么這里的get,肯定是從這個hashMap中取數據。我們來看看代碼:
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();// Mapper 映射
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory =
(MapperProxyFactory<T>) knownMappers.get(type);
try {
return mapperProxyFactory.newInstance(sqlSession); // 重點看這里
} catch (Exception e) {
}
}
}
我們調用的session.getMapper(BlogMapper.class);
最終會到達上面這個方法,這個方法,根據BlogMapper
的class對象,以它為key
在knowMappers
中找到了對應的value
—— MapperProxyFactory(BlogMapper) 對象,然后調用這個對象的newInstance()
方法。根據這個名字,我們就能猜到這個方法是創建了一個對象,代碼是這樣的:
public class MapperProxyFactory<T> { //映射器代理工廠
private final Class<T> mapperInterface;
private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 刪除部分代碼,便於閱讀
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//使用了JDK自帶的動態代理生成映射器代理類的對象
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
看到這里,就清楚了,最終是通過Proxy.newProxyInstance
產生了一個BlogMapper的代理對象。Mybatis 為了完成 Mapper 接口的實現,運用了代理模式。具體是使用了JDK動態代理,這個Proxy.newProxyInstance
方法生成代理類的三個要素是:
- ClassLoader —— 指定當前接口的加載器即可
- 當前被代理的接口是什么 —— 這里就是 BlogMapper
- 代理類是什么 —— 這里就是 MapperProxy
代理模式中,代理類(MapperProxy)中才真正的完成了方法調用的邏輯。我們貼出MapperProxy的代碼,如下:
public class MapperProxy<T> implements InvocationHandler, Serializable {// 實現了InvocationHandler
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理以后,所有Mapper的方法調用時,都會調用這個invoke方法
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args); // 注意1
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method); // 使用了緩存
//執行CURD
return mapperMethod.execute(sqlSession, args); // 注意2
}
}
我們調用的 Blog blog = mapper.selectBlog(1);
實際上最后是會調用這個MapperProxy
的invoke
方法。這段代碼中,if
語句先判斷,我們想要調用的方法是否來自Object類,這里的意思就是,如果我們調用toString()
方法,那么是不需要做代理增強的,直接還調用原來的method.invoke()就行了。只有調用selectBlog()
之類的方法的時候,才執行增強的調用——即mapperMethod.execute(sqlSession, args);
這一句代碼邏輯。
而mapperMethod.execute(sqlSession, args);
這句最終就會執行增刪改查了,代碼如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) { //insert 處理,調用SqlSession的insert
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) { // update
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) { // delete
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
// 刪除部分代碼
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 刪除部分代碼
return result;
}
再往下一層,就是執行JDBC那一套了,獲取鏈接,執行,得到ResultSet,解析ResultSet映射成JavaBean。
至此,我們已經摸清楚了Blog blog = mapper.selectBlog(1);
中,BlogMapper接口調用到得到數據庫數據過程中,Mybaitis 是如何為接口生成實現類的,以及在哪里出發了最終的CRUD調用。實際上,如果我們在調用Blog blog = mapper.selectBlog(1);
之前,把從slqSession中得到的 mapper
對象打印出來就會看到,輸出大概是這樣的:
com.sun.proxy.$Proxy17
動態代理沒錯吧,Java動態代理實在是太美妙了。
總結
上面我們用層層深入的方式摸清楚了 Mapper接口是如何找到實現類的。我們分析了 Mapper接口是如何注冊的,Mapper接口是如何產生動態代理對象的,Maper接口方法最終是如何執行的。總結起來主要就是這幾個點:
1. Mapper 接口在初始SqlSessionFactory 注冊的。
2. Mapper 接口注冊在了名為 MapperRegistry 類的 HashMap中, key = Mapper class value = 創建當前Mapper的工廠。
3. Mapper 注冊之后,可以從SqlSession中get
4. SqlSession.getMapper 運用了 JDK動態代理,產生了目標Mapper接口的代理對象。
5. 動態代理的 代理類是 MapperProxy ,這里邊最終完成了增刪改查方法的調用。