MyBatis是可以說是目前最主流的Spring持久層框架了,本文主要探討SpringBoot集成MyBatis的底層原理。完整代碼可移步Github。
如何使用MyBatis
一般情況下,我們在SpringBoot項目中應該如何集成MyBatis呢?
- 引入MyBatis依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
- 配置數據源
- 在啟動類上添加
@MapperScan
注解,傳入需要掃描的dao層的包路徑 - 在dao層中創建接口,在方法上傳入對應的SQL語句,也可以使用Mapper.xml文件進行配置。例如:
public interface UserDao {
@Select("insert into user xxx")
void insert();
}
- 完成這些工作后,我們就可以自動注入一個userDao,然后調用userDao.insert()方法來實現對數據庫的操作。
那么問題來了,MyBatis是如何通過如此簡單的配置完成完成與Spring的“無縫連接”和數據的持久化工作的呢?
Spring的BeanDefinition
眾所周知,Spring的一大特性是IoC,既控制反轉。當我們將一個對象交給Spring管理之后,我們就不需要手動地通過new
關鍵字去創建對象,只需要通過@Autowired
或者@Resource
自動注入即可。那么這個過程是如何實現的呢?
簡單來說,Spring會通過一個被聲明為bean的類信息生成一個beanDefinition(后面簡稱BD)對象,然后通過這個BD對象創建一個單例(不聲明為prototype的情況下),存入單例池,需要時進行調用。
在創建beadDefinition時,Spring會調用一系列的后置處理器(postProcessor)對BD加以處理,我們也可以自定義后置處理器,對BD的屬性進行修改,只需要實現BeanFactoryPostProcessor
接口即可,例如:
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
GenericBeanDefinition userDaoBD = (GenericBeanDefinition)beanFactory.getBeanDefinition("userDao");
userDaoBD.setBeanClassName("userDaoChanged");
userDaoBD.setAbstract(false);
// more...
}
}
在這個postProcessor中,我們獲取了UserDao的BD對象,並且將它的名字修改為"userDaoChanged",這樣我們就可以通過調用ApplicationContext的getBean("userDaochanged")方法獲取到原來的userDao的bean。
關於MyBatis
現在我們知道了,當我們將一個類交給Spring管理時,Spring通過beanDefinition構建bean單例。現在,我們又有了兩個新的問題:
- MyBatis如何將dao交給Spring管理的?
- 我們編寫的dao是一個接口,接口是如何實例化的?
MyBatis如何將dao交給Spring管理的?
在Spring中,將一個對象交給Spring管理主要有三種方式:
- @Bean
- beanFactory.registerSingleton(String beanName, Object singletonObject)
- FactoryBean
其中MyBatis使用的是FactoryBean的方式,實現FactoryBean接口就可以將我們的userDao注入到Spring當中。
beanFactory管理着Spring所有的bean,是一個大工廠。FactoryBean也是一個bean,但它卻有着Factory的功能,當我們調用Spring上下文的getBean()方法,並傳入自定義的FactoryBean時,返回的bean並不是這個FactoryBean本身,而是重寫的getObject()方法中所返回的對象。
如此看來,FactoryBean就是一個“小工廠”。
@Component
public class UserFactoryBean implements FactoryBean {
UserDao userDao;
@Override
public Object getObject() throws Exception {
return userDao;
}
@Override
public Class<?> getObjectType() {
return UserDao.class;
}
}
只是這樣寫當然是不能滿足我們的要求的,我們這時候調用getBean()方法會報錯,我們無法傳入一個userDao參數,因為UserDao不能被實例化。但是在MyBatis中,我們卻可以通過sqlSession.getMapper(UserDao.class)
方法獲取到一個UserDao的實例化對象,MyBatis是如何做到這一點的?
如何實例化一個接口?
為什么接口可以被實例化呢?查看MyBatis的源碼我們可以得知,MyBatis通過動態代理(Proxy)的技術在接口的基礎上包裝出了一個對象,然后將這個對象交給了Spring。沿着getMapper方法追根溯源我們可以發現這樣一個方法:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
MyBatis可以,那我們也可以,我們改寫一下UserFatoryBean:
@Component
public class UserFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
Class[] classes = new Class[]{UserDao.class};
return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
}
@Override
public Class<?> getObjectType() {
return UserDao.class;
}
}
Proxy.newProxyInstance()方法接收三個參數,分別為:
- ClassLoader loader:決定用哪個類加載器來加載生成的代理對象
- Class<?>[] interfaces:決定這個代理對象要實現哪些接口,擁有哪些功能
- InvocationHandler h:決定調用這個代理對象的某個方法時執行的具體邏輯
然后編寫一個InvokationHandler類用於執行代理對象的具體方法邏輯:
public class MyInvokationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("假裝查詢數據庫:" + method.getAnnotation(Select.class).value()[0]);
return null;
}
}
在這個handler中,我們獲取到@Select
注解中的信息,然后將它打印出來。
OK,現在我們運行一下:
@Test
void contextLoads() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
UserDao userDao = (UserDao) ac.getBean("userFactoryBean");
userDao.insert();
}
控制台輸出:
假裝查詢數據庫:insert into user xxx
至此,我們就完成了MyBatis的簡易實現的一小部分。但是還有一個重要的問題:我們這個FactoryBean是寫死的,只能返回UserDao的代理對象,實際情境下,我們如何根據用戶傳入的參數返回不同的代理對象呢?
動態生成代理對象
想要動態生成代理對象,首先我們需要修改UserFactoryBean的代碼,讓它能適配各種類型的dao,不妨直接改個名字叫MyFactoryBean:
public class MyFactoryBean implements FactoryBean {
Class mapperInterface;
// 為了支持XML配置,必須提供一個默認構造方法
public MyFactoryBean(){}
// 通過MapperScan方式
public MyFactoryBean(Class mapperInterface){
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
Class[] classes = new Class[]{mapperInterface};
return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
我們將UserDao.class改成了動態的mapperInterface,那么我們如何向MyFactoryBean的構造方法傳入這個參數呢?這就回到了我們一開始說到的beanDefinition,在Spring中,可以在BD期間修改bean的各種屬性,這其中就包括構造方法的參數。我們修改我們一開始寫的MyBeanFactoryPostProcessor:
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
List<Class> daos = new ArrayList<>();
// 獲取所有dao
daos.add(UserDao.class);
daos.add(AnchorDao.class);
for(Class dao:daos){
// 獲取一個空的beanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
// 為構造方法添加參數
beanDefinition.setBeanClass(MyFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(dao);
}
}
}
這樣的話我們就構造出了我們想要的beanDefinition,現在要做的是把它加入到Spring容器中去。注意:是把beanDefinition加入到Spring容器中,而不是把bean加到Spring容器中。我們前面說的@Bean之內的方法是不適用的。
這里需要用到另兩個知識點:@Import
和ImportBeanDefinitionRegistrar
在應用中,有時沒有把某個類注入到IOC容器中,但在運用的時候需要獲取該類對應的bean,此時就需要用到@Import注解。
@Import最強大的地方在於,它提供了一個擴展點給用戶。當我們用@Import導入的類實現了ImportBeanDefinitionRegistrar接口時,Spring不會直接將這個類包裝成一個bean,而是執行其內部的registerBeanDefinitions方法。這有點像FactoryBean,可以在類中執行自己的邏輯。
我們編寫這樣一個registerBeanDefinitions:
public class ImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
// 獲得包名,遍歷獲得所有類名
String packagePath = Appconfig.class.getAnnotation(MyScan.class).path();
List<String> classNames = SelectClassName.selectByPackage(packagePath);
for(String className:classNames){
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
registry.registerBeanDefinition(SelectClassName.getShortName(className),genericBeanDefinition);
// 添加構造方法參數
genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(className);
}
}
}
並且編寫一個MyScan注解類用戶獲取需要掃描的包名:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyScan {
String path();
}
然后在AppConfig類上加入@MyScan
注解,傳入包名,最后編寫一個工具類用來獲取包下的所有類名。MyBeanFactoryPostProcessor
類也可以刪除了,ImportBeanDefinitionRegister
替代了它的工作。
(完整代碼可以訪問我的Github)
@Test
void contextLoads() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
ac.getBean(AnchorDao.class).query();
ac.getBean(UserDao.class).insert();
}
控制台輸出:
假裝查詢數據庫:select * from anchor limit 5
假裝查詢數據庫:insert into user xxx
大功告成!回頭再看一下我們是如何使用MyBatis的:@MapperScan
、編寫dao接口、@Select
——和我們現在的功能幾乎完全一樣。只需要在MyInvokationHandler
中封裝一下JDBC,實現具體的訪問數據庫邏輯,你就可以在項目中使用自己編寫的“MyBatis”了。
總結
Spring提供了很好的環境用於第三方框架的開發,這也是Spring能發展出如今這樣龐大且完善的生態的原因之一。知識都是觸類旁通的,例如Spring的另一大特性:AOP,它就與本文談到的后置處理器beanPostProcessor和動態代理有關,對SpringBoot集成MyBatis底層原理的學習和研究,讓我對Spring和MyBatis都有了更深入的認識。(累死我了,歇會兒(;´д`)ゞ)