Mybatis原始碼分析(四)mapper介面方法是怎樣被呼叫到的

清幽之地發表於2019-03-10

一、問題

在Mybatis架構的最上層就是介面層,它定義的是與資料庫互動的方式。還記不記得我們在前面章節說的那兩種方式?不記得沒關係,我們回憶一下。

  • Mybatis提供的API

使用Mybatis提供的API進行操作,通過獲取SqlSession物件,然後根據Statement Id 和引數來運算元據庫。

String statement = "com.viewscenes.netsupervisor.dao.UserMapper.getUserList";
List<User> result = sqlsession.selectList(statement);
複製程式碼
  • mapper介面

定義Mapper介面,裡面定義一系列業務資料操作方法。在Service層通過注入mapper屬性,呼叫其方法就可以執行資料庫操作。就像下面這樣

public interface UserMapper {	
	List<User> getUserList();
}

@Service
public class UserServiceImpl implements UserService{
	@Autowired
	UserMapper userDao;
	@Override
	public List<User> getUserList() {
		return userDao.getUserList();
	}
}
複製程式碼

那麼,問題就來了。UserMapper 只是個介面,並沒有任何實現類。那麼,我們在呼叫它的時候,它是怎樣最終執行到我們的SQL語句的呢

二、掃描

1、配置資訊

說到這,我們就要看配置檔案中的另外一個Bean。通過指定基本包的路徑,Mybatis可以通過Spring掃描下面的類,將其註冊為BeanDefinition物件。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="com.viewscenes.netsupervisor.dao" />
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
複製程式碼

或者有的朋友專案裡還有個annotationClass的屬性,即

<property name="annotationClass" value="org.springframework.stereotype.Repository" />
複製程式碼

它的作用就是在掃描的包的時候,會過濾定義的annotationClass。如果有這個註解才會被掃描,通常會在類上以@Repository來標識。不過它的作用也僅是為了過濾而已,我們也完全可以自定義這個註解。比如:

@MyDao
public interface UserMapper {}
<property name="annotationClass" value="com.viewscenes.netsupervisor.util.MyDao" />
複製程式碼

當然了,如果你確定基本包路徑下的所有類都要被註冊,那就不必配置annotationClass。

2、掃描基本包

我們來到org.mybatis.spring.mapper.MapperScannerConfigurer這個類,可以看到它實現了幾個介面。其中的重點是BeanDefinitionRegistryPostProcessor。它可以 動態的註冊Bean資訊,方法為postProcessBeanDefinitionRegistry()

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
	//建立ClassPath掃描器,設定屬性,然後呼叫掃描方法
	ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
	scanner.setAnnotationClass(this.annotationClass);
	scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
	//如果配置了annotationClass,就將其新增到includeFilters
	scanner.registerFilters();
	scanner.scan(this.basePackage);
}
複製程式碼

ClassPathMapperScanner繼承自Spring中的類ClassPathBeanDefinitionScanner,所以scan方法會呼叫到父類的scan方法,而在父類的scan方法中又呼叫到子類的doScan方法。

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
	public Set<BeanDefinitionHolder> doScan(String... basePackages) {
		//呼叫Spring的scan方法。就是將基本包下的類註冊為BeanDefinition
		Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
		processBeanDefinitions(beanDefinitions);
		return beanDefinitions;
	}
}
複製程式碼

super.doScan(basePackages)是Spring中的方法。我們在Spring系列文章中已經詳細分析了,在這裡就不細究。主要看它返回的是BeanDefinition的集合。

3、配置BeanDefinition

上面已經掃描到了所有的Mapper介面,並將其註冊為BeanDefinition物件。接下來呼叫processBeanDefinitions()要配置這些BeanDefinition物件。

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {

	private MapperFactoryBean<?> mapperFactoryBean = new MapperFactoryBean<Object>();
	
	private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
		GenericBeanDefinition definition;
		for (BeanDefinitionHolder holder : beanDefinitions) {
			definition = (GenericBeanDefinition) holder.getBeanDefinition();
			
			//將mapper介面的名稱新增到構造引數
			definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
			//設定BeanDefinition的class
			definition.setBeanClass(this.mapperFactoryBean.getClass());
			//新增屬性addToConfig
			definition.getPropertyValues().add("addToConfig", this.addToConfig);
			//新增屬性sqlSessionFactory
			definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
			......
	}
}
複製程式碼

