您好,登錄后才能下訂單哦!
如何進行Mybatis的使用及跟Spring整合原理分析,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
專題要點如下:
這里要解決的是第二點,Mybatis的使用、原理及跟Spring整合原理分析
。
pom
文件添加如下依賴<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
創建mybaits
配置文件,mybatis-config.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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="password" value="123"/>
<property name="username" value="root"/>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/userMapper.xml"/>
</mappers>
</configuration>
創建mapper.xml
文件如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.dmz.mapper.UserMapper">
<select id="selectOne" resultType="org.apache.ibatis.dmz.entity.User">
select * from user where id = #{id}
</select>
</mapper>
public class User {
private int id;
private String name;
private int age;
// 省略getter/setter方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
// 1.解析XML配置
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 2.基于解析好的XML配置創建一個SqlSessionFactory
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
// 3.通過SqlSessionFactory,創建一個SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.測試直接調用mapper.xml中的方法
Object o = sqlSession.selectOne("org.apache.ibatis.dmz.mapper.UserMapper.selectOne",2);
if(o instanceof User){
System.out.println("直接執行mapper文件中的sql查詢結果:"+o);
}
// 5.獲取一個代理對象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 6.調用代理對象的方法
System.out.println("代理對象查詢結果:"+mapper.selectOne(1));
}
}
// 程序輸出如下,分別對應了我本地數據庫中的兩條記錄
// 直接執行mapper文件中的sql查詢結果:User{id=2, name='dmz', age=18}
// 代理對象查詢結果:User{id=1, name='dmz', age=18}
因為本專欄不是對mybatis
的源碼分析專題(筆者對于三大框架都會做一個源碼分析專題),所以對這塊的原理分析不會牽涉到過多源碼級別的內容。
從上面的例子中我們可以看到,對于Mybatis
的使用主要有兩種形式
sqlsession
調用相關的增刪改查的
API
,例如在我們上面的例子中就直接調用了
sqlsession
的
selectOne
方法完成了查詢。使用這種方法我們需要傳入
namespace+statamentId
以便于
Mybatis
定位到要執行的
SQL
,另外還需要傳入查詢的參數sqlsession
創建一個
代理對象
,然后調用代理對象的方法完成查詢本文要探究的原理主要是第二種形式的使用,換而言之,就是Mybatis
是如何生成這個代理對象的。在思考Mybatis是如何做的之前,我們不妨想一想,如果是我們自己要實現這個功能,那么你會怎么去做呢?
如果是我的話,我會這么做:
當然我這種做法省略了很多細節,比如如何將方法參數綁定到SQL
,如何封裝結果集,是否對同樣的Sql
進行緩存等等。正常Mybatis在執行Sql時起碼需要經過下面幾個流程
其中,Executor負責維護緩存以及事務的管理,它會將對數據庫的相關操作委托給StatementHandler
完成,StatementHandler
會先通過ParameterHandler
完成對Sql語句的參數的綁定,然后調用JDBC相關的API去執行Sql得到結果集,最后通過ResultHandler
完成對結果集的封裝。
本文只是對這個流程有個大致的了解即可,詳細的流程介紹我們在Mybatis的源碼分析專欄中再聊~
Mybatis中的事務管理主要有兩種方式
使用JDBC
的事務管理機制:即利用JDBC
中的java.sql.Connection
對象完成對事務的提交(commit())、回滾(rollback())、關閉(close())等
使用MANAGED的事務管理機制:這種機制MyBatis
自身不會去實現事務管理,而是讓程序的容器如(tomcat,jboss)來實現對事務的管理
在文章開頭的例子中,我在mybatis-config.xml
配置了
<transactionManager type="JDBC"/>
這意味著我們選用了JDBC
的事務管理機制,那么我們在哪里可以開啟事務呢?實際上Mybatis默認是關閉自動提交的,也就是說事務默認就是開啟的。而是否開啟事務我們可以在創建SqlSession
時進行控制。SqlSessionFactory
提供了以下幾個用于創建SqlSession
的方法
SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
我們在覺得使用哪個方法來創建SqlSession
主要是根據以下幾點
ExecutorType.SIMPLE
:每次執行
SQL
時都創建一個新的
PreparedStatement
ExecutorType.REUSE
:復用
PreparedStatement
對象ExecutorType.BATCH
:進行批處理在前面的例子中,我們使用的是空參的方法來創建SqlSession
對象的,這種情況下Mybatis會創建一個開啟了事務的、從配置的連接池中獲取連接的、事務隔離級別跟數據庫保持一致的、執行方式為ExecutorType.SIMPLE
的SqlSession對象。
我們基于上面的例子來體會一下Mybatis中的事務管理,代碼如下:
public class Main {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
// 1.解析XML配置
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 2.基于解析好的XML配置創建一個SqlSessionFactory
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
// 3.開啟一個SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.獲取一個代理對象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user =new User();
user.setId(3);
user.setName("dmz111");
user.setAge(27);
// 插入一條數據
mapper.insert(user);
// 拋出一個異常
throw new RuntimeException("發生異常!");
}
}
運行上面的代碼,我們會發現數據庫中并不會新增一條數據,但是如果我們在創建SqlSession
時使用下面這種方式
SqlSession sqlSession = sqlSessionFactory.openSession(true);
即使發生了異常,數據仍然會插入到數據庫中
首先明白一點,雖然我在之前介紹了Mybatis
的事務管理,但是當Mybatis
跟Spring進行整合時,事務的管理完全由Spring進行控制!所以對于整合原理的分析不會涉及到事務的管理
我們先來看一個Spring整合Mybatis
的案例,我這里以JavaConfig的形式進行整合,核心配置如下:
@Configuration
@ComponentScan("com.dmz.mybatis.spring")
// 掃描所有的mapper接口
@MapperScan("com.dmz.mybatis.spring.mapper")
public class MybatisConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setPassword("123");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8");
return driverManagerDataSource;
}
// 需要配置這個SqlSessionFactoryBean來得到一個SqlSessionFactory
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(patternResolver.getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean;
}
// 使用Spring中的DataSourceTransactionManager管理事務
@Bean
public TransactionManager transactionManager() {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
}
從這段配置中我們可以提煉出一個關鍵信息,如果我們要弄清楚Spring是如何整合Mybatis的,我們應該要弄明白兩點
@MapperScan
這個注解干了什么?SqlSessionFactoryBean
這個Bean的創建過程中干了什么?接下來我們就分為兩點來進行討論
首先我們看看這個類的繼承關系
看到它實現了InitializingBean
接口,那我們第一反應肯定是查看下它的afterPropertiesSet
方法,其源碼如下:
public void afterPropertiesSet() throws Exception {
// 調用buildSqlSessionFactory方法完成對成員屬性sqlSessionFactory的賦值
this.sqlSessionFactory = buildSqlSessionFactory();
}
// 通過我們在配置中指定的信息構建一個SqlSessionFactory
// 如果你對mybatis的源碼有一定了解的話
// 這個方法做的事情實際就是先構造一個Configuration對象
// 這個Configuration對象代表了所有的配置信息
// 等價于我們通過myabtis-config.xml指定的配置信息
// 然后調用sqlSessionFactoryBuilder的build方法創建一個SqlSessionFactory
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
// 接下來是通過配置信息構建Configuration對象的過程
// 我這里只保留幾個重要的節點信息
XMLConfigBuilder xmlConfigBuilder = null;
// 我們可以通過configLocation直接指定mybatis-config.xml的位置
if (this.configuration != null) {
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
LOGGER.debug(
() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
// 可以指定別名
if (hasLength(this.typeAliasesPackage)) {
scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
.filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
.filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
}
if (!isEmpty(this.typeAliases)) {
Stream.of(this.typeAliases).forEach(typeAlias -> {
targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
});
}
// 這里比較重要,注意在這里將事務交由了Spring進行管理
targetConfiguration.setEnvironment(new Environment(this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource));
// 可以直接指定mapper.xml
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
可以看到在初始化階段做的最重要的是就是給成員變量sqlSessionFactory
賦值,同時我們知道這是一個FactoryBean
,那么不出意外,它的getObject可以是返回了這個被賦值的成員變量,其源碼如下:
public SqlSessionFactory getObject() throws Exception {
// 初始化階段已經賦值了
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
// 果不其然,直接返回
return this.sqlSessionFactory;
}
查看@MapperScan
這個注解的源碼我們會發現
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
// basePackages屬性的別名,等價于basePackages
String[] value() default {};
// 掃描的包名
String[] basePackages() default {};
// 可以提供一個類,以類的包名作為掃描的包
Class<?>[] basePackageClasses() default {};
// BeanName的生成器,一般用默認的就好啦
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
// 指定要掃描的注解
Class<? extends Annotation> annotationClass() default Annotation.class;
// 指定標記接口,只有繼承了這個接口才會被掃描
Class<?> markerInterface() default Class.class;
// 指定SqlSessionTemplate的名稱,
// SqlSessionTemplate是Spring對Mybatis中SqlSession的封裝
String sqlSessionTemplateRef() default "";
// 指定SqlSessionFactory的名稱
String sqlSessionFactoryRef() default "";
// 這個屬性是什么意思呢?Spring跟Mybatis整合
// 最重要的事情就是將Mybatis生成的代理對象交由Spring來管理
// 實現這個功能的就是這個MapperFactoryBean
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
// 是否對mapper進行懶加載,默認為false
String lazyInitialization() default "";
}
接著我們就來看看MapperScannerRegistrar
做了什么,其源碼如下:
// 這里我們只關注它的兩個核心方法
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 獲取到@MapperScan這個注解中的屬性
AnnotchaationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
// 緊接著開始向Spring容器中注冊bd
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 打算注冊到容器中的bd的beanClass屬性為MapperScannerConfigurer.class
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
// 省略部分代碼
// ....
// 這部分代碼就是將注解中的屬性獲取出來
// 放到MapperScannerConfigurer這個beanDefinition中
// 最后將這個beanDefinition注冊到容器中
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
到這里我們可以確定了,@MapperScan
這個注解最大的作用就是向容器中注冊一個MapperScannerConfigurer
,我們順藤摸瓜,再來分析下MapperScannerConfigurer
是用來干嘛的
從上面這張圖中我們能得出的一個最重要的信息就是,MapperScannerConfigurer
是一個Bean工廠的后置處理器,并且它實現的是BeanDefinitionRegistryPostProcessor
,而BeanDefinitionRegistryPostProcessor
通常都是用來完成掃描的,我們直接定位到它的postProcessBeanDefinitionRegistry
方法,源碼如下:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
// 處理@MaperScan注解屬性中的占位符
processPropertyPlaceHolders();
}
// 在這里創建了一個ClassPathMapperScanner
// 這個類繼承了ClassPathBeanDefinitionScanner,并復寫了它的doScan、registerFilters等方法
// 其整體行為跟ClassPathBeanDefinitionScanner差不多,
// 關于ClassPathBeanDefinitionScanner的分析可以參考之前的《你知道Spring是怎么解析配置類的嗎?》
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));
}
// 這里設置了掃描規則
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
這個方法的整體實現邏輯還是比較簡單的,內部就是創建了一個ClassPathMapperScanner
來進行掃描,這個類本身繼承自ClassPathBeanDefinitionScanner
,關于ClassPathBeanDefinitionScanner
在之前的文章中已經做過詳細分析了,見《你知道Spring是怎么解析配置類的嗎?》如果你沒有看過之前的文章,問題也不大,你只需要知道是這個類完成了掃描并將掃描得到的BeanDefinition
注冊到容器中即可。ClassPathMapperScanner
復寫了這個類的doScan
方法已經registerFilters
,而在doScan
方法中這個類只是簡單調用了父類的doScan
方法完成掃描在對掃描后得到的BeanDefinition
做一些后置處理,也就是說ClassPathMapperScanner
只是在父類的基礎上定義了自己的掃描規則,通過對掃描后的BeanDefinition
會做進一步的處理。
基于此,我們先來看看,它的掃描規則是怎么樣的?查看其registerFilters
及isCandidateComponent
方法,代碼如下:
// 這個方法的代碼還是很簡單的
public void registerFilters() {
boolean acceptAllInterfaces = true;
// 第一步,判斷是否要掃描指定的注解
// 也就是判斷在@MapperScan注解中是否指定了要掃描的注解
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}
// 第二步,判斷是否要掃描指定的接口
// 同樣也是根據@MapperScan注解中的屬性做判斷
if (this.markerInterface != null) {
addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
@Override
protected boolean matchClassName(String className) {
return false;
}
});
acceptAllInterfaces = false;
}
// 如果既沒有指定注解也沒有指定標記接口
// 那么所有.class文件都會被掃描
if (acceptAllInterfaces) {
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
// 排除package-info文件
addExcludeFilter((metadataReader, metadataReaderFactory) -> {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
});
}
// 這個方法會對掃描出來的BeanDefinition進行檢查,必須符合要求才會注冊到容器中
// 從這里我們可以看出,BeanDefinition必須要是接口才行
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
從上面兩個方法中我們可以得出結論,默認情況下@MapperScan
注解會掃描指定包下的所有接口。
在前文我們也提到了,ClassPathBeanDefinitionScanner
不僅自定義了掃描的規則,而且復寫了doScan
方法,在完成掃描后會針對掃描出來的BeanDefinition
做一下后置處理,那么它做了什么呢?我們查看它的processBeanDefinitions
方法,其源碼如下:
// 下面這個方法看起來代碼很長,實際做的事情確很簡單
// 主要做了這么幾件事
// 1.將掃描出來的BeanDefinition的beanClass屬性設置為MapperFactoryBeanClass.class
// 2.在BeanDefinition的ConstructorArgumentValues添加一個參數
// 限定實例化時使用MapperFactoryBeanClass的帶參構造函數
// 3.檢查是否顯示的配置了sqlSessionFactory或者sqlSessionTemplate
// 4.如果沒有進行顯示配置,那么將這個BeanDefinition的注入模型設置為自動注入
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
// 往構造函數的參數集合中添加了一個值,那么在實例化時就會使用帶參的構造函數
// 等價于在XML中配置了
// <constructor-arg name="mapperInterface" value="mapperFactoryBeanClass"/>
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
// 將真實的BeanClass屬性設置為mapperFactoryBeanClass
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
// 開始檢查是否顯示的指定了sqlSessionFactory或者sqlSessionTemplate
boolean explicitFactoryUsed = false;
// 首先檢查是否在@MapperScan注解上配置了sqlSessionFactoryRef屬性
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
// 如果配置了的話,那么在這個bd的屬性集合中添加一個RuntimeBeanReference
// 等價于在xml中配置了
// <property name="sqlSessionFactory" ref="sqlSessionFactoryBeanName"/>
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
// 如果@MapperScan上沒有進行配置
// 那么檢查是否為這個bean配置了sqlSessionFactory屬性
// 正常來說我們都不會進行配置,會進入自動裝配的邏輯
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
// 省略sqlSessionTemplate部分代碼
// 邏輯跟sqlSessionFactory屬性的處理邏輯一致
// 需要注意的是,如果同時顯示指定了sqlSessionFactory跟sqlSessionTemplate
// 那么sqlSessionFactory的配置將失效
// .....
if (!explicitFactoryUsed) {
// 如果沒有顯示的配置,那么設置為自動注入
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
// 默認不是懶加載
definition.setLazyInit(lazyInitialization);
}
}
從上面的代碼中我們不難看到一個最特殊的操作,掃描出來的BeanDefinition
并沒有直接用去創建Bean,而是先將這些BeanDefinition
的beanClass
屬性全部都設置成了MapperFactoryBean
,從名字上我們就能知道他是一個FactoryBean
,那么不難猜測肯定是通過這個FactoryBean
的getObject
方法來創建了一個代理對象,我們查看下這個類的源碼:
我們重點看下它的兩個父類即可
DaoSupport
:這個類是所有的數據訪問對象(DAO)的基類,它定義的所有DAO的初始化模板,它實現了InitializingBean
接口,核心方法就是afterPropertiesSet
,其源碼如下:
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// 子類可以實現這個方法去檢查相關的配置信息
checkDaoConfig();
// 子類可以實現這個方法去進行一些初始化操作
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
SqlSessionDaoSupport
:這個類是專門為Mybatis
設計的,通過它能獲取到一個SqlSession
,起源嗎如下:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
// 這個是核心方法
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
// 省略一些getter/setter方法
// 在初始化時要檢查sqlSessionTemplate,確保其不為空
@Override
protected void checkDaoConfig() {
notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
我們在整合Spring跟Mybatis時,就是調用setSqlSessionFactory
完成了對這個類中SqlSessionTemplate
的初始化。前面我們也提到了MapperFactoryBean
默認使用的是自動注入,所以在創建每一個MapperFactoryBean
的屬性注入階段,Spring容器會自動查詢是否有跟MapperFactoryBean
中setter方法的參數類型匹配的Bean,因為我們在前面進行了如下配置:
通過我們配置的這個sqlSessionFactoryBean
能得到一個sqlSessionFactory
,因此在對MapperFactoryBean
進行屬性注入時會調用setSqlSessionFactory
方法。我們可以看到setSqlSessionFactory
方法內部就是通過sqlSessionFactory
創建了一個sqlSessionTemplate
。它最終會調用到sqlSessionTemplate
的一個構造函數,其代碼如下:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
SqlSessionTemplate
本身實現了org.apache.ibatis.session.SqlSession
接口,它的所有操作最終都是依賴其成員變量sqlSessionProxy
,sqlSessionProxy
是通過jdk動態代理生成的,對于動態代理生成的對象其實際執行時都會調用到InvocationHandler
的invoke方法,對應到我們上邊的代碼就是SqlSessionInterceptor
的invoke方法,對應代碼如下:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 第一步,獲取一個sqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 第二步,調用sqlSession對應的方法
Object result = method.invoke(sqlSession, args);
// 檢查是否開啟了事務,如果沒有開啟事務那么強制提交
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
// 處理異常
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
// 關閉sqlSession
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
我們再來看看,他在獲取SqlSession
是如何獲取的,不出意外的話肯定也是調用了Mybaits
的sqlSessionFactory.openssion
方法創建的一個sqlSession
,代碼如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 看到了吧,在這里調用了SqlSessionFactory創建了一個sqlSession
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
// 如果開啟了事務的話并且事務是由Spring管理的話,會將sqlSession綁定到當前線程上
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
對于MapperFactoryBean
我們關注下面兩個方法就行了
// 之前分析過了,這個方法會在MapperFactoryBean進行初始化的時候調用
protected void checkDaoConfig() {
super.checkDaoConfig();
Configuration configuration = getSqlSession().getConfiguration();
//addToConfig默認為true的,將mapper接口添加到mybatis的配置信息中
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e)
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
// 簡單吧,直接調用了mybatis中現成的方法獲取一個代理對象然后放入到容器中
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
首先我們知道,Mybatis
可以通過下面這種方式直接生成一個代理對象
String resource = "mybatis-config.xml";
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
基于這個代理對象,我們可以執行任意的Sql語句,那么如果Spring想要整合Mybatis,只需要將所有的代理對象管理起來即可,如何做到這一步呢?
這里就用到了Spring提供的一些列擴展點,首先,利用了BeanDefinitionRegistryPostProcessor
這個擴展點,利用它的postProcessBeanDefinitionRegistry
方法完成了對mapper接口的掃描,并將其注冊到容器中,但是這里需要注意的是,它并不是簡單的進行了掃描,在完成掃描的基礎上它將所有的掃描出來的BeanDefinition的beanClass屬性都替換成了MapperFactoryBean
,這樣做的原因是因為我們無法根據一個接口來生成Bean,并且實際生成代理對象的邏輯是由Mybatis控制的而不是Spring控制,Spring只是調用了mybatis的API來完成代理對象的創建并放入到容器中,基于這種需求,使用FactoryBean
是再合適不過了。
還有通過上面的分析我們會發現,并不是一開始就創建了一個SqlSession
對象的,而是在實際方法執行時才會去獲取SqlSession
的。
我們主要學習了Mybatis的基本使用,并對Mybatis的事務管理以及Spring整合Mybatis的原理進行了分析,其中最重要的便是整合原理的分析
,之前有小伙伴問我能不能介紹一些實際使用了Spring提供的擴展點的例子,我相信這就是最好的一個例子。
看完上述內容,你們掌握如何進行Mybatis的使用及跟Spring整合原理分析的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。