亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何進行Mybatis的使用及跟Spring整合原理分析

發布時間:2021-11-09 18:00:21 來源:億速云 閱讀:165 作者:柒染 欄目:大數據

如何進行Mybatis的使用及跟Spring整合原理分析,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。

前言

專題要點如下:

如何進行Mybatis的使用及跟Spring整合原理分析  

這里要解決的是第二點,Mybatis的使用、原理及跟Spring整合原理分析

 

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>
  2. 創建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}
   

原理分析

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

從上面的例子中我們可以看到,對于Mybatis的使用主要有兩種形式

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

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

如果是我的話,我會這么做:

如何進行Mybatis的使用及跟Spring整合原理分析  

當然我這種做法省略了很多細節,比如如何將方法參數綁定到SQL,如何封裝結果集,是否對同樣的Sql進行緩存等等。正常Mybatis在執行Sql時起碼需要經過下面幾個流程

如何進行Mybatis的使用及跟Spring整合原理分析  
9

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

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

 

Mybaits中的事務管理

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

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

  2. 使用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主要是根據以下幾點

  1. 是否要關閉自動提交,意味著開啟事務
  2. 使用外部傳入的連接對象還是從配置信息中獲取到的連接對象
  3. 使用哪種執行方式,一共有三種執行方式
    • 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);
 

即使發生了異常,數據仍然會插入到數據庫中

 

Spring整合Mybatis的原理

首先明白一點,雖然我在之前介紹了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的,我們應該要弄明白兩點

  1. @MapperScan這個注解干了什么?
  2. SqlSessionFactoryBean這個Bean的創建過程中干了什么?

接下來我們就分為兩點來進行討論

 

SqlSessionFactoryBean的初始化流程

首先我們看看這個類的繼承關系

 
繼承關系
如何進行Mybatis的使用及跟Spring整合原理分析  
 
源碼分析

看到它實現了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工作原理

查看@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分析

 
繼承關系
如何進行Mybatis的使用及跟Spring整合原理分析  
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會做進一步的處理。

基于此,我們先來看看,它的掃描規則是怎么樣的?查看其registerFiltersisCandidateComponent方法,代碼如下:

// 這個方法的代碼還是很簡單的
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,而是先將這些BeanDefinitionbeanClass屬性全部都設置成了MapperFactoryBean,從名字上我們就能知道他是一個FactoryBean,那么不難猜測肯定是通過這個FactoryBeangetObject方法來創建了一個代理對象,我們查看下這個類的源碼:

 

MapperFactoryBean分析

 
繼承關系
如何進行Mybatis的使用及跟Spring整合原理分析  

我們重點看下它的兩個父類即可

  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);
        }
    }
  2. 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,因為我們在前面進行了如下配置:

    如何進行Mybatis的使用及跟Spring整合原理分析    

    通過我們配置的這個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接口,它的所有操作最終都是依賴其成員變量sqlSessionProxysqlSessionProxy是通過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是如何獲取的,不出意外的話肯定也是調用了MybaitssqlSessionFactory.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整合原理分析的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

宾川县| 新余市| 曲靖市| 霍林郭勒市| 赤水市| 英德市| 庆元县| 昌平区| 仁寿县| 宿迁市| 会理县| 肥东县| 旬邑县| 永丰县| 凯里市| 普兰店市| 南澳县| 冀州市| 石城县| 黄平县| 黔南| 齐齐哈尔市| 巴塘县| 阜新| 观塘区| 东宁县| 海安县| 盘锦市| 兴安盟| 镇赉县| 浠水县| 瑞昌市| 古交市| 百色市| 玉山县| 怀仁县| 贺州市| 女性| 建始县| 承德市| 巫山县|