處理的過程很簡單,就是往BeanDefinition物件中設定了一些屬性。我們重點關注兩個。

  • 設定beanClass

設定BeanDefinition物件的BeanClass為MapperFactoryBean<?>。這意味著什麼呢?以UserMapper為例,意味著當前的mapper介面在Spring容器中,beanName是userMapper,beanClass是MapperFactoryBean.class。那麼在IOC初始化的時候,例項化的物件就是MapperFactoryBean物件。

  • 設定sqlSessionFactory屬性

為BeanDefinition物件新增屬性sqlSessionFactory,這就意味著,在為BeanDefinition物件設定PropertyValue的時候,會呼叫到setSqlSessionFactory()

三、建立SqlSession的代理

上面我們說在為BeanDefinition物件設定PropertyValue的時候,會呼叫它的setSqlSessionFactory,我們來看這個方法。

首先,這裡說的BeanDefinition物件就是beanClass為MapperFactoryBean.class的MapperFactoryBean物件。定位到這個類,我們發現它繼承自org.mybatis.spring.support.SqlSessionDaoSupport

public abstract class SqlSessionDaoSupport extends DaoSupport {
	
	private SqlSession sqlSession;
	
	public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
		if (!this.externalSqlSession) {
			this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
		}
	}
}
複製程式碼

在它的setSqlSessionFactory方法裡,最終呼叫的是new SqlSessionTemplate()。所以sqlSession的物件其實是一個SqlSessionTemplate的例項。我們來看它的建構函式。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
	public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
						PersistenceExceptionTranslator exceptionTranslator) {
		
		//設定sqlSessionFactory
		this.sqlSessionFactory = sqlSessionFactory;
		//設定執行器的型別
		this.executorType = executorType;
		//異常相關處理類
		this.exceptionTranslator = exceptionTranslator;
		//sqlSession的代理
		this.sqlSessionProxy = (SqlSession) newProxyInstance(
		SqlSessionFactory.class.getClassLoader(),
		new Class[] { SqlSession.class },
		new SqlSessionInterceptor());
	}
}
複製程式碼

對JDK動態代理熟悉的朋友,一定會先看到newProxyInstance。它是給sqlSession介面建立了一個代理類,這個代理類的處理器程式就是SqlSessionInterceptor()。不用多說,SqlSessionInterceptor肯定實現了InvocationHandler介面。 這就意味著,當呼叫到sqlSession的時候,實際執行的它的代理類,代理類又會呼叫到處理器程式的invoke()方法。

private class SqlSessionInterceptor implements InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args){
		//內容先略過不看
	}
}
複製程式碼

最終在setSqlSessionFactory這個方法裡,sqlSession獲取到的是SqlSessionTemplate例項。而在SqlSessionTemplate物件中,主要包含sqlSessionFactory和sqlSessionProxy,而sqlSessionProxy實際上是SqlSession介面的代理物件。

sqlSession物件例項

四、建立Mapper介面的代理

上面我們說到MapperFactoryBean繼承自SqlSessionDaoSupport,還有一點沒說的是,它同時實現了FactoryBean介面。

這就說明,MapperFactoryBean不是一個純粹的人。啊不對,不是一個純粹的Bean,而是一個工廠Bean。如果要宣告一個Bean為工廠Bean,它要實現FactoryBean介面,這個介面就三個方法。

public interface FactoryBean<T> {
	//返回物件的例項
	T getObject() throws Exception;
	//返回物件例項的型別
	Class<?> getObjectType();
	//是否為單例
	boolean isSingleton();
}
複製程式碼

