Nota: El análisis del código fuente de esta serie se basa en mybatis-spring 2.0.6, la versión correspondiente de mybatis es 3.5.6 y la dirección del almacén del código fuente es: funcy/mybatis-spring .
En cuanto a mybatis
, la mayoría de las veces lo spring
usamos en combinación con el contenedor, este artículo analizará el proceso de integración de este mybatis
y el contenedor.spring
1. Obtener el código fuente y preparardemo
1.1 mybatis
y spring
proyecto de integración y código fuente
mybatis
El spring
proyecto integrado se llama mybatis-spring
y el código fuente está alojado en github. Por supuesto, también lo saqué a mi propio gitee para crearlo. La dirección es: funcy/mybatis . Sobre el código fuente checkout
y pull
la operación, no es el enfoque . de este artículo, así que no diré más.
mybatis
La versión analizada anteriormente es 3.5.6
, la versión que analizamos aquí es y la mybatis-spring
versión 2.0.6
de mybatis a la que se refiere es exactamente 3.5.6
.
1.2 preparación de demostración
Después de obtener el código fuente, puede crear una demostración de ejemplo. src/test
La crearemos a continuación. El directorio completo es el siguiente:
Directorio de códigos:
archivo asignador.xml:
- preparar
model
/mapper
/service
/**
* User.java
*/
public class User implements Serializable {
/**
* 用户id
*/
private Long id;
/**
* 登录名
*/
private String loginName;
/**
* 用户昵称
*/
private String nick;
... 省略set/get方法
}
/**
* UserMapper.java
*/
public interface UserMapper {
/**
* 查询
*
* @return
*/
List<User> selectList(@Param("id") Long id, @Param("limit") Integer limit);
/**
* 更新
*
* @param id
* @param nick
* @return
*/
int updateUser(@Param("id") Long id, @Param("nick") String nick);
}
/**
* UserService.java
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 获取列表
*
* @param id
* @param limit
* @return
*/
public List<User> getUser(Long id, Integer limit) {
return userMapper.selectList(id, limit);
}
}
复制代码
UserService.java
Manejado por resorte e inyectado por resorte userMapper
.
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.spring.demo.mapper.UserMapper">
<select id="selectList" resultType="org.mybatis.spring.demo.model.User">
select id, login_name as loginName, nick from user
<trim prefix="where" prefixOverrides="AND |OR">
<if test="id != null">
and id = #{id}
</if>
</trim>
<if test="limit != null">
limit #{limit}
</if>
</select>
<update id="updateUser">
update user set nick = #{nick} where id = #{id}
</update>
</mapper>
复制代码
- Preparar la clase de configuración
@Configuration
public class Demo01Config {
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 设置数据源
factoryBean.setDataSource(dataSource());
// mapper.xml 文件路径
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/**/*.xml");
factoryBean.setMapperLocations(resources);
return factoryBean.getObject();
}
@Bean
public DataSource dataSource() throws Exception {
Driver driver = new com.mysql.cj.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "123";
return new SimpleDriverDataSource(driver, url, username, password);
}
}
复制代码
- preparar la clase principal
Finalmente la clase principal:
@ComponentScan
@MapperScan("org.mybatis.spring.demo.mapper")
public class Demo01Main {
public static void main(String[] args) {
ApplicationContext application
= new AnnotationConfigApplicationContext(Demo01Main.class);
// 获取 userService 对象
UserService userService = (UserService) application.getBean("userService");
List<User> list = userService.getUser(3L, 10);
System.out.println(list);
}
}
复制代码
Ejecutar, los resultados son los siguientes:
[User{id=3, loginName='test', nick='test'}]
复制代码
Se puede ver que la integración de mybatis y spring es exitosa, y luego analizaremos el mybatis-spring
proyecto de acuerdo con esta demostración.
2. SqlSessionFactoryBean
Clase
我们先来看看Demo01Config
引入的Bean
:
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 设置数据源
factoryBean.setDataSource(dataSource());
// mapper.xml 文件路径
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/**/*.xml");
factoryBean.setMapperLocations(resources);
return factoryBean.getObject();
}
复制代码
这个方法中,先是创建了SqlSessionFactoryBean
,然后配置了数据源以及mapper.xml
文件路径,然后调用SqlSessionFactory#getObject()
方法获取SqlSessionFactory
,看来SqlSessionFactoryBean
还是挺关键的,我们看看它的定义:
/**
* 实现了 FactoryBean
*/
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>,
InitializingBean, ApplicationListener<ApplicationEvent> {
...
}
复制代码
可以看到,SqlSessionFactoryBean
实现了FactoryBean
,其中类为SqlSessionFactory
,这样调用SqlSessionFactory#getObject()
方法获取SqlSessionFactory
了。
FactoryBean
是spring中一类重要的bean
,它可以将普通的类包装成FactoryBean
注册到spring容器中,要获取该类的bean时,只需调用SqlSessionFactory#getObject()
方法即可。
SqlSessionFactory
是mybatis
中的重要类,用来生成SqlSession
,而SqlSession
正是用来获取XxxMapper
实例、执行数据库操作的类。
SqlSessionFactoryBean
可以定义很多很多的配置,它的setXxx
方法如下:
实际上,在mybatis-config.xml
配置文件中配置的内容都可以在这里找到对应的setXxx
方法,示例demo中,只是设置了数据源与mapper.xml
的文件路径。
了解了SqlSessionFactoryBean
的相关功能后,我们来看看SqlSessionFactoryBean#getObject
方法:
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
// 主动调用 afterPropertiesSet()
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
/**
* 重写了 InitializingBean 的方法
*/
public void afterPropertiesSet() throws Exception {
...
// 这里构建了 sqlSessionFactory
this.sqlSessionFactory = buildSqlSessionFactory();
}
复制代码
我们继续进入buildSqlSessionFactory()
方法:
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
// 这里省略了好多好多的代码,都是处理 targetConfiguration 的配置
...
// 处理数据源
targetConfiguration.setEnvironment(new Environment(this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory()
: this.transactionFactory, this.dataSource));
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "...");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
mapperLocation.getInputStream(), targetConfiguration,
mapperLocation.toString(), targetConfiguration.getSqlFragments());
// 在这里解析 mapper.xml 文件
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException(...);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
复制代码
首先,这个方法非常长,以上代码是精简后的代码,省略的是大段大段的设置targetConfiguration
属性的代码,剩下的是两个功能:
- 设置数据源
- 解析xml
这两个属性也是我们在sqlSessionFactory()
中设置的。
处理完sqlSessionFactory()
方法后,sqlSessionFactory
就注册到spring容器中了。
3. @MapperScan
扫描流程
sqlSessionFactory
注册到spring容器并不能实现MapperInterface
的自动注入,像这样:
@Autowired
private UserMapper userMapper;
复制代码
这个功能的实现,就是@MapperScan
的功劳了。
在Demo01Main.java
中,我们是这样配置@MapperScan
注解的:
@ComponentScan
@MapperScan("org.mybatis.spring.demo.mapper")
public class Demo01Main {
...
}
复制代码
这里的org.mybatis.spring.demo.mapper
就是MapperInterface
的包路径了,也是mybatis
要扫描的包路径.
我们进入@MapperScan
:
...
// @Import 里的类是关键
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...
}
复制代码
在MapperScan
中,有一个@Import
标签,它引入了一个类:MapperScannerRegistrar
,我们来看看它做了啥。
3.1 MapperScannerRegistrar
MapperScannerRegistrar
的代码如下:
/**
* 实现了 ImportBeanDefinitionRegistrar类
*/
public class MapperScannerRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
...
/**
* 来自于 ImportBeanDefinitionRegistrar 的方法,可以往spring容器中注册beanDefinition
*/
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 获取 MapperScan 的属性值
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
// 注册mapper接口类
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
...
}
复制代码
MapperScannerRegistrar
实现了ImportBeanDefinitionRegistrar
,在其registerBeanDefinitions
中向spring容器注入了某些beanDefinition
,我们继续进入registerBeanDefinitions(...)
方法,看看注入了啥:
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 创建 BeanDefinitionBuilder,构建 MapperScannerConfigurer
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
// 以下为解析 @MapperScan 的各种属性,并它他们设置到 builder 中
...
// 解析扫描的包路径
List<String> basePackages = new ArrayList<>();
basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("value"))
.filter(StringUtils::hasText).collect(Collectors.toList()));
...
// 添加属性值
builder.addPropertyValue("basePackage",
StringUtils.collectionToCommaDelimitedString(basePackages));
// 注册 MapperScannerConfigurer 为 beanDefinition
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
复制代码
registerBeanDefinitions(...)
方法先是为MapperScannerConfigurer
创建了一个BeanDefinitionBuilder
,然后就是解析@MapperScan
的各种属性,并添加到BeanDefinitionBuilder
中,然后调用registry.registerBeanDefinition
容器中。上述方法是精简后的代码,由于@MapperScan
支持的配置比较多,原本的代码非常长,精简后的代码中仅留下了我们设置的value
值,它就是这样设置到BeanDefinitionBuilder
中的。
因此,整个MapperScannerRegistrar
就只是向spring容器中注册了一个类:MapperScannerConfigurer
。
3.2 MapperScannerConfigurer
接下来我们就把目光指向了MapperScannerConfigurer
:
/**
* 它实现了 BeanDefinitionRegistryPostProcessor
*/
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor,
InitializingBean, ApplicationContextAware, BeanNameAware {
/**
* 来自于 BeanDefinitionRegistryPostProcessor 的方法
* 确切地说,是来自于 BeanFactoryPostProcessor 的方法,因为 BeanFactoryPostProcessor 是
* BeanDefinitionRegistryPostProcessor 的父类
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 方法实现为空,没有任何内容,所以spring为何不在接口方法中加个default关键字呢
}
/**
* 来自于 BeanDefinitionRegistryPostProcessor 的方法
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
// 定义扫描类
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 设置各种属性,一些是由 @MapperScan 中解析得到的
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
// 注册过滤配置
scanner.registerFilters();
// 在这里处理扫描操作
scanner.scan(
// 这里就是前面解析的 basePackage
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
}
复制代码
MapperScannerConfigurer
实现了BeanDefinitionRegistryPostProcessor
,这是spring提供的BeanFactoryPostProcessor
之一,可以在spring执行的早期向容器中注册beanDefinition
,它提供了两个方法:postProcessBeanFactory(...)
(定制化beanFactory
的某些行为)与postProcessBeanDefinitionRegistry(...)
(处理beanDefinition
注册),由于postProcessBeanFactory(...)
方法为空实现,因此我们重点关注postProcessBeanDefinitionRegistry(...)
方法。
在postProcessBeanDefinitionRegistry(...)
方法中,先是创建了一个ClassPathMapperScanner
的实例,该实例主要功能就是用来处理MapperInterface
的扫描功能的,之后再对ClassPathMapperScanner
实例进行一些属性设置,这些属性大部分来自于@@MapperScan
的设置,最后调用ClassPathMapperScanner#scan(...)
方法也进行扫描操作,在这个方法里,满足条件的MapperInterface
会被转化为 BeanDefinition
从而注册到spring容器中的。
接下来我们就重点关注ClassPathMapperScanner
。
3.3 ClassPathMapperScanner
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
...
}
复制代码
可以看到,ClassPathMapperScanner
继承了ClassPathBeanDefinitionScanner
,关于ClassPathBeanDefinitionScanner
,熟悉spring的小伙伴并不陌生,它就是spring中用来处理@ComponentScan
注解扫描的类,也就是说,处理@MapperScan
的扫描底层使用的也是与@ComponentScan
一样的方式。
1. ClassPathMapperScanner#registerFilters(...)
我们知道,spring的@ComponentScan
只扫描包下标记了@Component
注解的类,而MapperInterface
只是个接口,它为何也能扫描呢?这就是ClassPathMapperScanner#registerFilters(...)
的功能了,在MapperScannerConfigurer#postProcessBeanDefinitionRegistry
中我们也看到了它的调用:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 省略一些配置
...
// 注册过滤配置
scanner.registerFilters();
// 在这里处理扫描操作
scanner.scan(
// 这里就是前面解析的 basePackage
StringUtils.tokenizeToStringArray(this.basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
复制代码
我们来看看ClassPathMapperScanner
的注册过滤配置的方法registerFilters
:
public void registerFilters() {
boolean acceptAllInterfaces = true;
// 省略一些配置
...
// 表示包下的接口也扫描
if (acceptAllInterfaces) {
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
// 过滤 package-info.java 文件
addExcludeFilter((metadataReader, metadataReaderFactory) -> {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
});
}
复制代码
这个方法里注册了一些过滤配置,这里我们只关注acceptAllInterfaces
的处理,相关代码为:
if (acceptAllInterfaces) {
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
复制代码
这表明包下的一切类、接口等都会被扫描为BeanDefinition
。
2.ClassPathMapperScanner#doScan
准备工作做完后,我们来看看具体的扫描流程,也就是scanner.scan(...)
方法,ClassPathMapperScanner
并没有重写该方法,调用的 是spring 提供的 ClassPathBeanDefinitionScanner#scan
方法中:
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
// 这是真正干活的方法
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
复制代码
doScan(...)
方法ClassPathMapperScanner
是有重写的,我们进入ClassPathMapperScanner#doScan
方法:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 调用父类的方法进行扫描
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
// 处理扫描得到的 beanDefinitions
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
复制代码
doScan(...)
方法继续调用父类的doScan(...)
方法进行扫描,然后再调用processBeanDefinitions(...)
处理扫描结果,调用super.doScan(basePackages)
得到的结果如下:
org.mybatis.spring.demo.mapper
包下的UserMapper
已经被扫描为beanDefinition
了,它的beanClass
是UserMapper
,这是个接口,它该如何进行实例化呢?这就是processBeanDefinitions(...)
的功能,我们继续看下去。
3. ClassPathMapperScanner#processBeanDefinitions
ClassPathMapperScanner#processBeanDefinitions
方法的代码如下:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
// 遍历得到的 beanDefinitions
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
...
// 设置构造方法参数
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
// 获取 beanClassName
String beanClassName = definition.getBeanClassName();
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
// 修改 beanClass 为 MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBeanClass);
// 设置 MapperFactoryBean 的 factoryBeanObjectType 为 beanClassName
definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);
boolean explicitFactoryUsed = false;
// 设置 sqlSessionFactory,这里的 sqlSessionFactoryBeanName 与 sqlSessionFactory 都是
// 由 @MapperScan 中配置的,如果未配置,这两个属性的值就为 null
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
// 设置 sqlSessionFactory 属性,设置时会调用 setSqlSessionFactory(...) 方法
definition.getPropertyValues().add("sqlSessionFactory",
// RuntimeBeanReference 会把传入的字符串解析为 bean
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
// 如果传入的是 sqlSessionFactory,则直接设置值
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
// 设置 sqlSessionTemplate,操作同设置sqlSessionFactory类似
// 同样地,这里的 sqlSessionTemplateBeanName 与 sqlSessionTemplate 都是
// 由 @MapperScan 中配置的,如果未配置,这两个属性的值就为 null
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
// 设置 sqlSessionTemplate,这是个beanName,RuntimeBeanReference会beanName解析成对应的bean
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
// 设置 sqlSessionTemplate,直接设置 sqlSessionTemplate 的值
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
// 如果 sqlSessionFactory与sqlSessionTemplate都未设置,就使用byType的自动注入方式
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
// 省略了其他代码
...
}
}
复制代码
这个方法还是有点长的,经过精简后,代码还是比较清晰的,这里先来看看它是如何替换beanDefinition
的beanClass的:
- 调用
definition.getBeanClassName()
获取原本的beanClassName
,这里得到的beanClassName
是org.mybatis.spring.demo.mapper.UserMapper
- 设置构造方法参数为
beanClassName
,也就是org.mybatis.spring.demo.mapper.UserMapper
- 将
definition
的beanClass
设置为org.mybatis.spring.mapper.MapperFactoryBean
经过这三步之后,definition
的beanClass
就是MapperFactoryBean
了,就可以放心大胆地创建MapperFactoryBean
实例了:
我们要的是org.mybatis.spring.demo.mapper.UserMapper
,但spring给我们创建的却是org.mybatis.spring.mapper.MapperFactoryBean
,这两者之间有何关系呢?我们进入MapperFactoryBean
一探究竟:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
...
}
复制代码
从代码来看,它继承了FactoryBean
,类型为mapperInterface
,也就是我们处理构造方法传入的类型:
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
复制代码
这样一来,相当于MapperFactoryBean
的mapperInterface
值为org.mybatis.spring.demo.mapper.UserMapper
.
除了构造方法外,ClassPathMapperScanner#processBeanDefinitions
方法还为MapperFactoryBean
设置了其他的一些值,这里我们主要关注sqlSessionFactory
与sqlSessionTemplate
的设置。
先看sqlSessionFactory
的设置,如果@MapperScan
中配置了sqlSessionFactoryBeanName
,就会将其设置到MapperFactoryBean
到sqlSessionFactory
属性中。如果sqlSessionFactoryBeanName
未配置,就看@MapperScan
有没有配置sqlSessionFactory
,如果配置了,就直接设置。
处理sqlSessionFactoryBeanName
设置操作时,有两点需要注意:
- 传入的
sqlSessionFactoryBeanName
是字符串,如果转换为spring bean
呢?这就是RuntimeBeanReference
的功能了,它会字符串解析为具体的spring bean
; MapperFactoryBean
并没有sqlSessionFactory
的属性,不仅如此,就边它的父类也没有,它该如何设置呢?实际上,在spring中设置属性值,并不需要属性存在,只需要属性对应的setXxx(...)
方法存在就可以了,而MapperFactoryBean
的父类SqlSessionDaoSupport
正好有setSqlSessionFactory(...)
方法,因此设置sqlSessionFactory
属性时,调用的是setSqlSessionFactory(...)
方法。
关于sqlSessionTemplate
的设置同sqlSessionFactory
非常类似,这里就不再分析了。
在我们的demo中,@MapperScan
并没有配置sqlSessionFactoryBeanName
/sqlSessionFactory
/sqlSessionTemplateName
/sqlSessionTemplate
,MapperFactoryBean
的sqlSessionFactory
是从何而来的呢?
注意看processBeanDefinitions
方法,如果sqlSessionFactory
与sqlSessionTemplate
,explicitFactoryUsed
的值就为false
,接下来会有一个关键操作:
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
复制代码
这个设置表明,该beanDefinition
的注入模式为byType
,也就是按类型注入,spring在处理注入时,会搜索MapperFactoryBean
的setXxx()
,为其设置值。
在我们的示例demo中,MapperFactoryBean
的setSqlSessionFactory(...)
方法如下:
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory
!= this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
复制代码
spring 会先获取到setSqlSessionFactory(...)
方法,然后获知其参数类型为SqlSessionFactory
,接着就从spring容器中找到SqlSessionFactory
类型的bean,这个bean我们在Demo01Config
已经创建了:
@Configuration
public class Demo01Config {
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
...
}
...
}
复制代码
找到SqlSessionFactory
类型的bean后,就以此bean为参数,调用setSqlSessionFactory(...)
方法。至于sqlSessionFactory
如何设置到MapperFactoryBean
的具体操作,那就是setSqlSessionFactory(...)
的操作了。
一句话,spring 提供sqlSessionFactory
参数,setSqlSessionFactory(...)
负责具体的设置操作。
4. 获取注入的XxxMapper
:MapperFactoryBean#getObject
MapperInterface
注入到spring容器的bean是MapperFactoryBean
,在自动注入时,该如何拿到对应的MapperInterface
bean 呢?实际上,spring 在处理FactoryBean
的自动注入时,会调用FactoryBean#getObject
获取FactoryBean
实际表示的bean,我们进入MapperFactoryBean
的getObject()
方法:
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
复制代码
这个方法分为两部分:getSession()
与getMapper()
,我们先来看getSession()
方法,它位于SqlSessionDaoSupport
内:
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
复制代码
这个方法只是简单地返回了sqlSessionTemplate
。在前面我们并没有设置sqlSessionTemplate
,这个sqlSessionTemplate
是从何而来呢?通过调试,我们发现它是从setSqlSessionFactory(...)
中来的:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
/**
* 设置 SqlSessionFactory
*/
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null
|| sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
/**
* 创建 SqlSessionTemplate
*/
protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
...
}
复制代码
在调用setSqlSessionFactory(...)
方法时,会创建sqlSessionTemplate
。我们再进入SqlSessionTemplate
方法,看看它的处理逻辑:
public class SqlSessionTemplate implements SqlSession, DisposableBean {
/** 传入的的 sqlSessionFactory */
private final SqlSessionFactory sqlSessionFactory;
/** 这就是 sqlSessionProxy */
private final SqlSession sqlSessionProxy;
/**
* SqlSessionTemplate 的构造方法
*/
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
// 传入的 sqlSessionFactory
this.sqlSessionFactory = sqlSessionFactory;
// 又是一个动态代理对象,代理的是 SqlSession 对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
...
}
...
}
复制代码
SqlSessionTemplate
中在两个重要的成员变量:sqlSessionFactory
与sqlSessionProxy
,这个sqlSessionFactory
就是在SqlSessionDaoSupport#setSqlSessionFactory(...)
中传入的sqlSessionFactory
,而sqlSessionProxy
是SqlSession
的代理对象,它的InvocationHandler
实例为SqlSessionInterceptor
,关于它的妙用我们后面再分析。
回到MapperFactoryBean#getObject()
方法,看完了getSession()
后,我们再来看看getMapper
的逻辑:
public class SqlSessionTemplate implements SqlSession, DisposableBean {
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
...
}
复制代码
这个方法比较简单,就是先获取Configuration
,然后从Configuration
中获取UserMapper
的实例,这里的type
为UserMapper.class
,this
为SqlSessionTemplate
的实例。继续跟进去,到达MapperRegistry#getMapper
方法:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
// 创建对就的 type 实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
复制代码
可以看到,XxxMapper
实例是在调用MapperRegistry#getMapper
方法时创建的,关于创建流程及mapperProxyFactory
,这些是mybatis
相关的内容,本文主要关注与spring的整合,这些就不分析了。
经过了以上步骤,MapperFactoryBean#getObject
就能成功获取UserMapper
实例了,UserService
中的UserMapper
也能注入成功了。
4. 执行数据库操作
接下来我们来看看数据库的执行操作,也就是userMapper.selectList(...)
方法。
关于mybatis
中XxxMapper
的执行流程,在mybatis源码分析02:执行sql语句一文已经详细分析过了,本文不再赘述,本节我们主要关注SqlSessionTemplate
的相关操作。
在前面的分析文章中,我们提到mybatis
提供的sqlSession
默认实现DefaultSqlSession
不是线程安全的,在前面得到的UserMapper
中,使用的是SqlSessionTemplate
,它是线程安全的吗?我们通过调试来进行分析。
在userMapper.selectList(...)
的调用过程中,经过一系列的调用栈后,最终会来到MapperMethod#executeForMany
方法:
在MapperMethod#executeForMany
方法中,又会调用sqlSession.selectList(...)
方法来获取数据,而这个sqlSession
就是SqlSessionTemplate
:
再进入SqlSessionTemplate#selectList(...)
方法:
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.selectList(statement, parameter);
}
复制代码
它是调用sqlSessionProxy
对象的selectList
方法进行查询的。对于sqlSessionProxy
,上一节在分析SqlSessionTemplate
的构造方法时我们提到,它是在SqlSessionTemplate
中创建的:
// 又是一个动态代理对象,代理的是 SqlSession 对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
复制代码
没错,它是个动态代理对象,它的InvocationHandler
实例为SqlSessionInterceptor
,执行sqlSessionProxy.selectList(...)
时,SqlSessionInterceptor#invoke(...)
会先被执行,我们进入SqlSessionInterceptor#invoke(...)
方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取 sqlSession,获取到的类型是 DefaultSqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
...
} finally {
...
}
}
复制代码
这个方法里,主要是处理sqlSession
的获取操作,得到sqlSession
之后会再调用原本方法。这个获取到的sqlSession
是不是线程安全的呢?从运行结果来看,它的实际类型依旧是DefaultSqlSession
,并不是线程安全的。那这个操作依然不是线程安全的吗?不一定,衡量DefaultSqlSession
是否线程安全,并不一定它本身是否线程安全,如果每个线程都有自己的DefaultSqlSession
,DefaultSqlSession
不会被跨线程共享,那么即使DefaultSqlSession
非线程安全,但整个操作还是线程安全的。
每个线程都有自己的DefaultSqlSession
的办法或许你已经想到了,就是ThreadLocal
!mbyatis
提供的SqlSessionManager
就是这样实现的,而SqlSessionTemplate
由于是与spring结合,这块功能它托管给了spring来做,我们且看sqlSession
的获取:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory,
ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
...
// 获取 SqlSessionHolder,托管给了 spring
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager
.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 新建一个 sqlSession
session = sessionFactory.openSession(executorType);
// 注册SessionHolder
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
复制代码
从代码来看,SqlSession
是由TransactionSynchronizationManager.getResource(...)
获取的,获取不到时才会新建。获取到的SqlSession
是所有线程共享的,还是一个线程独占的呢?关于TransactionSynchronizationManager.getResource(...)
,本文并不打算分析,我们直接看它的方法说明:
从这个方法的说明来看,它就是用来返回与当前线程的绑定的资源的,这也就是说,通过TransactionSynchronizationManager.getResource(...)
得到的SqlSession
就是线程独占的,因此不存在线程安全问题。
另外,据本人验证,只有在开启事务时,SqlSession
才会被保存,如果未开启事务,TransactionSynchronizationManager.getResource(...)
得到的SqlSession
一直为null
,每次调用getSqlSession(...)
时都会创建一个新的SqlSession
.
到了这里,我们就明白了,在执行MapperInterface
的方法时,获取到的SqlSession
由spring容器托管,并且线程独占。
5. 总结
Este artículo analiza principalmente el proceso de integración de mybatis y spring:
SqlSessionFactory
La configuración de propiedades de estáSqlSessionFactoryBean
a cargo de esta clase, que proporciona muchossetXxx(...)
métodos para personalizarmybatis
una serie de configuraciones;@MapperScan
La ruta de escaneo que se puede especificarMapperInterface
, estas rutas de paquetes seMapperScannerConfigurer
escanearán en la clase y se registrarán en el contenedor de primavera;- La inicial obtenida al escanear la ruta del paquete es
beanDefinition
labeanClass
interfaz, quemybatis
se convertiráClassPathMapperScanner#processBeanDefinitions
en , es decir , el tipo real es ;beanDefinition
beanClass
MapperFactoryBean
FactoryBean
MapperInterface
- Cuando se obtiene el bean a inyectar , se obtiene el bean correspondiente
MapperInterface
a través del método , y el bean obtenido es un objeto proxy dinámico, entre los cuales se encuentra ;MapperFactoryBean#getObject(...)
MapperInterface
sqlSession
SqlSessionTemplate
SqlSessionTemplate
Existe una variable miembro:sqlSessionProxy
, cuyo tipo esSqlSession
, cuando se ejecuta el métodoMapperInterface
, eventualmente se usará el método de operación de la base de datossqlSessionProxy
llamado ;SqlSession
- Dado que
sqlSessionProxy
es un objeto de proxy dinámico, cuando se ejecuta un método, pasará a través delInterceptor#invoke(...)
método y el método obtendrá el enlace al subproceso actualDefaultSqlSession
, que también se utiliza en la ejecución realDefaultSqlSession
, evitando así problemas de seguridad del subproceso.
El enlace al texto original de este artículo: my.oschina.net/funcy/blog/… , limitado al nivel personal del autor, inevitablemente hay errores en el texto, ¡bienvenidos a corregirme! La originalidad no es fácil. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización. Para reimpresiones no comerciales, indique la fuente.