Spring事務源碼分析專題(二)Mybatis的使用及跟Spring整合原理分析

前言

專題要點以下:java

Spring事務源碼分析專題(二)Mybatis的使用及跟Spring整合原理分析

本文要解決的是第二點,Mybatis的使用、原理及跟Spring整合原理分析。mysql

Mybatis的簡單使用

搭建項目程序員

  1. 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>
  1. 建立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>
  1. 建立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>
  1. 實體類以下
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 +
            '}';
    }
}
  1. 測試代碼以下
    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}spring

> **原理分析**

由於本專欄不是對mybatis的源碼分析專題(筆者對於三大框架都會作一個源碼分析專題),因此對這塊的原理分析不會牽涉到過多源碼級別的內容。

從上面的例子中咱們能夠看到,對於Mybatis的使用主要有兩種形式

1. 直接經過sqlsession調用相關的增刪改查的API,例如在咱們上面的例子中就直接調用了sqlsession的selectOne方法完成了查詢。使用這種方法咱們須要傳入namespace+statamentId以便於Mybatis定位到要執行的SQL,另外還須要傳入查詢的參數
1. 第二種形式,則是先經過sqlsession建立一個代理對象,而後調用代理對象的方法完成查詢

本文要探究的原理主要是第二種形式的使用,換而言之,就是Mybatis是如何生成這個代理對象的。在思考Mybatis是如何作的以前,咱們不妨想想,若是是咱們本身要實現這個功能,那麼你會怎麼去作呢?

若是是個人話,我會這麼作:

![](https://s4.51cto.com/images/blog/202008/10/5d09bd2be5dcfffe5fb9ca6d0f34a152.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

固然我這種作法省略了不少細節,好比如何將方法參數綁定到SQL,如何封裝結果集,是否對一樣的Sql進行緩存等等。正常Mybatis在執行Sql時起碼須要通過下面幾個流程

![](https://s4.51cto.com/images/blog/202008/10/6992f5b6c7bd628daad4ad18624dc491.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
9

其中,Executor負責維護緩存以及事務的管理,它會將對數據庫的相關操做委託給StatementHandler完成,StatementHandler會先經過ParameterHandler完成對Sql語句的參數的綁定,而後調用JDBC相關的API去執行Sql獲得結果集,最後經過ResultHandler完成對結果集的封裝。

本文只是對這個流程有個大體的瞭解便可,詳細的流程介紹咱們在Mybatis的源碼分析專欄中再聊~

# Mybaits中的事務管理

Mybatis中的事務管理主要有兩種方式

1. 使用JDBC的事務管理機制:即利用JDBC中的java.sql.Connection對象完成對事務的提交(commit())、回滾(rollback())、關閉(close())等

1. 使用MANAGED的事務管理機制:這種機制MyBatis自身不會去實現事務管理,而是讓程序的容器如(tomcat,jboss)來實現對事務的管理

在文章開頭的例子中,我在mybatis-config.xml配置了

<transactionManager type="JDBC"/>sql

這意味着咱們選用了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主要是根據如下幾點

1. 是否要關閉自動提交,意味着開啓事務
1. 使用外部傳入的鏈接對象仍是從配置信息中獲取到的鏈接對象
1. 使用哪一種執行方式,一共有三種執行方式
* 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("發生異常!");
}
}apache

運行上面的代碼,咱們會發現數據庫中並不會新增一條數據,可是若是咱們在建立SqlSession時使用下面這種方式

 SqlSession sqlSession = sqlSessionFactory.openSession(true);
即便發生了異常,數據仍然會插入到數據庫中

# Spring整合Mybatis的原理

首先明白一點,雖然我在以前介紹了Mybatis的事務管理,可是當Mybatis跟Spring進行整合時,事務的管理徹底由Spring進行控制!因此對於整合原理的分析不會涉及到事務的管理

咱們先來看一個Spring整合Mybatis的案例,我這裏以JavaConfig的形式進行整合,核心配置以下:

@Configurationbr/>@ComponentScan("com.dmz.mybatis.spring")
// 掃描全部的mapper接口br/>@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;
}

}tomcat

從這段配置中咱們能夠提煉出一個關鍵信息,若是咱們要弄清楚Spring是如何整合Mybatis的,咱們應該要弄明白兩點

1. @MapperScan這個註解幹了什麼?
1. SqlSessionFactoryBean這個Bean的建立過程當中幹了什麼?

接下來咱們就分爲兩點來進行討論

> **SqlSessionFactoryBean的初始化流程**

首先咱們看看這個類的繼承關係

**繼承關係**

![](https://s4.51cto.com/images/blog/202008/10/ccfd24a2e47157e095315f0c6fa28581.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

**源碼分析**

看到它實現了InitializingBean接口,那咱們第一反應確定是查看下它的afterPropertiesSet方法,其源碼以下:

public void afterPropertiesSet() throws Exception {
// 調用buildSqlSessionFactory方法完成對成員屬性sqlSessionFactory的賦值
this.sqlSessionFactory = buildSqlSessionFactory();
}session

// 經過咱們在配置中指定的信息構建一個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工做原理**

查看@MapperScan這個註解的源碼咱們會發現

@Retention(RetentionPolicy.RUNTIME)br/>@Target(ElementType.TYPE)
@Documentedbr/>@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分析

**繼承關係**

![](https://s4.51cto.com/images/blog/202008/10/56e949bc31c51a957d019844a52b3f3c.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
image-20200722092411193
從上面這張圖中咱們能得出的一個最重要的信息就是,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方法來建立了一個代理對象,咱們查看下這個類的源碼:

MapperFactoryBean分析

**繼承關係**

![](https://s4.51cto.com/images/blog/202008/10/6d5a1e56a9f40fba4bd088992cf97280.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

咱們重點看下它的兩個父類便可

1. 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);
}

}

1. 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,確保其不爲空br/>@Override
protected void checkDaoConfig() {
notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}

咱們在整合Spring跟Mybatis時,就是調用setSqlSessionFactory完成了對這個類中SqlSessionTemplate的初始化。前面咱們也提到了MapperFactoryBean默認使用的是自動注入,因此在建立每個MapperFactoryBean的屬性注入階段,Spring容器會自動查詢是否有跟MapperFactoryBean中setter方法的參數類型匹配的Bean,由於咱們在前面進行了以下配置:
![](https://s4.51cto.com/images/blog/202008/10/8b5e4ddef1df06138b4f693d374e5d94.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

經過咱們配置的這個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 {br/>@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中現成的方法獲取一個代理對象而後放入到容器中br/>@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提供的擴展點的例子,我相信這就是最好的一個例子。

本文爲事務專欄的第二篇,之因此特意寫一篇mybaits的文章是由於後續咱們不只要分析單獨的Spring中的事務管理,還得分析Spring整合Mybatis的事務管理,雖然Spring整合Mybatis後徹底由Spring來進行管理事務,可是咱們要知道Mybatis自身是有本身的事務管理機制的,那麼Spring是如何接手的呢?對於這個問題,在後續的文章中我會作詳細分析

本文就到這裏啦,若是本位對你有幫助的話,記得幫忙三連哈,感謝~!

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!

**往期精選**

Spring官網閱讀筆記
Spring雜談
JVM系列文章
Spring源碼專題
![](https://s4.51cto.com/images/blog/202008/10/d2b2b3f6e2ed85edeb57fb04142b7391.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

程序員DMZ
點贊、轉發、在看,多謝多謝!
喜歡做者
相關文章
相關標籤/搜索