Spring原始碼之一步步拓展實現spring-mybatis
講在前面
上一章 Spring原始碼之BeanFactoryPostProcessor的執行順序,我們掌握了 BeanFactoryPostProcessor 的執行順序。
這一章,我們就來看一下程式設計師要如何使用 BeanFactoryPostProcessor 對 Spring 進行拓展? 本文以 mybatis 為例,看看 mybatis-spring 是如何將 Spring 和 Mybatis 做整合的?
首先,我們當然需要通過官方網站來了解 mybatis 和 mybatis-spring :
網站 | 網址 |
---|---|
mybatis英文 | https://mybatis.org/mybatis-3/ |
mybatis中文 | https://mybatis.org/mybatis-3/zh/ |
mybatis-spring英文 | https://mybatis.org/spring/ |
mybatis-spring中文 | https://mybatis.org/spring/zh/ |
這次實驗需要用到的,寫在子模組 build.gradle 中的依賴
dependencies { compile(project(":spring-context")) compile('org.mybatis:mybatis:3.5.0') compile('org.mybatis:mybatis-spring:2.0.5') // JDBC驅動 compile('mysql:mysql-connector-java:8.0.20') // 輕量連線池 compile(project(":spring-jdbc")) testCompile group: 'junit', name: 'junit', version: '4.12' }
MyBatis-Spring Quick Start
配置類
@MapperScan("coderead.springframework.dao") @ComponentScan("coderead.springframework") public class AppConfig { @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); return factoryBean.getObject(); } @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } }
- SqlSessionFactoryBean 正是 FactoryBean 的子類,也是依賴項 mybatis-spring 的類
- DriverManagerDataSource 是依賴專案 spring-jdbc 中的連線池的類,這個類在開發演示中比較輕量和簡單。複雜的商用連線池有 druid 和 j3p0
- MapperScan 告訴 Spring 要去哪裡掃描 DAO
常見服務類:
@Component
public class UserService {
@Autowired
private UserDao dao;
public List<Map<String, Object>> query() {
return dao.query();
}
}
- 這是我們專案中常寫的服務類的樣式,使用 @Component 註解 UserService 類,並且使用 @Autowired 註解自動注入 UserDao 的例項
DAO介面類:
public interface UserDao {
@Select("select * from user")
public List<Map<String,Object>> query();
}
應用啟動類:
public class MyApplication {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
UserService service = ctx.getBean(UserService.class);
System.out.println(service.query());
}
}
如果你比較傾向於看官方文件,那麼在此給你推薦:
Mybatis Quick Start
我們來看一下如何使用 Java 的方式獲取 SqlSessionFactory 物件:
依賴結構如下:
當我們得到了 SqlSessionFactory 物件,我們就可以獲取 SqlSession 物件,進而得到 Mapper 物件:
SqlSession session = sqlSessionFactory.openSession();
UserDao mapper = session.getMapper(UserDao.class);
System.out.println(mapper.query());
SqlSession.getMapper可以幫我們得到一個物件,且這個物件必然是實現了 UserDao 介面的。
要滿足這兩點,常用的核心技術就是JDK 動態代理
/**
* 根據介面類(可以多個),生成一個動態代理物件
* @param loader 類載入器
* @param interfaces 需要代理物件實現的一組介面
* @param h 分發方法觸發的處理器
*/
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
InvocationHandler 介面
/**
* 處理代理物件的觸發方法,並且返回結果物件
* @param proxy 代理物件
* @param method 反射方法
* @param args 方法引數
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
SqlSession#getMapper方法的呼叫時序圖如下圖所示:
- DefaultSqlSession 依賴成員變數 Configuration,所以可以通過該成員變數直接呼叫 configuration#getMapper(Class cls, SqlSession session)
- Configuration、MapperRegistry 的 getMapper方法引數除了 Class 類物件引數,還有 SqlSession 物件引數(即上一步的 DefaultSqlSession 物件)
如果你比較傾向自己去官方網站查證,那麼在此推薦連結:
過程簡化
我們知道了原理是JDK動態代理,那麼我們可以用 MockSqlSession 簡化模擬一個 Mybatis 的 SqlSession 來執行 getMapper 方法。當觸發代理介面物件的方法時的邏輯是先獲取資料庫連線,然後執行 sql 語句,我們都用列印日誌的方式來簡化示意。
public class MockSqlSession {
public static Object getMapper(Class mapper) {
Class[] classes = new Class[]{mapper};
return Proxy.newProxyInstance(MockSqlSession.class.getClassLoader(), classes, new MyInvocationHandler());
}
static class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------- getConnection -------");
Select select = method.getAnnotation(Select.class);
String[] sqls = select.value();
System.out.println("------- execute : " + sqls[0] + " -------");
return null;
}
}
}
接著我們不用 mybatis-spring 中的 MapperScan, 也不用 SqlSessionFactoryBean, 因此我們來對 AppConfig 類做一些刪減:
@ComponentScan("coderead.springframework")
public class AppConfig {
}
然後我們再次執行 MyApplication,執行之後出現如下異常
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userService': Unsatisfied dependency expressed through field 'dao'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'coderead.springframework.dao.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
分析: 無法建立 userService Bean物件,因為無法注入 UserDao。找不到 UserDao 的 BeanDefinition,Spring 沒法幫我們建立一個 userDao Bean物件。MyBatis 是用 JDK 動態代理來建立 UserDao 物件的,MyBatis 需要自己掌控 UserDao 的建立過程,因此不能把類交給 Spring 管理,而是要把建立好的物件交給 Spring 管理!
把物件交給 Spring 管理
如何把 MyBatis 的物件交給 Spring 管理?可選的方法有:
- FactoryBean
- AnnotationConfigApplicationContext#getBeanFactory().registerSingleton()
- @Bean
registerSingleton
我們向 Spring 中注入了一個 Singleton 物件
public class MyApplication {
public static void main(String[] args) throws IOException {
UserDao dao = (UserDao) MockSqlSession.getMapper(UserDao.class);
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getBeanFactory().registerSingleton("userDao", dao);
ctx.register(AppConfig.class);
ctx.refresh();
UserService service = ctx.getBean(UserService.class);
System.out.println(service.query());
}
}
這種寫法意味著,每多一個 XXXDao,都需要呼叫獲取 XXXDao 物件,並通過 registerSingleton 方法註冊到 Spring 中去。
@Bean
還原 MyApplication 類,修改 AppConfig 類改用註解方法注入 UserDao
public class MyApplication {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
UserService service = ctx.getBean(UserService.class);
System.out.println(service.query());
}
}
AppConfig類
@ComponentScan("coderead.springframework")
public class AppConfig {
@Bean(name = "userDao")
public UserDao userDao() {
return (UserDao) MockSqlSession.getMapper(UserDao.class);
}
}
這種寫法意味著,每多一個 XXXDao,都需要寫一段相似度極高的@Bean方法,註冊 XXXDao Bean物件到 Spring 中去。假如有 100 個 Dao,那麼 AppConfig 中就有 100 段 @Bean 的程式碼
FactoryBean
FactoryBean 是一個特殊的Bean,它必須實現一個介面,這個 FactoryBean 還能產生一個 Bean。
我們去掉 AppConfig 中的 @Bean 的程式碼
@Component("userDao")
public class MockFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
return MockSqlSession.getMapper(UserDao.class);
}
@Override
public Class<?> getObjectType() {
return UserDao.class;
}
}
這裡需要注意的是雖然 "userDao" 在單例池 singletonObjects 中對應的是 MockFactoryBean 物件:
但是,使用"userDao"這個名稱獲取到的 Bean 是 MockFactoryBean 所產生的 Bean 物件,即實現了 UserDao 的動態代理物件。
public class MyApplication {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
System.out.println(ctx.getBean("userDao") instanceof UserDao);
}
}
實驗結果如下:
如果想要獲取 MockFactoryBean 物件,應該 ctx.getBean("&userDao"), beanName 多加上一個 & 符號。
由 MockFactoryBean.getObject() 獲取的物件會存放在 FactoryBeanRegistrySupport 成員變數 factoryBeanObjectCache 中:
MyBatis 使用的就是 FactoryBean 的方式來把物件交給 Spring 管理的
MapperFactoryBean
MyBatis 是以第三方 jar 的形式被我們所使用的,最好不要用@Component註解,因為那樣需要使用者配置掃描 MyBatis 的包。如果本來是內部專案,然後捐給了 Apache,強制要修改包名了,配置掃描那就不太好。
這裡使用了 mapperInterface 變數支援動態建立不同 DAO 介面的 MapperFactoryBean 物件。解決的問題:
原來需要每新增一個 XXXDAO 類,就要對應新增一個 XXXFactoryBean。現在方便了,使用者建立再多的 XXXDAO 類,也只需要一個 MapperFactoryBean 類!。這就是設計成員變數 mapperInterface 的作用!
我們再來改寫我們的 MockFactoryBean,刪除 @Component 註解,新增成員變數 mapperInterface:
public class MockFactoryBean implements FactoryBean {
private Class mapperInterface;
public MockFactoryBean() {
}
public MockFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public void setMapperInterface(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
return MockSqlSession.getMapper(mapperInterface);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
Injecting Mappers
既然我們不使用 @Component,現在就需要我們來考慮注入的邏輯了。首先是注入一個 Mapper 類的方法,官網上提供了指南
註冊單個 mapper
首先修改 AppConfig 類,新增一個 @Bean 註解註冊一個 MockFactoryBean,引數是 UserDao.class。
@ComponentScan("coderead.springframework")
public class AppConfig {
@Bean
public MockFactoryBean userFactoryBean() {
MockFactoryBean bean = new MockFactoryBean(UserDao.class);
return bean;
}
}
然後測試 MyApplication :
public class MyApplication {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
UserDao dao = (UserDao) ctx.getBean(UserDao.class);
System.out.println(dao.query());
}
}
這種注入單個 Mapper 的方式有一個非常大的缺陷,每次新增一個 XXXDao,AppConfig 類中就需要新增加一段“類似”的 @Bean 的程式碼,只是把建構函式的入參改為 XXXDao.class 這一點變化。
為了解決這個缺陷,MyBatis 提供的方案是掃描所有的 Mapper。
模擬掃描
如果希望自定義的 FactoryBean 能夠提供我們所期望的 Bean 物件,首先要保證 FactoryBean 在 Spring 容器中。那麼,如何使得 FactoryBean 在 Spring 容器中呢?
- 加 @Component 註解,但是這個方法不能傳遞 mapperInterface 引數,該方法 pass!
- spring.xml 中加入 <bean> 標籤(或者在 AppConfig 類中加入 @Bean) ———— 該方式可以傳遞 mapperInterface 引數,但是不能實現掃描功能,該方法 pass!
- 拓展 Spring ,把自定義 FactoryBean 對應類的 BeanDefinition 放入 beanDefinitionMap 中。
首先想到使用 BeanPostProcessor ,但是不可行。因為 postProcessBeanFactory(ConfigurableListableBeanFactory factory) 方法只能修改已有的 BeanDefinition,不能新增 BeanDefinition!
新增 BeanDefinition
新增 BeanDefinition 需要藉助 ImportBeanDefinitionRegistrar:
public class MockImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MockFactoryBean.class);
builder.addPropertyValue("mapperInterface", "coderead.springframework.dao.UserDao");
BeanDefinition bd = builder.getBeanDefinition();
registry.registerBeanDefinition("mockFactoryBean", bd);
}
}
這段程式碼需要注意的是 builder.addPropertyValue("mapperInterface", "coderead.springframework.dao.UserDao"),實際上呼叫的程式碼是 this.beanDefinition.getPropertyValues().add("mapperInterface", "coderead.springframework.dao.UserDao"),這裡"coderead.springframework.dao.UserDao"表示的是類的路徑。
使 ImportBeanDefinitionRegistrar 生效
使用 @Import 註解來使得自定義的 MockImportBeanDefinitionRegistrar 生效
@ComponentScan("coderead.springframework")
@Import(MockImportBeanDefinitionRegistrar.class)
public class AppConfig {
}
批量新增 BeanDefinition
public class MockImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
List<Class> list = new ArrayList<>();
list.add(UserDao.class);
// ...這裡還可以有更多 Dao
for (Class aClass : list) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MockFactoryBean.class);
builder.addPropertyValue("mapperInterface", aClass.getName());
BeanDefinition bd = builder.getBeanDefinition();
registry.registerBeanDefinition(aClass.getSimpleName(), bd);
}
}
}
總結
由於篇幅原因,本文就不再繼續介紹實現掃描的方法,下一篇文章,會分析 mybatis-spring 的原始碼,來看看 mybatis 到底是如何實現掃描的。
通過本文,我們可以學到的是
- 把物件交給 Spring 管理的幾種方法:① FactoryBean ② @Bean ③registerSingleton
- 新增 BeanDefinition 的方法:加上 @Import 註解實現 ImportBeanDefinitionRegistrar 的類