本文將通過模擬Mybatis動態代理生成Mapper代理類,講解Mybatis原理
1.平常我們是如何使用Mapper的
先寫一個簡單的UserMapper,它包含一個全表查詢的方法,代碼如下
public interface UserMapper {
@Select("select * from user")
public List<User> queryAll();
}
然后大家思考一個問題,我們平時是怎么使用這個UserMapper的?
很多時候我們會把Mybatis和Spring整合起來一起使用,於是會有類似下面的代碼:
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
public List<User> queryAll(){
return this.userMapper.queryAll();
}
}
看到這段熟得不能再熟的代碼不知道大家會不會有一絲疑惑:UserMapper明明是一個接口,為什么可以直接調用他的queryAll方法呢?
這個問題其實也不難解,我們不能直接調用一個接口的方法,這背后肯定是有一個對象的,至於這個對象是怎么來的,這里直接告訴大家是通過動態代理生成的。只要弄懂了這個動態代理對象是怎么生成的,整個Mybatis框架原理就就說清楚了。在模擬之前我們先驗證一下是不是使用JDK的動態代理。
2.驗證Mybatis是通過動態代理生成Mapper代理對象
由於上面一段代碼整合了spring,spring又為我們封裝了許多細節,我們重新看一段代碼,看看沒有spring的情況下我們怎么獲得一個UserMapper
public static void main(String[] args){
SqlSessionFactory sqlSessionFactory = .... //這里省略,官網給了很多配置SqlSessionFactory的方法(不一定是這么獲得)
SqlSession session = sqlSessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
}
為了驗證獲得UserMapper采用的是動態代理,我們可以在IDE中對着session.getMapper(UserMapper.class)
一路按着Ctrl點進去,我們會發現最終調用的代碼是這樣的:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
最后果然調用的是JDK的動態代理
3.動手模擬一次Mybatis的動態代理
既然知道了原理,下面我們就動手驗證吧!
為了簡單明了,我們不寫SqlSessionFactory類了,直接自定義一個MySession類,在里面給出模擬的getMapper方法:
public class MySession {
public static Object getMapper(Class clazz){
//調用newProxyInstance需要傳入class數組
Class[] clazzs = new Class[]{clazz};
//把動態代理過程中生成的代理類保留下來,有助於新手理解動態代理(此行可省略)
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//通過動態代理生成Mapper
Object object = Proxy.newProxyInstance(MySession.class.getClassLoader(), clazzs, new MyInvocationHandler());
return object;
}
}
如果不熟悉JDK的動態代理也沒關系,下面我會逐步分析。
大家可以看到調用動態代理生成動態代理對象需要三個參數:
- 類加載器
- class數組
- InvocationHandler實例
為什么需要類加載器?
動態代理生成類和其調用類必須通過同一個類加載器加載,否則它們之間無法相互調用
為什么有個class數組?
JDK的動態代理是基於接口的,class數組中存放的是動態代理類需要實現的接口。比如本文中的例子生成的動態代理類需要實現UserMapper接口,所以你得把接口告訴它。
為什么會有InvocationHandler實例?
動態代理會在原有方法上實現增強,而增強的邏輯就寫在InvocationHandler類的invoke方法上,所以要有這么個實例。
你想一想,當初的這段代碼
public interface UserMapper {
@Select("select * from user")
public List<User> queryAll();
}
,我們想實現一個怎樣的功能?
無非就是給它一條sql語句,希望它能去數據庫中執行這條sql語句並返回結果。這個過程可以拆分成兩個部分:
- 得到sql語句
- 通過JDBC操作數據庫,並執行sql返回結果
這里省略JDBC的過程,給出一個簡單的invoke方法示例(Mybatis為我們封裝了一切JDBC的處理細節):
public class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//解析得到sql
Select annotation = method.getAnnotation(Select.class);
String sql = annotation.value()[0];
//執行sql(模擬JDBC)
System.out.println(sql + " executing...");
return null;
}
}
最終我們執行UserMapper的queryAll()方法時,就會出現如下結果:
public class Main {
public static void main(String[] args) {
UserMapper userMapper = (UserMapper) MySession.getMapper(UserMapper.class);
userMapper.query();
}
}
//打印:
//select * from user executing...
總結
最后我們總結一下Mybatis框架的核心原理:
- 用戶只需要創建Mapper接口,並使用Mapper接口即可。
- Mybatis會對Mapper接口產生動態代理對象,這個動態代理對象實現了Mapper接口,擁有Mapper中定義的所有方法,並對這些方法進行了增強。增強的邏輯是獲得sql語句和執行sql語句。
通過個核心原理我們也就知道了Mybatis為我們做了什么:
- 讓方法和sql語句對應起來,操作數據庫就如同調用方法一般簡單
- 屏蔽掉JDBC的細節