文章导航
mybatis和spring的整合
1.mybatis工作流程回顾
首先回顾一下mybatis的启动过程(详情可以参考上一篇文章):
String resource = "resources/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 读取配置文件获取sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取session
SqlSession session = sqlSessionFactory.openSession();
// 获取mapper代理类
FsiptUserMapper mapper = session.getMapper(FsiptUserMapper.class);
// 查询(执行代理类invoke方法)
FsiptUser fsiptUser = mapper.selectUser("S67190");
System.out.println(fsiptUser.toString());
大致流程还是比较清晰的:
获取到代理对象mapper之后,就可以利用mapper进行crud等操作了。所以mybatis整合spring的过程,一句话来说就是:把代理对象mapper作为一个bean放入spring容器,然后像使用普通的bean一样使用这个代理对象。
2.简单回顾spring中bean的实例化过程
“标准化”过程
简单回顾下spring中bean的实例化的“标准化”过程:
“定制化”过程
除了“标准化”过程之外,spring还提供了一种生成bean的方式,利用spring中的FactoryBean。简单来说,Spring提供了一个org.springframework.beans.factory.FactoryBean的工厂类接口,我们可以通过实现该接口定制实例化Bean的逻辑。如果一个Bean实现了FactoryBean接口的Bean,那么和普通Bean不同的是,实例化的时候从BeanFactory中获取的实际上是FactoryBean的getObject()方法返回的对象,而非FactoryBean对象。
3.整合原理概述(两个标签完成整合)
(这篇文章写的很好)
回到主题,我们的目标是在spring中生成一个bean(代理对象mapper),如果按照正常spring生成bean的流程,生成一个mapper对象就3步:
- 生成一个BeanDefinition(可以理解成
BeanDefinitoin bd = new BeanDefinitoin()
); - 加载类(可以理解成
bd.setBeanClassName(MapperProxy.class.getName())
); - 放入spring容器(可以理解成
SpringContainer.addBd(bd)
)
如果我们可以通过某种方式获取这个bean对象(mapper)的BeanDefinition(后面统一简写为bd),将bean对象的类信息赋值给bd,然后把bd交给spring。那么spring就可以根据这个bd自动生成bean对象。
听起来似乎很简单,不过有个很重要的问题:mapper是个jdk动态代理生成的代理对象,我们只有它对应的接口没有它对应的类,并不清楚它对应的类信息。所以这种“标准化”流程行不通。实际上mybatis走的是另一条路,即上文的“定制化”流程。下面详细介绍。
完成整合工作的是mybatis-spring这个jar包:
spring项目中整合mybatis相关的xml:
以及
其实整合只需要配置这两个bean:SqlSessionFactoryBean和MapperScannerConfigurer,可见这两个bean的作用至关重要。本文主要介绍利用xml文件进行整合的过程,也可以利用注解(@MapperScan、@MapperScannerRegistrar),二者仅有形式上的区别。
这里建议首先理解mybatis的启动过程,这对于理解mybatis和spring的整合事半功倍。
4.SqlSessionFactoryBean标签的作用
很粗暴,直接点进SqlSessionFactoryBean看看,发现它实现了三个接口:
其中ApplicationListener接口和整合没关系,只看InitializingBean和FactoryBean接口,这俩接口规定SqlSessionFactoryBean要实现afterPropertiesSet()和getObject()方法。
- afterPropertiesSet()方法
闻香识女…不好意思,是看名字猜这个方法的功能,此方法应该是在“设置完属性后”执行的。事实上正是如此,spring在创建bean的过程中首先调用的就是这个方法:
执行了buildSqlSessionFactory()方法:
如果了解过mybatis的启动过程,就会对这个方法里的很多似曾相识。XMLConfigBuilder:用于解析mybatis配置;XMLMapperBuilder:用于解析sql源;configuration:各种配置解析完之后封装成的对象。最后的build方法:this.sqlSessionFactoryBuilder.build(configuration)
更眼熟,都是得到了一个SqlSessionFactory,点进去发现生成的是一个DefaultSqlSessionFactory,和mybatis的流程一样样的:
这里可以对比下mybatis获取SqlSessionFactory的相关代码:
当然了,在mybatis中这个SqlSessionFactory接下来还要获取一个SqlSession,这个SqlSession再执行getMapper()方法才获取到mapper代理对象的。这部分内容在整合的过程中怎么实现的,我们下面再讲。 - getObject()方法
在上面概述原理的时候我们说过,实现了FactoryBean接口的bean在获取实例的时候不是直接new的,而是调用其getObject()方法获取的。这个getObject()方法干了点啥呢,很简单,返回刚刚生成的SqlSessionFactory:
到这里第一个xml标签的作用已经很清楚了,就一件事,生成一个SqlSessionFactoryBean(或者说SqlSessionFactory的一个实例)。
5.MapperScannerConfigurer标签的作用
MapperScannerConfigurer继承了org.mybatis.spring.mapper.MapperScannerConfigurer,同时覆写了postProcessBeanDefinitionRegistry()方法:
所以,要理解MapperScannerConfigurer这个bean干了些什么,重点就要看这个postProcessBeanDefinitionRegistry方法。进入这个方法,看到它首先调用了父类的同名方法:super.postProcessBeanDefinitionRegistry(registry);
进入父类的方法,看到了定义扫描器,以及进行扫描的过程:
spring中在bean的实例化过程中就有扫描指定package下所有类得到BeanDefinition这一步,不难猜测,这里应该是在扫描mybatis中需要用的接口,究竟如何继续debug。
-
ClassPathMapperScanner扫描器
看一下这个ClassPathMapperScanner的结构:
它的父类ClassPathBeanDefinitionScanner是spring的扫描器,说明它的扫描过程和spring的扫描过程是类似的。我们进入ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry)
:
这里有一个点,在spiring默认配置中useDefaultFilter是true,可理解为启用默认扫描器。默认扫描器时会扫描@Component,@Repository,@Service,@Controller四种注解:
而mybatis的扫描器不启用默认扫描器。 -
扫描并获取BeanDefinition
生成扫描器之后就开始扫描了:
一路debug进入doScan()方法:
这里看到先调用了父类的doScan
方法,说明和spring扫描在流程上是一样的。然后扫描得到BeanDefinition后再执行processBeanDefinitions(beanDefinitions)
方法对BeanDefinition进行一些处理。现在来简单看一下扫描流程:
详细的扫描流程这里就不介绍了,上面红框中的findCandidateComponents(basePackage)
方法看名字也知道,是在挑选符合条件的“候选者”,mybatis怎么挑选呢?进到这个方法中:
isCandidateComponent
方法中可以看到,mybatis会把接口作为“候选者”:
因为我们关注的是整合过程,那到这里基本上就走完了扫描流程,mybatis会挑选出我们定义的各种Mapper接口,把这些接口的相关信息封装成BeanDefinition(从刚才的截图能看到具体的类是ScannedGenericBeanDefinition)。 -
处理扫描得到的BeanDefinition
处理BeanDefinition也是mybatis和spring整合的重头戏,也就是processBeanDefinitions(beanDefinitions)
方法:
进入这个方法,我们要重点关注这里:
可以看到,这个bd的类型原本还是刚刚扫描得到的接口类,但是随后被改成了MapperFactoryBean
类型。这个MapperFactoryBean
前文提到过,认真的同学已经似曾相识,而不认真的同学一脸懵b。这里我们先放一放,接着往下看:
在改了bd的Class之后,进一步还会对bd进行一次属性的设置,这个属性的类型就是我们配置文件中配置的那个SqlSessionFactoryBean。而我们知道一个bean实例化后会进行依赖注入,因此这里的这个SqlSessionFactoryBean的实例(也就是上图中的sqlSessionFactoryFsiptDB)将来会作为依赖被注入进来。注意到这里的属性传的是new RuntimeBeanReference(this.sqlSessionFactoryBeanName)
,什么意思呢?这其实是对sqlSessionFactoryFsiptDB的一个动态引用,因为此时sqlSessionFactoryFsiptDB很可能还没准备好。而在真正进行依赖注入时,Spring会将该引用替换成实际生成的Bean对象。这个道理就和xml配置中的"ref"一样:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactory" ref="sqlSessionFactoryFsiptDB" />
</bean>
到这里第二个xml标签的作用也讲完了,做了两件事:扫描得到bd后,改beanClass和设置SqlSessionFactoryBean作为它的一个属性。
至此我宣布,mybatis和spring的整合已经完成了!不过对比mybatis的启动过程,好像还没看到通过jdk动态代理获取mapper代理对象这个过程。如果你有这个疑问,请接着往下看。
6.mybatis如何获取mapper代理对象
在所有的整合配置完成的情况下,获取mapper代理对象其实也很简单了。在这之前,首先简单回顾下mybatis获取mapper代理对象的样子:
所以,mapper代理对象是利用这个session的getMapper
方法获取的,而session是从SqlSessionFactory中获取的。
接着来看和spring整合后的mybatis怎么操作。一般我们在项目里都是这么用的:
@Service("FsiptDeployServiceImpl")
public class FsiptDeployServiceImpl implements IFsiptDeployService {
@Autowired
private InternetFsiptUserMapper internetFsiptUserMapper;
public void crud(){
...
internetFsiptUserMapper.selectAll();
...
}
}
internetFsiptUserMapper在作为依赖注入进入FsiptDeployServiceImpl时会被实例化,在实例化的时候会根据InternetFsiptUserMapper的名字(也就是“internetFsiptUserMapper”,首字母小写)去获取其bd(这是spring的知识),然后发现它的bd中类信息是MapperFactoryBean类型,看一下这个MapperFactoryBean的类图:
发现它的是SqlSessionDaoSupport的一个子类,同时实现了FactoryBean接口,而我们前面提到过,实现了FactoryBean接口的bean在实例化的时候,是通过其getObject()方法获取实例的:
有点眼熟,这就是mybatis中通过sqlSession获取mapper代理对象的步骤。继续进入getSqlSession()
发现来到了父类SqlSessionDaoSupport中,就是返回了一个sqlSession:
那么最后一个问题来了,这个sqlSession好像没看到什么时候设置的。其实这涉及到spring的知识点:
在spring中,SqlSessionDaoSupport作为MapperFactoryBean的父类,在MapperFactoryBean进行依赖注入的时候,除本身的属性之外,其父类SqlSessionDaoSupport的属性也会被注入进来。Spring中依赖注入的方式有多种,其中一种就是通过set方法注入依赖。在父类进行依赖注入时,通过set方法将SqlSessionFactory的实例注入进来的,而这里注入的这个SqlSessionFactory的实例是哪个呢?在扫描的过程中已经设置好了,正式之前提到过的:
至于sqlSessionFactoryFsiptDB的生成过程,就是我们讲的第一个xml标签:
7.关于使用纯注解的整合
使用纯注解这里不详细说了,但是原理没有区别,只是具体操作方式有区别,无非是标签换成注解而已:
补充:在mybatis-spring 2.0.5版本后,可以不再使用@MapperScan,转而定义一个普通的bean: