前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring事务源码分析专题(二)Mybatis的使用及跟Spring整合原理分析

Spring事务源码分析专题(二)Mybatis的使用及跟Spring整合原理分析

作者头像
程序员DMZ
发布2020-07-24 15:39:13
1K0
发布2020-07-24 15:39:13
举报
文章被收录于专栏:程序员DMZ

前言

专题要点如下:

本文要解决的是第二点,Mybatis的使用、原理及跟Spring整合原理分析

Mybatis的简单使用

搭建项目

  1. pom文件添加如下依赖
代码语言:javascript
复制
<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
代码语言:javascript
复制

<?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文件如下
代码语言:javascript
复制
<?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. 实体类如下
代码语言:javascript
复制
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. 测试代码如下
代码语言:javascript
复制
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,例如在我们上面的例子中就直接调用了sqlsessionselectOne方法完成了查询。使用这种方法我们需要传入namespace+statamentId以便于Mybatis定位到要执行的SQL,另外还需要传入查询的参数
  2. 第二种形式,则是先通过sqlsession创建一个代理对象,然后调用代理对象的方法完成查询

本文要探究的原理主要是第二种形式的使用,换而言之,就是Mybatis是如何生成这个代理对象的。在思考Mybatis是如何做的之前,我们不妨想一想,如果是我们自己要实现这个功能,那么你会怎么去做呢?

如果是我的话,我会这么做:

当然我这种做法省略了很多细节,比如如何将方法参数绑定到SQL,如何封装结果集,是否对同样的Sql进行缓存等等。正常Mybatis在执行Sql时起码需要经过下面几个流程

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配置了

代码语言:javascript
复制
<transactionManager type="JDBC"/>

这意味着我们选用了JDBC的事务管理机制,那么我们在哪里可以开启事务呢?实际上Mybatis默认是关闭自动提交的,也就是说事务默认就是开启的。而是否开启事务我们可以在创建SqlSession时进行控制。SqlSessionFactory提供了以下几个用于创建SqlSession的方法

代码语言:javascript
复制
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中的事务管理,代码如下:

代码语言:javascript
复制
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时使用下面这种方式

代码语言:javascript
复制
 SqlSession sqlSession = sqlSessionFactory.openSession(true);

即使发生了异常,数据仍然会插入到数据库中

Spring整合Mybatis的原理

首先明白一点,虽然我在之前介绍了Mybatis的事务管理,但是当Mybatis跟Spring进行整合时,事务的管理完全由Spring进行控制!所以对于整合原理的分析不会涉及到事务的管理

我们先来看一个Spring整合Mybatis的案例,我这里以JavaConfig的形式进行整合,核心配置如下:

代码语言:javascript
复制
@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的初始化流程

首先我们看看这个类的继承关系

继承关系
源码分析

看到它实现了InitializingBean接口,那我们第一反应肯定是查看下它的afterPropertiesSet方法,其源码如下:

代码语言:javascript
复制
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可以是返回了这个被赋值的成员变量,其源码如下:

代码语言:javascript
复制
public SqlSessionFactory getObject() throws Exception {
  // 初始化阶段已经赋值了 
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }
  // 果不其然,直接返回
  return this.sqlSessionFactory;
}

@MapperScan工作原理

查看@MapperScan这个注解的源码我们会发现

代码语言:javascript
复制
@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做了什么,其源码如下:

代码语言:javascript
复制
// 这里我们只关注它的两个核心方法
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分析

继承关系

image-20200722092411193

从上面这张图中我们能得出的一个最重要的信息就是,MapperScannerConfigurer是一个Bean工厂的后置处理器,并且它实现的是BeanDefinitionRegistryPostProcessor,而BeanDefinitionRegistryPostProcessor通常都是用来完成扫描的,我们直接定位到它的postProcessBeanDefinitionRegistry方法,源码如下:

方法分析
代码语言:javascript
复制
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方法,代码如下:

代码语言:javascript
复制
// 这个方法的代码还是很简单的
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方法,其源码如下:

代码语言:javascript
复制
// 下面这个方法看起来代码很长,实际做的事情确很简单
// 主要做了这么几件事
// 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分析

继承关系

我们重点看下它的两个父类即可

  1. DaoSupport:这个类是所有的数据访问对象(DAO)的基类,它定义的所有DAO的初始化模板,它实现了InitializingBean接口,核心方法就是afterPropertiesSet,其源码如下:
代码语言:javascript
复制

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,起源吗如下:
代码语言:javascript
复制

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的一个构造函数,其代码如下:

代码语言:javascript
复制

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我们关注下面两个方法就行了

代码语言:javascript
复制
// 之前分析过了,这个方法会在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可以通过下面这种方式直接生成一个代理对象

代码语言:javascript
复制
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是如何接手的呢?对于这个问题,在后续的文章中我会做详细分析。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员DMZ 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Mybatis的简单使用
    • 搭建项目
      • 原理分析
      • Mybaits中的事务管理
      • Spring整合Mybatis的原理
        • SqlSessionFactoryBean的初始化流程
          • @MapperScan工作原理
            • MapperScannerConfigurer分析
            • MapperFactoryBean分析
          • 整合原理总结
          • 总结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档