Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析

前言

本文将分析 mybatis 与 spring 整合的 MapperScannerConfigurer 的底层原理,之前已经分析过 java 中实现动态,可以使用 jdk 自带 api 和 cglib 第三方库生成动态代理。本文分析的 mybatis 版本 3.2.7,mybatis-spring 版本 1.2.2。

MapperScannerConfigurer 介绍

MapperScannerConfigurer是 spring 和 mybatis 整合的 mybatis-spring jar 包中提供的一个类。

想要了解该类的作用,就得先了解MapperFactoryBean

MapperFactoryBean 的出现为了代替手工使用 SqlSessionDaoSupport 或 SqlSessionTemplate 编写数据访问对象 (DAO) 的代码, 使用动态代理实现。

比如下面这个官方文档中的配置:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

org.mybatis.spring.sample.mapper.UserMapper 是一个接口,我们创建一个 MapperFactoryBean 实例,然后注入这个接口和 sqlSessionFactory(mybatis 中提供的 SqlSessionFactory 接口,MapperFactoryBean 会使用 SqlSessionFactory 创建 SqlSession)这两个属性。

之后想使用这个 UserMapper 接口的话,直接通过 spring 注入这个 bean,然后就可以直接使用了,spring 内部会创建一个这个接口的动态代理。

当发现要使用多个 MapperFactoryBean 的时候,一个一个定义肯定非常麻烦,于是 mybatis-spring 提供了 MapperScannerConfigurer 这个类,它将会查找类路径下的映射器并自动将它们创建成 MapperFactoryBean。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
</bean>

这段配置会扫描 org.mybatis.spring.sample.mapper 下的所有接口,然后创建各自接口的动态代理类。

MapperScannerConfigurer 底层代码分析

以以下代码为示例进行讲解 (部分代码,其他代码及配置省略):

package org.format.dynamicproxy.mybatis.dao;
public interface UserDao {
    public User getById(int id);
    public int add(User user);    
    public int update(User user);    
    public int delete(User user);    
    public List<User> getAll();    
}

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.format.dynamicproxy.mybatis.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

我们先通过测试用例 debug 查看 userDao 的实现类到底是什么。

我们可以看到,userDao 是 1 个 MapperProxy 类的实例。
看下 MapperProxy 的源码,没错,实现了 InvocationHandler,说明使用了 jdk 自带的动态代理。

public class MapperProxy<T> implements InvocationHandler, Serializable {

private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

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);
}

private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}

}

下面开始分析 MapperScannerConfigurer 的源码

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,BeanDefinitionRegistryPostProcessor 接口是一个可以修改 spring 工长中已定义的 bean 的接口,该接口有个 postProcessBeanDefinitionRegistry 方法。

然后我们看下 ClassPathMapperScanner 中的关键是如何扫描对应 package 下的接口的。

其实 MapperScannerConfigurer 的作用也就是将对应的接口的类型改造为 MapperFactoryBean,而这个 MapperFactoryBean 的属性 mapperInterface 是原类型。MapperFactoryBean 本文开头已分析过。

所以最终我们还是要分析 MapperFactoryBean 的实现原理!

MapperFactoryBean 继承了 SqlSessionDaoSupport 类,SqlSessionDaoSupport 类继承 DaoSupport 抽象类,DaoSupport 抽象类实现了 InitializingBean 接口,因此实例个 MapperFactoryBean 的时候,都会调用 InitializingBean 接口的 afterPropertiesSet 方法。

DaoSupport 的 afterPropertiesSet 方法:

MapperFactoryBean 重写了 checkDaoConfig 方法:

然后通过 spring 工厂拿对应的 bean 的时候:

这里的 SqlSession 是 SqlSessionTemplate,SqlSessionTemplate 的 getMapper 方法:

Configuration 的 getMapper 方法,会使用 MapperRegistry 的 getMapper 方法:

MapperRegistry 的 getMapper 方法:

MapperProxyFactory 构造 MapperProxy:

没错! MapperProxyFactory 就是使用了 jdk 组带的 Proxy 完成动态代理。
MapperProxy 本来一开始已经提到。MapperProxy 内部使用了 MapperMethod 类完成方法的调用:

下面,我们以 UserDao 的 getById 方法来 debug 看看 MapperMethod 的 execute 方法是如何走的。

@Test
public void testGet() {
    int id = 1;
    System.out.println(userDao.getById(id));
}
<select id="getById" parameterType="int" resultType="org.format.dynamicproxy.mybatis.bean.User">
    SELECT * FROM users WHERE id = #{id}
</select>


示例代码:https://github.com/fangjian0423/dynamic-proxy-mybatis-study

总结

来到了新公司,接触了 Mybatis,以前接触过~ 但是接触的不深入,突然发现 spring 与 mybatis 整合之后可以只写个接口而不实现,spring 默认会帮我们实现,然后觉得非常神奇,于是写了篇java 动态代码浅析和本文。

参考资料

https://mybatis.github.io/spring/zh/mappers.html