使用 MyBatis 的 SqlSession MyBatis 的 提供了執行 SQL 語句、提交或回滾事務和獲取映射器實例的方法。 SqlSession 由工廠類 SqlSessionFactory 來創建, SqlSessionFactory 又是構造器類 SqlSessionFactoryBu ...
使用 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<Class<?>, MapperProxyFactory<?>> knownMappers
,key 為 Mapper 介面,value 為 Mapper 代理工廠類 MapperProxyFactory;最後,使用 MapperAnnotationBuilder.parse() 來解析 XML 配置文件或者方法註解,緩存到 Configuration 的 Map<String, MappedStatement> mappedStatements
,源碼流程如下:
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<T> implements InvocationHandler
可知是使用了 JDK 的動態代理,源碼流程如下:
MapperFactoryBean.getObject()
->SqlSessionTemplate.getMapper(mapperInterface)
->Configuration.getMapper(mapperInterface, this)
->MapperRegistry.getMapper(mapperInterface, sqlSession)
->MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
->mapperProxyFactory.newInstance(sqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> 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<UserEntity> 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