MapperFactoryBean既然是一個工廠Bean,那麼它返回就不是這個物件的本身,而是這個物件getObjectType方法返回的例項。為什麼會這樣呢? 在Spring中執行getBean的時候,在建立完Bean物件且完成依賴注入之後,用呼叫到 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);。 這個方法會判斷當前Bean是否為FactoryBean,如果不是就不再執行,如果是最終就會呼叫到它的getObject()方法。

protected Object getObjectForBeanInstance(
			Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {

	if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
		return beanInstance;
	}
	//getObjectFromFactoryBean最終呼叫的是getObject
	Object object = getObjectFromFactoryBean(factory, beanName, !synthetic);
	return object;
}
複製程式碼

說了這麼多,就是怕有的朋友對工廠Bean不瞭解,看這塊內容的時候會比較迷惑。那麼,getObject究竟會返回什麼物件呢?

1、getObject()

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
	public T getObject() throws Exception {	
		//mapperInterface是mapper介面的物件
		return getSqlSession().getMapper(this.mapperInterface);
	}
}
複製程式碼

getSqlSession()我們已經分析完了,它返回的就是SqlSessionTemplate物件的例項。所以,我們主要看getMapper()。

2、getMapper

public class MapperRegistry {
	public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
		final MapperProxyFactory<T> mapperProxyFactory = knownMappers.get(type);
		return mapperProxyFactory.newInstance(sqlSession);	 
	}
}
複製程式碼

我們看到,它實現比較簡單。不過,有個問題是knownMappers是從哪兒來的呢?它為什麼可以根據type介面就能獲取到MapperProxyFactory例項呢?

是否還記得,在掃描註解式SQL宣告的時候,它呼叫到addMapper方法,其實就是這個類。

public class MapperRegistry {
	public <T> void addMapper(Class<T> type) {
		if (type.isInterface()) {
			try {
				/注入type介面的對映
				knownMappers.put(type, new MapperProxyFactory<T>(type));
				//掃描註解
				MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
				parser.parse();
			}
		}
	}
}
複製程式碼

這也就解釋了為什麼knownMappers.get(type)就能獲取到MapperProxyFactory的例項,下面來看它內部到底建立了什麼物件並返回的。

3、newInstance

在建立過程中,實際返回的是一個代理類,即mapper介面的代理類。

public class MapperProxyFactory<T> {

	public T newInstance(SqlSession sqlSession) {
		//mapperProxy就是一個呼叫程式處理器,顯然它要實現InvocationHandler介面
		final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
		
		//JDK動態代理,生成的就是mapperInterface介面的代理類
		//mapperInterface就是我們的mapper介面
		//比如com.viewscenes.netsupervisor.dao.UserMapper
		return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
				new Class[] { mapperInterface }, mapperProxy);
	}
}
複製程式碼
public class MapperProxy<T> implements InvocationHandler {

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		//具體流程先略過....
		return method.invoke(this, args);
	}
}
複製程式碼

看到這裡,我們都已經明白了。getObject方法返回的就是mapper介面的代理類。換言之,每一個mapper介面對應的都是自身的介面代理。那麼,在實際呼叫到mapper方法的時候,就會呼叫到呼叫程式處理器的MapperProxy.invoke(Object proxy, Method method, Object[] args)

五、總結

本章節重要闡述了Mapper介面的代理建立過程。我們簡單梳理下流程:

  • 掃描mapper介面基本包,將為註冊為BeanDefinition物件。

  • 設定BeanDefinition的物件的beanClass和sqlSessionFactory屬性。

  • 設定sqlSessionFactory屬性的時候,呼叫SqlSessionTemplate的構造方法,建立SqlSession介面的代理類。

  • 獲取BeanDefinition物件的時候,呼叫其工廠方法getObject,返回mapper介面的代理類。

最後我們在Service層,通過@Autowired UserMapper userDao注入屬性的時候,返回的就是代理類。執行userDao的方法的時候,實際呼叫的是代理類的invoke方法。 最後的最後,我們看一下這個代理類長什麼樣子。

mapper介面的代理類例項

相關文章