使用 MyBatis 的 SqlSession
MyBatis 的 提供了执行 SQL 语句、提交或回滚事务和获取映射器实例的方法。SqlSession 由工厂类 SqlSessionFactory 来创建,SqlSessionFactory 又是构造器类 SqlSessionFactoryBuilder 创建的。
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
使用 mybatis-spring 的 SqlSession
使用 mybatis-spring 集成 Spring 时 ,SqlSessionFactory 使用了 Spring 的 FactoryBean 的实现类 SqlSessionFactoryBean 间接地调用 SqlSessionFactoryBuilder 来创建。 SqlSession 由 它的线程安全的实现类 SqlSessionTemplate 替代,它能基于 Spring 的事务机制自动提交、回滚、关闭 session。要在 Spring 容器中使用 SqlSessionTemplate,就要将其注入到容器中。
// 注入 SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSession() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 指定数据源连接信息
factoryBean.setDataSource(dataSource());
// 指定 mapper 文件路径
InputStream inputStream = Resources.getResourceAsStream("mapper/UserSpringMapper.xml");
factoryBean.setMapperLocations(new InputStreamResource(inputStream));
return factoryBean.getObject();
}
// 使用 Spring 事务机制
@Bean
PlatformTransactionManager getTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
使用 mybatis-spring-boot-starter 自动注入
如果使用 Springboot,可以通过引入mybatis-spring-boot-starter
,将 MyBatis 的组件自动注入到 Spring 容器中,这个 starter 会引入mybatis-spring-boot-autoconfigure
(查看如何开发自己的 Springboot starter),这个包里面有一个重要的配置类MybatisAutoConfiguration
,通过查看其源码可知,它还有两个静态内部类MapperScannerRegistrarNotFoundConfiguration
、AutoConfiguredMapperScannerRegistrar
,其中,MybatisAutoConfiguration
和MapperScannerRegistrarNotFoundConfiguration
都加了 Spring 的 @Configuration 注解,所以 Spring 启动时会将它们都加载到容器中,而AutoConfiguredMapperScannerRegistrar
是通过MapperScannerRegistrarNotFoundConfiguration
的注解 @Import 间接地注入容器的。
AutoConfiguredMapperScannerRegistrar
实现了 ImportBeanDefinitionRegistrar,所以其方法 registerBeanDefinitions() 会在容器启动时执行,主要有如下两个作用:
- 从 BeanFactory 获取包扫描的路径
- 初始化和配置 MapperScannerConfigurer (指定注解类型为 @Mapper、指定包路径等),注册到 BeanFactory
MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor,所以其方法 postProcessBeanDefinitionRegistry() 会在容器启动时执行,通过这个方法初始化 ClassPathBeanDefinitionScanner 的子类 ClassPathMapperScanner,调用 scan(String... basePackages),扫描包路径下 @Mapper 注解的所有接口,注册到 BeanFactory,接着进行后置处理:
- 将 BeanDefinition 的类型修改为 MapperFactoryBean
- 指定 MapperFactoryBean 的构造器参数为 @Mapper 接口类的全类名
- 设置 sqlSessionFactory、sqlSessionTemplate、按照类型自动装配等
- 利用反射创建 MapperFactoryBean 实例,调用其有参构造器,将 @Mapper 接口传入,缓存到 Class
mapperInterface
如下图: MapperFactoryBean 的继承关系
初始化和配置解析
DaoSupport 实现了 InitializingBean.afterPropertiesSet(),通过这个方法,将 Mapper 缓存到 MapperRegistry 的 Map
,key 为 Mapper 接口,value 为 Mapper 代理工厂类 MapperProxyFactory;最后,使用 MapperAnnotationBuilder.parse() 来解析 XML 配置文件或者方法注解,缓存到 Configuration 的 Map
,源码流程如下:
DaoSupport.afterPropertiesSet()
->MapperFactoryBean.checkDaoConfig()
->Configuration.addMapper(this.mapperInterface)
->MapperRegistry.addMapper(type)
->knownMappers.put(type, new MapperProxyFactory<>(type))
// 解析 SQL 配置
->MapperAnnotationBuilder.parse()
-->configuration.addMappedStatement(statement)
生成代理对象
MapperFactoryBean 实现了 FactoryBean.getObject(),从 knownMappers 缓存取出 Mapper 接口映射的 MapperProxyFactory,使用这个工厂类来创建 MapperProxy 代理类,从 MapperProxy
可知是使用了 JDK 的动态代理,源码流程如下:
MapperFactoryBean.getObject()
->SqlSessionTemplate.getMapper(mapperInterface)
->Configuration.getMapper(mapperInterface, this)
->MapperRegistry.getMapper(mapperInterface, sqlSession)
->MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
->mapperProxyFactory.newInstance(sqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
到这里,代理对象就生成了,在 Springboot 应用中就可以简单的通过 @Autowired 的注解方便的从容器中获取 Mapper 接口的代理对象(MapperProxy)了。
执行流程
假设存在 @Mapper 注解的类 UserDao。
@Mapper
public interface UserDao {
@Select("select * from t_user where id = #{id}")
Optional findOne(String id);
}
通过 @Autowired 获取 Bean。由上面可知,实际获取到的是代理对象 MapperProxy。
@Autowired
UserDao userDao;
调用 UserDao 的方法实际上执行的是代理对象 MapperProxy 的 invoke() 方法。
// 调用 findOne
userDao.findOne(id);
// 实际执行的方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
invoke() 方法大致的源码执行流程如下:
MapperMethod.execute(sqlSession, args)
sqlSessionProxy.selectOne(statement, parameter)
需要注意 SqlSession
在 SqlSessionTemplate 的有参构造器中初始化,并且它也是个代理类,被 SqlSessionInterceptor 代理
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
所以 selectOne 方法会被 SqlSessionInterceptor.invoke() 拦截,反射执行 SqlSession.selectOne() 方法,源码流程如下:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 打开获取 DefaultSqlSession;
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 反射执行 SqlSession 的方法 selectOne(String statement, Object parameter) 进行查询
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// 提交
sqlSession.commit(true);
}
// 返回查询结果
return result;
} catch (Throwable t) {
// 异常时释放连接
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
} finally {
if (sqlSession != null) {
// 释放连接
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
注解和配置文件
Springboot 应用同样可以选择使用注解,或者配置文件的方式使用 MyBatis,一般简单的增删改查直接使用注解的方式(比如 @Select、@SelectProvider)即可,可以减少很多配置文件;比较复杂的 SQL 可能还是使用配置文件的方式操作起来更加方便一些,具体还是得看实际情况来选择,需要注意的是,每个 DAO 可以同时存在注解和配置的方式,但是同一个方法不能同时存在注解和配置的方式。
如果是通过配置文件的方式,可以在 application.yml 配置文件指定 DAO 的配置文件所在位置:
# 使用基于配置文件的 MyBatis 时指定 Mapper 配置的路径
mybatis:
mapper-locations: mapper/*Dao.xml