温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

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

发布时间:2021-11-09 18:00:21 阅读:181 作者:柒染 栏目:大数据
开发者测试专用服务器限时活动,0元免费领,库存有限,领完即止! 点击查看>>

如何进行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方法创建一个SqlSessionFactoryprotected 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中现成的方法获取一个代理对象然后放入到容器中@Overridepublic 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整合原理分析的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

原文链接:https://my.oschina.net/u/4547531/blog/4422748

AI

开发者交流群×