1. 程式人生 > 實用技巧 >Spring原始碼之一步步拓展實現spring-mybatis

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 中的連線池的類,這個類在開發演示中比較輕量和簡單。複雜的商用連線池有 druidj3p0
  • 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 管理?可選的方法有:

  1. FactoryBean
  2. AnnotationConfigApplicationContext#getBeanFactory().registerSingleton()
  3. @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 容器中呢?

  1. 加 @Component 註解,但是這個方法不能傳遞 mapperInterface 引數,該方法 pass!
  2. spring.xml 中加入 <bean> 標籤(或者在 AppConfig 類中加入 @Bean) ———— 該方式可以傳遞 mapperInterface 引數,但是不能實現掃描功能,該方法 pass!
  3. 拓展 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 到底是如何實現掃描的。
通過本文,我們可以學到的是

  1. 把物件交給 Spring 管理的幾種方法:① FactoryBean ② @Bean ③registerSingleton
  2. 新增 BeanDefinition 的方法:加上 @Import 註解實現 ImportBeanDefinitionRegistrar 的類