百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

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

xsobi 2024-11-24 00:29 1 浏览

前言

专题要点如下:

本文要解决的是第二点,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是如何做的之前,我们不妨想一想,如果是我们自己要实现这个功能,那么你会怎么去做呢?

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

当然我这种做法省略了很多细节,比如如何将方法参数绑定到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配置了

<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. 使用哪种执行方式,一共有三种执行方式
  4. ExecutorType.SIMPLE:每次执行 SQL时都创建一个新的 PreparedStatement
  5. ExecutorType.REUSE:复用 PreparedStatement对象
  6. 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的初始化流程

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

继承关系

源码分析

看到它实现了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分析

继承关系

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分析

继承关系

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

  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,因为我们在前面进行了如下配置:通过我们配置的这个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提供的扩展点的例子,我相信这就是最好的一个例子。

文末福利:

→ 送全套程序员必读资料!

→ 精选技术资料共享!

→ 高手如云交流社群!

相关推荐

js向对象中添加元素(对象,数组) js对象里面添加元素

一、添加一个元素对象名["属性名"]=值(值:可以是一个值,可以是一个对象,也可以是一个数组)这样添加进去的元素,就是一个值或对象或数组...

JS小技巧,如何去重对象数组?(一)

大家好,关于数组对象去重的业务场景,想必大家都遇到过类似的需求吧,这对这样的需求你是怎么做的呢。下面我就先和大家分享下如果是基于对象的1个属性是怎么去重实现的。方法一:使用.filter()和....

「C/C++」之数组、vector对象和array对象的比较

数组学习过C语言的,对数组应该都不会陌生,于是这里就不再对数组进行展开介绍。模板类vector模板类vector类似于string,也是一种动态数组。能够在运行阶段设置vector对象的长度,可以在末...

如何用sessionStorage保存对象和数组

背景:在工作中,我将[{},{}]对象数组形式,存储到sessionStorage,然后ta变成了我看不懂的形式,然后我想取之用之,发现不可能了~记录这次深刻的教训。$clickCouponIndex...

JavaScript Array 对象 javascript的array对象

Array对象Array对象用于在变量中存储多个值:varcars=["Saab","Volvo","BMW"];第一个数组元素的索引值为0,第二个索引值为1,以此类推。更多有...

JavaScript中的数组Array(对象) js array数组

1:数组Array:-数组也是一个对象-数组也是用来存储数据的-和object不同,数组中可以存储一组有序的数据,-数组中存储的数据我们称其为元素(element)-数组中的每一个元素都有一...

数组和对象方法&amp;数组去重 数组去重的5种方法前端

列举一下JavaScript数组和对象有哪些原生方法?数组:arr.concat(arr1,arr2,arrn);--合并两个或多个数组。此方法不会修改原有数组,而是返回一个新数组...

C++ 类如何定义对象数组?初始化数组?linux C++第43讲

对象数组学过C语言的读者对数组的概念应该很熟悉了。数组的元素可以是int类型的变量,例如int...

ElasticSearch第六篇:复合数据类型-数组,对象

在ElasticSearch中,使用JSON结构来存储数据,一个Key/Value对是JSON的一个字段,而Value可以是基础数据类型,也可以是数组,文档(也叫对象),或文档数组,因此,每个JSON...

第58条:区分数组对象和类数组对象

示例设想有两个不同类的API。第一个是位向量:有序的位集合varbits=newBitVector;bits.enable(4);bits.enable([1,3,8,17]);b...

八皇后问题解法(Common Lisp实现)

如何才能在一张国际象棋的棋盘上摆上八个皇后而不致使她们互相威胁呢?这个著名的问题可以方便地通过一种树搜索方法来解决。首先,我们需要写一个函数来判断棋盘上的两个皇后是否互相威协。在国际象棋中,皇后可以沿...

visual lisp修改颜色的模板函数 怎么更改visual studio的配色

(defunBF-yansemokuai(tuyuanyanse/ss)...

用中望CAD加载LISP程序技巧 中望cad2015怎么加载燕秀

1、首先请加载lisp程序,加载方法如下:在菜单栏选择工具——加载应用程序——添加,选择lisp程序然后加载,然后选择添加到启动组。2、然后是添加自定义栏以及图标,方法如下(以...

图的深度优先搜索和广度优先搜索(Common Lisp实现)

为了便于描述,本文中的图指的是下图所示的无向图。搜索指:搜索从S到F的一条路径。若存在,则以表的形式返回路径;若不存在,则返回nil。...

两个有助于理解Common Lisp宏的例子

在Lisp中,函数和数据具有相同的形式。这是Lisp语言的一个重大特色。一个Lisp函数可以分析另一个Lisp函数;甚至可以和另一个Lisp函数组成一个整体,并加以利用。Lisp的宏,是实现上述特色的...