MyBatis基于接口编程的原理分析

源码

UserMapper

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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。

1
2
3
4
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="basePackage" value="org.denger.mapper"></property>
</bean>

1
2
3
4
5
/**
* BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for
* interfaces and registers them as {@code MapperFactoryBean}. Note that only interfaces with at
* least one method will be registered; concrete classes will be ignored.
* <p>

以上的MapperScannerConfigurer的注释中描述道。
从base包中搜索所有下面所有interface,并将其注册到Spring Bean容器中,其注册的class bean是MapperFactoryBean
看看它的注册过程,从MapperScannerConfigurer#Scanner类中抽取,在初始化以上application-content.xml文件时就会进行调用。
主要用于是搜索base packages下的所有mapper.class,并将其注册至spring的benfinitionHolder中。

MapperScannerConfigurer#Scanner#doScan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 
1. Calls the parent search that will search and register all the candidates. Then the
2. 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的一些属性信息(如scopeclassNamebeanName等)保存至BeanDefinitionHolder中;MyBatis这里首先会调用Spring中的ClassPathBeanDefinitionScanner#doScan(),将所有Mapper接口的class注册至BeanDefinitionHolder实例中,然后返回一个Set<BeanDefinitionHolder>,包含所有搜索到的Mapper#BeanDefinitionHolder对象。
  2. 由于#1中注册的都是接口class, 可以肯定的是接口是不能直接初始化的;for循环中替换当前所有holderclassNameMapperFactoryBean.class,并且将 mapper interfaceclass name setterMapperFactoryBean 属性为 mapperInterface 中,也就是 #3代码所看到的。

再看看MapperFactoryBean,它是直接实现了Spring的FactoryBeanInitializingBean接口。其实既使不看这两个接口,当看MapperFactoryBeanclassname就知道它是一个专门用于创建 Mapper 实例Bean的工厂。
至此,已经完成了Spring与MyBatis的整合的初始化配置的过程。
接着,在以上的Test类中,需要注入UserMapper接口实例时,由于MyBatis给所有的Mapper实例注册都是一个MapperFactory的生成,所以产生UserMapper实现仍需要MapperFactoryBean来进行创建。接下来看看 MapperFactoryBean的处理过程。
先需要创建Mapper实例时,首先在 MapperFactoryBean中执行的方法。

MapperFactoryBean#afterPropertiesSet()

1
2
3
4
5
6
7
8
9
10
11
12
/** 
* {@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()

1
2
3
4
5
6
/** 
* {@inheritDoc}
*/
public T getObject() throws Exception {
return this.sqlSession.getMapper(this.mapperInterface);
}

通过Debug可以看到 sqlSessionmapperInterface对象:

到目前为止UserMapper实例实际上还并未产生,再进入org.mybatis.spring.SqlSessionTemplate#getMapper(),该方法将thismapper interface class 作为参数传入 org.apache.ibatis.session.Configuration#getMapper() ,代码如下。

SqlSessionTemplate#getMapper()

1
2
3
4
5
6
/** 
* {@inheritDoc}
*/
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}

org.apache.ibatis.session.Configuration#getMapper()

1
2
3
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()

1
2
3
4
5
6
7
8
9
10
11
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  
// #1
if (!knownMappers.contains(type))
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
try {
// #2
return MapperProxy.newMapperProxy(type, sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
  1. 首先判断当前knownMappers是否存在mapper interface class,因为前面说到afterPropertiesSet中已经将当前的mapper interface class添加进入了。
  2. 生成Mapper Proxy对象。

    MapperProxy#newMapperProxy()

    1
    2
    3
    4
    5
    6
    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中方法的调用,如下。

MapperProxy#invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 中进行数据库操作来返回结果的。
就是一个实现了 MapperInterfaceMapperProxy 实例被MapperProxy代理了,可以Debug看看 userMapper实例就是 MapperProxy

Mybatis版本差异

以上文章很早以前写的,现在用Mybatis3.4.0版本,2个版本存在一些差异。

MapperFactoryBean#checkDaoConfig()

MapperFactoryBean#afterPropertiesSet方法,在3.4版本中不存在,3.4版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void checkDaoConfig() {
super.checkDaoConfig();

notNull(this.mapperInterface, "Property 'mapperInterface' is required");

Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}

MapperProxy#invoke()

在3.4版本中也不一样。3.4版本:

1
2
3
4
5
6
7
8
9
10
11
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×