對於整合Spring及Mybatis不作詳細介紹,可以參考: MyBatis
3 User Guide Simplified Chinese.pdf 或者 mybatis_spring整合——1。貼出我的主要代碼如下:
package org.denger.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.denger.po.User;
public interface UserMapper {
@Select("select * from tab_uc_account where id=#{userId}")
User getUser(@Param("userId") Long userId);
}
application-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- Provided by annotation-based configuration -->
<context:annotation-config/>
<!--JDBC Transaction Manage -->
<bean id="dataSourceProxy" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
<constructor-arg>
<ref bean="dataSource" />
</constructor-arg>
</bean>
<!-- The JDBC c3p0 dataSource bean-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/noah" />
<property name="user" value="root" />
<property name="password" value="123456" />
</bean>
<!--MyBatis integration with Spring as define sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="basePackage" value="org.denger.mapper"></property>
</bean>
</beans>
Test Case
package org.denger.mapper;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
@ContextConfiguration(locations = { "/application-context.xml"})
public class UserMapperTest extends AbstractJUnit4SpringContextTests{
@Autowired
public UserMapper userMapper;
@Test
public void testGetUser(){
Assert.assertNotNull(userMapper.getUser(300L));
}
}
實現原理分析
對於以上極其簡單代碼看上去並無特殊之處,主要亮點在於 UserMapper 居然不用實現類,而且在調用 getUser 的時候,也是使用直接調用了UserMapper實現類,那麼Mybatis是如何去實現 UserMapper的接口的呢? 可能你馬上能想到的實現機制就是通過動態代理方式,好吧,看看MyBatis整個的代理過程吧。
首先在Spring的配置文件中看到下面的Bean:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="basePackage" value="org.denger.mapper"></property>
</bean>
以上的MapperScannerConfigurer class的註釋中描述道:
從base 包中搜索所有下面所有 interface,並將其註冊到 Spring Bean容器中,其註冊的class bean是MapperFactoryBean。
好吧,看看它的註冊過程,下面方法來從 MapperScannerConfigurer中的Scanner類中抽取,下面方法在初始化以上application-content.xml文件時就會進行調用。 主要用於是搜索 base packages 下的所有mapper class,並將其註冊至 spring 的 benfinitionHolder中。
/**
* Calls the parent search that will search and register all the candidates. Then the
* registered objects are post processed to set them as MapperFactoryBeans
*/
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
//#1
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + MapperScannerConfigurer.this.basePackage
+ "' package. Please check your configuration.");
} else {
//#2
for (BeanDefinitionHolder holder : beanDefinitions) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '"+ definition.getBeanClassName() + "' mapperInterface");
}
//#3
definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
definition.setBeanClass(MapperFactoryBean.class);
}
return beanDefinitions;
}
#1: 瞭解Spring初始化Bean過程的人可能都知道,Spring 首先會將需要初始化的所有class先通過BeanDefinitionRegistry進行註冊,並且將該Bean的一些屬性信息(如scope、className、beanName等)保存至BeanDefinitionHolder中;Mybatis這裏首先會調用Spring中的ClassPathBeanDefinitionScanner.doScan方法,將所有Mapper接口的class註冊至BeanDefinitionHolder實例中,然後返回一個Set,其它包含了所有搜索到的Mapper
class BeanDefinitionHolder對象。
#2: 首先,由於 #1 中註冊的都是接口class, 可以肯定的是接口是不能直接初始化的;實際 #2 中for循環中替換當前所有 holder的 className爲 MapperFactoryBean.class,並且將 mapper interface的class name setter 至 MapperFactoryBean 屬性爲 mapperInterface 中,也就是 #3 代碼所看到的。
再看看 MapperFactoryBean,它是直接實現了 Spring 的 FactoryBean及InitializingBean 接口。其實既使不看這兩個接口,當看MapperFactoryBean的classname就知道它是一個專門用於創建 Mapper 實例Bean的工廠。
至此,已經完成了Spring與mybatis的整合的初始化配置的過程。
接着,當我在以上的 Test類中,需要注入 UserMapper接口實例時,由於mybatis給所有的Mapper 實例註冊都是一個MapperFactory的工廠,所以產生UserMapper實現仍需要 MapperFactoryBean來進行創建。接下來看看 MapperFactoryBean的處理過程。
先需要創建Mapper實例時,首先在 MapperFactoryBean中執行的方法是:
/**
* {@inheritDoc}
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.sqlSession.getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
configuration.addMapper(this.mapperInterface);
}
}
上面方法中先會檢測當前需要創建的 mapperInterface在全局的 Configuration中是否存在,如果不存在則添加。呆會再說他爲什麼要將 mapperInterface 添加至 Configuration中。該方法調用完成之後,就開始調用 getObject方法來產生mapper實例,看到MapperFactoryBean.getObject的該方法代碼如下:
/**
* {@inheritDoc}
*/
public T getObject() throws Exception {
return this.sqlSession.getMapper(this.mapperInterface);
}
通過Debug可以看到 sqlSession及mapperInterface對象:
到目前爲止我們的 UserMapper 實例實際上還並未產生;再進入 org.mybatis.spring.SqlSessionTemplate 中的 getMapper 方法,該方法將 this 及 mapper interface class 作爲參數傳入 org.apache.ibatis.session.Configuration的getMapper() 方法中,代碼如下:
/**
* {@inheritDoc}
*/
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
於是,再進入 org.apache.ibatis.session.Configuration.getMapper 中代碼如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
其中 mapperRegistry保存着當前所有的 mapperInterface class. 那麼它在什麼時候將 mapperinterface class 保存進入的呢?其實就是在上面的 afterPropertiesSet 中通過 configuration.addMapper(this.mapperInterface) 添加進入的。
再進入 org.apache.ibatis.binding.MapperRegistry.getMapper 方法,代碼如下:
/**
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//首先判斷當前knownMappers是否存在mapper interface class.
//因爲前面說到 afterPropertiesSet 中已經將當前的 mapperinterfaceclass 添加進入了。
if (!knownMappers.contains(type))
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
try {
return MapperProxy.newMapperProxy(type, sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
嗯,沒錯,看到 MapperProxy.newMapperProxy後可以肯定的是它確實採用的代理模式,再進入一看究竟吧:
public static <T> T newMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) {
ClassLoader classLoader = mapperInterface.getClassLoader();
Class[] interfaces = new Class[]{mapperInterface};
MapperProxy proxy = new MapperProxy(sqlSession);
return (T) Proxy.newProxyInstance(classLoader, interfaces, proxy);
}
JDK的動態代理就不說了,至此,基本Mybatis已經完成了Mapper實例的整個創建過程,也就是你在具體使用 UserMapper.getUser 時,它實際上調用的是 MapperProxy,因爲此時 所返回的 MapperProxy是實現了 UserMapper接口的。只不過 MapperProxy攔截了所有對userMapper中方法的調用,如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果調用不是 object 中默認的方法(如equals之類的)
if (!OBJECT_METHODS.contains(method.getName())) {
//因爲當前MapperProxy代理了所有 Mapper,所以他需要根據當前攔截到的方法及代理對象獲取 MapperInterface class,也就是我這裏的 UserMapper.class
final Class declaringInterface = findDeclaringInterface(proxy, method);
//然後再根據UserMapper.class、及當前調用的Method(也就是getUser)及SqlSession構造一個 MapperMethod,在這裏面會獲取到 getUser方法上的 @Select() 的SQL,然後再通過 sqlSession來進行執行
final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession);
//execute執行數據庫操作,並返回結果集
final Object result = mapperMethod.execute(args);
if (result == null && method.getReturnType().isPrimitive()) {
throw new BindingException("Mapper method '" + method.getName() + "' (" + method.getDeclaringClass() + ") attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
爲什麼說它攔截了呢?可以看到, 它並沒有調用 method.invoke(object)方法,因爲實際上 MapperProxy只是動態的 implement 了UserMapper接口,但它沒有真正實現 UserMapper中的任何方法。至於結果的返回,它也是通過 MapperMethod.execute 中進行數據庫操作來返回結果的。說白了,就是一個實現了 MapperInterface 的 MapperProxy 實例被MapperProxy代理了,可以debug看看 userMapper實例就是 MapperProxy。