Spring整合Mybatis原理
在介紹Spring整合Mybatis原理之前,我們得先來稍微介紹Mybatis的工作原理。
Mybatis的基本工作原理
在Mybatis中,我們可以使用一個接口去定義要執行sql,簡化代碼如下:
定義一個接口,@Select表示要執行查詢sql語句。
public interface UserMapper { @Select("select * from user where id = #{id}") User selectById(Integer id); }
以下為執行sql代碼:
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); // 以下使我們需要關注的重點 UserMapper mapper = sqlSession.getMapper(UserMapper.class); Integer id = 1; User user = mapper.selectById(id);
Mybatis的目的是:使得程序員能夠以調用方法的方式執行某個指定的sql,將執行sql的底層邏輯進行了封裝。
這里重點思考以下mapper這個對象,當調用SqlSession的getMapper方法時,會對傳入的接口生成一個代理對象,而程序要真正用到的就是這個代理對象,在調用代理對象的方法時,Mybatis會取出該方法所對應的sql語句,然后利用JDBC去執行sql語句,最終得到結果。
分析需要解決的問題
Spring和Mybatis時,我們重點要關注的就是這個代理對象。因為整合的目的就是:把某個Mapper的代理對象作為一個bean放入Spring容器中,使得能夠像使用一個普通bean一樣去使用這個代理對象,比如能被@Autowire自動注入。
比如當Spring和Mybatis整合之后,我們就可以使用如下的代碼來使用Mybatis中的代理對象了:
@Component public class UserService { @Autowired private UserMapper userMapper; public User getUserById(Integer id) { return userMapper.selectById(id); } }
UserService中的userMapper屬性就會被自動注入為Mybatis中的代理對象。如果你基於一個已經完成整合的項目去調試即可發現,userMapper的類型為:org.apache.ibatis.binding.MapperProxy@41a0aa7d。證明確實是Mybatis中的代理對象。
好,那么現在我們要解決的問題的就是:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?
要解決這個,我們需要對Spring的bean生成過程有一個了解。
Spring中Bean的產生過程
Spring啟動過程中,大致會經過如下步驟去生成bean
- 掃描指定的包路徑下的class文件
- 根據class信息生成對應的BeanDefinition
- 在此處,程序員可以利用某些機制去修改BeanDefinition
- 根據BeanDefinition生成bean實例
- 把生成的bean實例放入Spring容器中
假設有一個A類,假設有如下代碼:
一個A類:
@Component public class A { }
一個B類,不存在@Component注解
public class B { }
執行如下代碼:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println(context.getBean("a"));
輸出結果為:com.luban.util.A@6acdbdf5
A類對應的bean對象類型仍然為A類。但是這個結論是不確定的,我們可以利用BeanFactory后置處理器來修改BeanDefinition,我們添加一個BeanFactory后置處理器:
@Component public class LubanBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { BeanDefinition beanDefinition = beanFactory.getBeanDefinition("a"); beanDefinition.setBeanClassName(B.class.getName()); } }
這樣就會導致,原本的A類對應的BeanDefiniton被修改了,被修改成了B類,那么后續正常生成的bean對象的類型就是B類。此時,調用如下代碼會報錯:
context.getBean(A.class);
但是調用如下代碼不會報錯,盡管B類上沒有@Component注解:
context.getBean(B.class);
並且,下面代碼返回的結果是:com.luban.util.B@4b1c1ea0
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println(context.getBean("a"));
之所以講這個問題,是想說明一個問題:在Spring中,bean對象跟class沒有直接關系,跟BeanDefinition才有直接關系。
那么回到我們要解決的問題:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?
在Spring中,如果你想生成一個bean,那么得先生成一個BeanDefinition,就像你想new一個對象實例,得先有一個class。
解決問題
繼續回到我們的問題,我們現在想自己生成一個bean,那么得先生成一個BeanDefinition,只要有了BeanDefinition,通過在BeanDefinition中設置bean對象的類型,然后把BeanDefinition添加給Spring,Spring就會根據BeanDefinition自動幫我們生成一個類型對應的bean對象。
所以,現在我們要解決兩個問題:
- Mybatis的代理對象的類型是什么?因為我們要設置給BeanDefinition
- 我們怎么把BeanDefinition添加給Spring容器?
注意:上文中我們使用的BeanFactory后置處理器,他只能修改BeanDefinition,並不能新增一個BeanDefinition。我們應該使用Import技術來添加一個BeanDefinition。后文再詳細介紹如果使用Import技術來添加一個BeanDefinition,可以先看一下偽代碼實現思路。
假設:我們有一個UserMapper接口,他的代理對象的類型為UserMapperProxy。
那么我們的思路就是這樣的,偽代碼如下:
BeanDefinitoin bd = new BeanDefinitoin(); bd.setBeanClassName(UserMapperProxy.class.getName()); SpringContainer.addBd(bd);
但是,這里有一個嚴重的問題,就是上文中的UserMapperProxy是我們假設的,他表示一個代理類的類型,然而Mybatis中的代理對象是利用的JDK的動態代理技術實現的,也就是代理對象的代理類是動態生成的,我們根本無法確定代理對象的代理類到底是什么。
所以回到我們的問題:Mybatis的代理對象的類型是什么?
本來可以有兩個答案:
- 代理對象對應的代理類
- 代理對象對應的接口
那么答案1就相當於沒有了,因為是代理類是動態生成的,那么我們來看答案2:代理對象對應的接口
如果我們采用答案2,那么我們的思路就是:
BeanDefinition bd = new BeanDefinitoin(); // 注意這里,設置的是UserMapper bd.setBeanClassName(UserMapper.class.getName()); SpringContainer.addBd(bd);
但是,實際上給BeanDefinition對應的類型設置為一個接口是行不通的,因為Spring沒有辦法根據這個BeanDefinition去new出對應類型的實例,接口是沒法直接new出實例的。
那么現在問題來了,我要解決的問題:Mybatis的代理對象的類型是什么?
兩個答案都被我們否定了,所以這個問題是無解的,所以我們不能再沿着這個思路去思考了,只能回到最開始的問題:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?
總結上面的推理:我們想通過設置BeanDefinition的class類型,然后由Spring自動的幫助我們去生成對應的bean,但是這條路是行不通的。
終極解決方案
那么我們還有沒有其他辦法,可以去生成bean呢?並且生成bean的邏輯不能由Spring來幫我們做了,得由我們自己來做。
FactoryBean
有,那就是Spring中的FactoryBean。我們可以利用FactoryBean去自定義我們要生成的bean對象,比如:
@Component public class LubanFactoryBean implements FactoryBean { @Override public Object getObject() throws Exception { Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 執行代理邏輯 return null; } } }); return proxyInstance; } @Override public Class<?> getObjectType() { return UserMapper.class; } }
我們定義了一個LubanFactoryBean,它實現了FactoryBean,getObject方法就是用來自定義生成bean對象邏輯的。
執行如下代碼:
public class Test { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println("lubanFactoryBean: " + context.getBean("lubanFactoryBean")); System.out.println("&lubanFactoryBean: " + context.getBean("&lubanFactoryBean")); System.out.println("lubanFactoryBean-class: " + context.getBean("lubanFactoryBean").getClass()); } }
將打印:
lubanFactoryBean: com.luban.util.LubanFactoryBean$1@4d41cee
&lubanFactoryBean: com.luban.util.LubanFactoryBean@3712b94
lubanFactoryBean-class: class com.sun.proxy.$Proxy20
從結果我們可以看到,從Spring容器中拿名字為"lubanFactoryBean"的bean對象,就是我們所自定義的jdk動態代理所生成的代理對象。
所以,我們可以通過FactoryBean來向Spring容器中添加一個自定義的bean對象。上文中所定義的LubanFactoryBean對應的就是UserMapper,表示我們定義了一個LubanFactoryBean,相當於把UserMapper對應的代理對象作為一個bean放入到了容器中。
但是作為程序員,我們不可能每定義了一個Mapper,還得去定義一個LubanFactoryBean,這是很麻煩的事情,我們改造一下LubanFactoryBean,讓他變得更通用,比如:
@Component public class LubanFactoryBean implements FactoryBean { // 注意這里 private Class mapperInterface; public LubanFactoryBean(Class mapperInterface) { this.mapperInterface = mapperInterface; } @Override public Object getObject() throws Exception { Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 執行代理邏輯 return null; } } }); return proxyInstance; } @Override public Class<?> getObjectType() { return mapperInterface; } }
改造LubanFactoryBean之后,LubanFactoryBean變得靈活了,可以在構造LubanFactoryBean時,通過構造傳入不同的Mapper接口。
實際上LubanFactoryBean也是一個Bean,我們也可以通過生成一個BeanDefinition來生成一個LubanFactoryBean,並給構造方法的參數設置不同的值,比如偽代碼如下:
BeanDefinition bd = new BeanDefinitoin(); // 注意一:設置的是LubanFactoryBean bd.setBeanClassName(LubanFactoryBean.class.getName()); // 注意二:表示當前BeanDefinition在生成bean對象時,會通過調用LubanFactoryBean的構造方法來生成,並傳入UserMapper bd.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getName()) SpringContainer.addBd(bd);
特別說一下注意二,表示表示當前BeanDefinition在生成bean對象時,會通過調用LubanFactoryBean的構造方法來生成,並傳入UserMapper的Class對象。那么在生成LubanFactoryBean時就會生成一個UserMapper接口對應的代理對象作為bean了。
到此為止,其實就完成了我們要解決的問題:把Mybatis中的代理對象作為一個bean放入Spring容器中。只是我們這里是用簡單的JDK代理對象模擬的Mybatis中的代理對象,如果有時間,我們完全可以調用Mybatis中提供的方法區生成一個代理對象。這里就不花時間去介紹了。
Import
到這里,我們還有一個事情沒有做,就是怎么真正的定義一個BeanDefinition,並把它添加到Spring中,上文說到我們要利用Import技術,比如可以這么實現:
定義如下類:
public class LubanImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); beanDefinition.setBeanClass(LubanFactoryBean.class); beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class); // 添加beanDefinition registry.registerBeanDefinition("luban"+UserMapper.class.getSimpleName(), beanDefinition); } }
並且在AppConfig上添加@Import注解:
@Import(LubanImportBeanDefinitionRegistrar.class) public class AppConfig {
這樣在啟動Spring時就會新增一個BeanDefinition,該BeanDefinition會生成一個LubanFactoryBean對象,並且在生成LubanFactoryBean對象時會傳入UserMapper.class對象,通過LubanFactoryBean內部的邏輯,相當於會自動生產一個UserMapper接口的代理對象作為一個bean。
總結
總結一下,通過我們的分析,我們要整合Spring和Mybatis,需要我們做的事情如下:
- 定義一個LubanFactoryBean
- 定義一個LubanImportBeanDefinitionRegistrar
- 在AppConfig上添加一個注解@Import(LubanImportBeanDefinitionRegistrar.class)
優化
這樣就可以基本完成整合的需求了,當然還有兩個點是可以優化的
第一,單獨再定義一個@LubanScan的注解,如下:
@Retention(RetentionPolicy.RUNTIME) @Import(LubanImportBeanDefinitionRegistrar.class) public @interface LubanScan { }
這樣在AppConfig上直接使用@LubanScan即可
第二,在LubanImportBeanDefinitionRegistrar中,我們可以去掃描Mapper,在LubanImportBeanDefinitionRegistrar我們可以通過AnnotationMetadata獲取到對應的@LubanScan注解,所以我們可以在@LubanScan上設置一個value,用來指定待掃描的包路徑。然后在LubanImportBeanDefinitionRegistrar中獲取所設置的包路徑,然后掃描該路徑下的所有Mapper,生成BeanDefinition,放入Spring容器中。
所以,到此為止,Spring整合Mybatis的核心原理就結束了,再次總結一下:
- 定義一個LubanFactoryBean,用來將Mybatis的代理對象生成一個bean對象
- 定義一個LubanImportBeanDefinitionRegistrar,用來生成不同Mapper對象的LubanFactoryBean
- 定義一個@LubanScan,用來在啟動Spring時執行LubanImportBeanDefinitionRegistrar的邏輯,並指定包路徑
以上這個三個要素分別對象org.mybatis.spring中的:
- MapperFactoryBean
- MapperScannerRegistrar
- @MapperScan