案例
案例一: MyBatis单独使用
在 resources 目录下新建 mybatis-config.xml 配置文件,文件内容如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--配置日志-->
<settings>
<setting name="logImpl" value="LOG4J2"/>
</settings>
<!--配置数据源和事务管理器-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/study"/>
<property name="username" value="root"/>
<property name="password" value="xxxxxx"/>
</dataSource>
</environment>
</environments>
<!--配置 mapper 文件的位置-->
<mappers>
<mapper resource="com/study/mybatis/UserMapper.xml"/>
</mappers>
</configuration>
在 resources
下面的 com/study/mybatis
文件夹中新建 UserMapper.xml
文件,文件内容如下:
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="com.study.mybatis.mapper.UserMapper">
<select id="getUserById" resultType="com.study.mybatis.entity.User">
select * from User where id = #{id}
</select>
</mapper>
同时在对应层级的 package 下新建 UserMapper
接口,该接口中包含一个 getUserById()
方法和 UserMapper.xml
文件中配置的相对应。代码如下:
java
@Mapper
public interface UserMapper {
User getUserById(@Param("id") Long id);
}
然后在 Java 代码中,可以通过现构造 SqlSessionFactory
对象,从这个对象中获取一个 SqlSession
对象,然后再通过它获取到 UserMapper
接口的动态代理对象,然后通过这个动态代理对象来调用对应的方法。代码如下:
java
public static void main(String[] args) throws IOException {
// 读取配置文件生成 SqlSessionFactory 对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取 SqlSession 对象,然后通过该对象获取 Mapper 接口的动态代理对象
// 然后根据操作该动态代理对象
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.getUserById(1L);
System.out.println(JSON.toJSONString(user));
}
}
案例二: MyBatis和Spring一起使用
java
@Configuration
@MapperScan("com.study.mapper")
public class MyBatisConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study");
dataSource.setUsername("root");
dataSource.setPassword("xxxxxx");
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
return sessionFactoryBean.getObject();
}
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/{id}")
@ResponseBody
public User getUserById(@PathVariable Long id) {
return userMapper.getUserById(id);
}
}

从上面的两个案例可以看到,当 MyBatis 单独使用的时候,需要手动通过 SqlSession
对象来获取 Mapper
接口的动态代理对象;当 MyBatis 和 Spring 单独使用的时候,可以把 Mapper
对象当作一个普通的 Bean 对象,通过 @Autowired
注解注入到需要使用的类里面就可以了。
那当 MyBatis 和 Spring 一起使用的时候,是如何做到把 Mapper
对象作为一个 Bean 注入到 Spring 中的呢?本文将带你从源码的角度进行分析。
先说结论,Spring 通过把 @MapperScan
注解指定的路径下的 Mappper
接口扫描成 Bean 定义,并通过 MapperFactoryBean
作为 Bean 定义的 BeanClass
属性,在 MapperFactoryBean
的 getObject()
方法中还是调用了 SqlSession
的 getMapper()
方法获取到动态代理对象,并把这个动态代理对象放到 Spring 容器中,供其它需要注入它的地方使用。
源码
首先看下 @MapperScan
注解,它通过 @Import
注解引入了实现了 ImportBeanDefinitionRegistrar
接口的 MapperScannerRegistrar
类。代码如下:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}
在它的 registerBeanDefinitions()
方法中,会从 @MapperScan
注解中获取配置的值,比如像 sqlSessionFactoryRef
, basePackages
等一些配置,然后构造了一个MapperScannerConfigurer
的 Bean 定义然后注册。代码如下:
java
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// 省略代码
String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
if (StringUtils.hasText(sqlSessionTemplateRef)) {
builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
}
String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
if (StringUtils.hasText(sqlSessionFactoryRef)) {
builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
}
// 省略代码
List<String> basePackages = new ArrayList<>();
basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
.collect(Collectors.toList()));
basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
.collect(Collectors.toList()));
if (basePackages.isEmpty()) {
basePackages.add(getDefaultBasePackage(annoMeta));
}
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
// for spring-native
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// 这里注册了一个MapperScannerConfigurer类型的Bean定义
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
}
而 MapperScannerConfigurer
实现了 BeanDefinitionRegistryPostProcessor
接口,这个接口的作用在前面的文章 3 个案例看透 Spring @Component 扫描:从普通应用到 Spring Boot介绍过。 在它的 postProcessBeanDefinitionRegistry()
方法中会委托 ClassPathMapperScanner
去扫描路径下的 Mapper
接口为 Bean 定义。代码如下:
java
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
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注册过滤器
scanner.registerFilters();
// 调用scan方法扫描Mapper
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
}
ClassPathMapperScanner
继承了 ClassPathBeanDefinitionScanner
,在它的 doScan()
方法中,首先会调用父类的 doScan()
方法扫描 Bean 定义,然后在 processBeanDefinitions()
方法中将 Bean 的 BeanClass
设置为 MapperFactoryBean
,从名字可以看出它是一个实现了 FactoryBean
接口的类。代码如下:
java
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
// 省略代码
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
// 设置BeanClass为MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBeanClass);
// 省略代码
}
}
}
在 Spring 中当一个 Bean 定义的 BeanClass 被设置为 FactoryBean
的时候,最终注册到容器中的实际上是它的 getObject()
方法返回的对象。而 MapperFactoryBean
的 getObject()
方法实际上还是通过 SqlSession
对象来获取 Mapper 的动态代理对象。从而实现了把原来 手动通过调用 SqlSession
的 getMapper()
方法得到动态代理对象放到 Spring 容器中了,其它地方就可以通过 @Autowired
注解进行注入。代码如下:
java
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}