欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析

程序员文章站 2023-10-31 10:39:22
在前面几篇文章中我们主要分析了Mybatis的单独使用,在实际在常规项目开发中,大部分都会使用mybatis与Spring结合起来使用,毕竟现在不用Spring开发的项目实在太少了。本篇文章便来介绍下Mybatis如何与Spring结合起来使用,并介绍下其源码是如何实现的。 Spring-Mybat ......

在前面几篇文章中我们主要分析了mybatis的单独使用,在实际在常规项目开发中,大部分都会使用mybatis与spring结合起来使用,毕竟现在不用spring开发的项目实在太少了。本篇文章便来介绍下mybatis如何与spring结合起来使用,并介绍下其源码是如何实现的。

spring-mybatis使用

添加maven依赖

<dependency>
    <groupid>org.springframework</groupid>
    <artifactid>spring-jdbc</artifactid>
    <version>4.3.8.release</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupid>org.mybatis</groupid>
    <artifactid>mybatis-spring</artifactid>
    <version>1.3.2</version>
</dependency>

在src/main/resources下添加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>
    <typealiases>
        <typealias alias="user" type="com.chenhao.bean.user" />
    </typealiases>
    <plugins>
        <plugin interceptor="com.github.pagehelper.pageinterceptor">
            <property name="helperdialect" value="mysql"/>
        </plugin>
    </plugins>

</configuration>

在src/main/resources/mapper路径下添加user.xml

<?xml version="1.0" encoding="utf-8"?>
<!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
<mapper namespace="com.chenhao.mapper.usermapper">
    <select id="getuser" parametertype="int"
        resulttype="com.chenhao.bean.user">
        select *
        from user
        where id = #{id}
    </select>
</mapper>

在src/main/resources/路径下添加beans.xml

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
    xsi:schemalocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="datasource" class="org.springframework.jdbc.datasource.drivermanagerdatasource">
        <property name="driverclassname" value="com.mysql.jdbc.driver"></property>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
 
    <bean id="sqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean">
        <property name="configlocation" value="classpath:mybatis-config.xml"></property>
        <property name="datasource" ref="datasource" />
        <property name="mapperlocations" value="classpath:mapper/*.xml" />
    </bean>
    
    <bean class="org.mybatis.spring.mapper.mapperscannerconfigurer">
        <property name="basepackage" value="com.chenhao.mapper" />
    </bean>
 
</beans>

注解的方式

  • 以上分析都是在spring的xml配置文件applicationcontext.xml进行配置的,mybatis-spring也提供了基于注解的方式来配置sqlsessionfactory和mapper接口。
  • sqlsessionfactory主要是在@configuration注解的配置类中使用@bean注解的名为sqlsessionfactory的方法来配置;
  • mapper接口主要是通过在@configuration注解的配置类中结合@mapperscan注解来指定需要扫描获取mapper接口的包。
@configuration
@mapperscan("com.chenhao.mapper")
public class appconfig {

  @bean
  public datasource datasource() {
     return new embeddeddatabasebuilder()
            .addscript("schema.sql")
            .build();
  }
 
  @bean
  public datasourcetransactionmanager transactionmanager() {
    return new datasourcetransactionmanager(datasource());
  }
 
  @bean
  public sqlsessionfactory sqlsessionfactory() throws exception {
    //创建sqlsessionfactorybean对象
    sqlsessionfactorybean sessionfactory = new sqlsessionfactorybean();
    //设置数据源
    sessionfactory.setdatasource(datasource());
    //设置mapper.xml路径
    sessionfactory.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:mapper/*.xml"));
    // 设置mybatis分页插件
    pageinterceptor pageinterceptor = new pageinterceptor();
    properties properties = new properties();
    properties.setproperty("helperdialect", "mysql");
    pageinterceptor.setproperties(properties);
    sessionfactory.setplugins(new interceptor[]{pageinterceptor});
    return sessionfactory.getobject();
  }
}

对照spring-mybatis的方式,也就是对照beans.xml文件来看

<bean id="sqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean">
    <property name="configlocation" value="classpath:mybatis-config.xml"></property>
    <property name="datasource" ref="datasource" />
    <property name="mapperlocations" value="classpath:mapper/*.xml" />
</bean>

就对应着sqlsessionfactory的生成,类似于原生mybatis使用时的以下代码

sqlsessionfactory sqlsessionfactory = new sqlsessionfactorybuilder().build( resources.getresourceasstream("mybatis-config.xml"));

而usermapper代理对象的获取,是通过扫描的形式获取,也就是mapperscannerconfigurer这个类

<bean class="org.mybatis.spring.mapper.mapperscannerconfigurer">
    <property name="basepackage" value="com.chenhao.mapper" />
</bean>

对应着mapper接口的获取,类似于原生mybatis使用时的以下代码:

sqlsession session = sqlsessionfactory.opensession();
usermapper mapper = session.getmapper(usermapper.class);

接着我们就可以在service中直接从spring的beanfactory中获取了,如下

Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析

所以我们现在就主要分析下在spring中是如何生成sqlsessionfactory和mapper接口的

sqlsessionfactorybean的设计与实现

大体思路

  • mybatis-spring为了实现spring对mybatis的整合,即将mybatis的相关组件作为spring的ioc容器的bean来管理,使用了spring的factorybean接口来对mybatis的相关组件进行包装。spring的ioc容器在启动加载时,如果发现某个bean实现了factorybean接口,则会调用该bean的getobject方法,获取实际的bean对象注册到ioc容器,其中factorybean接口提供了getobject方法的声明,从而统一spring的ioc容器的行为。
  • sqlsessionfactory作为mybatis的启动组件,在mybatis-spring中提供了sqlsessionfactorybean来进行包装,所以在spring项目中整合mybatis,首先需要在spring的配置,如xml配置文件applicationcontext.xml中,配置sqlsessionfactorybean来引入sqlsessionfactory,即在spring项目启动时能加载并创建sqlsessionfactory对象,然后注册到spring的ioc容器中,从而可以直接在应用代码中注入使用或者作为属性,注入到mybatis的其他组件对应的bean对象。在applicationcontext.xml的配置如下:
<bean id="sqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean">
       // 数据源
       <property name="datasource" ref="datasource" />
       // mapper.xml的资源文件,也就是sql文件
       <property name="mapperlocations" value="classpath:mybatis/mapper/**/*.xml" />
       //mybatis配置mybatisconfig.xml的资源文件
       <property name="configlocation" value="classpath:mybatis/mybitas-config.xml" />
</bean>

接口设计与实现

sqlsessionfactory的接口设计如下:实现了spring提供的factorybean,initializingbean和applicationlistener这三个接口,在内部封装了mybatis的相关组件作为内部属性,如mybatisconfig.xml配置资源文件引用,mapper.xml配置资源文件引用,以及sqlsessionfactorybuilder构造器和sqlsessionfactory引用。

// 解析mybatisconfig.xml文件和mapper.xml,设置数据源和所使用的事务管理机制,将这些封装到configuration对象
// 使用configuration对象作为构造参数,创建sqlsessionfactory对象,其中sqlsessionfactory为单例bean,最后将sqlsessionfactory单例对象注册到spring容器。
public class sqlsessionfactorybean implements factorybean<sqlsessionfactory>, initializingbean, applicationlistener<applicationevent> {

  private static final logger logger = loggerfactory.getlogger(sqlsessionfactorybean.class);

  // mybatis配置mybatisconfig.xml的资源文件
  private resource configlocation;

  //解析完mybatisconfig.xml后生成configuration对象
  private configuration configuration;

  // mapper.xml的资源文件
  private resource[] mapperlocations;

  // 数据源
  private datasource datasource;

  // 事务管理,mybatis接入spring的一个重要原因也是可以直接使用spring提供的事务管理
  private transactionfactory transactionfactory;

  private properties configurationproperties;

  // mybatis的sqlsessionfactorybuidler和sqlsessionfactory
  private sqlsessionfactorybuilder sqlsessionfactorybuilder = new sqlsessionfactorybuilder();

  private sqlsessionfactory sqlsessionfactory;
  
  
  // 实现factorybean的getobject方法
  @override
  public sqlsessionfactory getobject() throws exception {
  
    //...

  }
  
  // 实现initializingbean的
  @override
  public void afterpropertiesset() throws exception {
  
    //...
    
  }
  // 为单例
  public boolean issingleton() {
    return true;
  }
}

我们重点关注factorybean,initializingbean这两个接口,spring的ioc容器在加载创建sqlsessionfactorybean的bean对象实例时,会调用initializingbean的afterpropertiesset方法进行对该bean对象进行相关初始化处理。

initializingbean的afterpropertiesset方法

大家最好看一下我前面关于spring源码的文章,有bean的生命周期详细源码分析,我们现在简单回顾一下,在getbean()时initializebean方法中调用initializingbean的afterpropertiesset,而在前一步操作populatebean中,以及将该bean对象实例的属性设值好了,initializingbean的afterpropertiesset进行一些后置处理。此时我们要注意,populatebean方法已经将sqlsessionfactorybean对象的属性进行赋值了,也就是xml中property配置的datasource,mapperlocations,configlocation这三个属性已经在sqlsessionfactorybean对象的属性进行赋值了,后面调用afterpropertiesset时直接可以使用这三个配置的值了。

// bean对象实例创建的核心实现方法
protected object docreatebean(final string beanname, final rootbeandefinition mbd, final @nullable object[] args)
        throws beancreationexception {

    // 使用构造函数或者工厂方法来创建bean对象实例
    // instantiate the bean.
    beanwrapper instancewrapper = null;
    if (mbd.issingleton()) {
        instancewrapper = this.factorybeaninstancecache.remove(beanname);
    }
    if (instancewrapper == null) {
        instancewrapper = createbeaninstance(beanname, mbd, args);
    }
    
    ...

    // 初始化bean对象实例,包括属性赋值,初始化方法,beanpostprocessor的执行
    // initialize the bean instance.
    object exposedobject = bean;
    try {

        // 1. instantiationawarebeanpostprocessor执行:
        //     (1). 调用instantiationawarebeanpostprocessor的postprocessafterinstantiation,
        //  (2). 调用instantiationawarebeanpostprocessor的postprocessproperties和postprocesspropertyvalues
        // 2. bean对象的属性赋值
        populatebean(beanname, mbd, instancewrapper);

        // 1. aware接口的方法调用
        // 2. beanpostprocess执行:调用beanpostprocessor的postprocessbeforeinitialization
        // 3. 调用init-method:首先initializingbean的afterpropertiesset,然后应用配置的init-method
        // 4. beanpostprocess执行:调用beanpostprocessor的postprocessafterinitialization
        exposedobject = initializebean(beanname, exposedobject, mbd);
    }
    

    // register bean as disposable.
    try {
        registerdisposablebeanifnecessary(beanname, bean, mbd);
    }
    catch (beandefinitionvalidationexception ex) {
        throw new beancreationexception(
                mbd.getresourcedescription(), beanname, "invalid destruction signature", ex);
    }

    return exposedobject;
}

如上,在populatebean阶段,datasource,mapperlocations,configlocation这三个属性已经在sqlsessionfactorybean对象的属性进行赋值了,调用afterpropertiesset时直接可以使用这三个配置的值了。那我们来接着看看afterpropertiesset方法

@override
public void afterpropertiesset() throws exception {
    notnull(datasource, "property 'datasource' is required");
    notnull(sqlsessionfactorybuilder, "property 'sqlsessionfactorybuilder' is required");
    state((configuration == null && configlocation == null) || !(configuration != null && configlocation != null),
              "property 'configuration' and 'configlocation' can not specified with together");

    // 创建sqlsessionfactory
    this.sqlsessionfactory = buildsqlsessionfactory();
}

sqlsessionfactorybean的afterpropertiesset方法实现如下:调用buildsqlsessionfactory方法创建用于注册到spring的ioc容器的sqlsessionfactory对象。我们接着来看看buildsqlsessionfactory

protected sqlsessionfactory buildsqlsessionfactory() throws ioexception {

    // 配置类
   configuration configuration;
    // 解析mybatis-config.xml文件,
    // 将相关配置信息保存到configuration
   xmlconfigbuilder xmlconfigbuilder = null;
   if (this.configuration != null) {
     configuration = this.configuration;
     if (configuration.getvariables() == null) {
       configuration.setvariables(this.configurationproperties);
     } else if (this.configurationproperties != null) {
       configuration.getvariables().putall(this.configurationproperties);
     }
    //资源文件不为空
   } else if (this.configlocation != null) {
     //根据configlocation创建xmlconfigbuilder,xmlconfigbuilder构造器中会创建configuration对象
     xmlconfigbuilder = new xmlconfigbuilder(this.configlocation.getinputstream(), null, this.configurationproperties);
     //将xmlconfigbuilder构造器中创建的configuration对象直接赋值给configuration属性
     configuration = xmlconfigbuilder.getconfiguration();
   } 
   
    //略....

   if (xmlconfigbuilder != null) {
     try {
       //解析mybatis-config.xml文件,并将相关配置信息保存到configuration
       xmlconfigbuilder.parse();
       if (logger.isdebugenabled()) {
         logger.debug("parsed configuration file: '" + this.configlocation + "'");
       }
     } catch (exception ex) {
       throw new nestedioexception("failed to parse config resource: " + this.configlocation, ex);
     }
   }
    
   if (this.transactionfactory == null) {
     //事务默认采用springmanagedtransaction,这一块非常重要,我将在后买你单独写一篇文章讲解mybatis和spring事务的关系
     this.transactionfactory = new springmanagedtransactionfactory();
   }
    // 为sqlsessionfactory绑定事务管理器和数据源
    // 这样sqlsessionfactory在创建sqlsession的时候可以通过该事务管理器获取jdbc连接,从而执行sql
   configuration.setenvironment(new environment(this.environment, this.transactionfactory, this.datasource));
    // 解析mapper.xml
   if (!isempty(this.mapperlocations)) {
     for (resource mapperlocation : this.mapperlocations) {
       if (mapperlocation == null) {
         continue;
       }
       try {
         // 解析mapper.xml文件,并注册到configuration对象的mapperregistry
         xmlmapperbuilder xmlmapperbuilder = new xmlmapperbuilder(mapperlocation.getinputstream(),
             configuration, mapperlocation.tostring(), configuration.getsqlfragments());
         xmlmapperbuilder.parse();
       } catch (exception e) {
         throw new nestedioexception("failed to parse mapping resource: '" + mapperlocation + "'", e);
       } finally {
         errorcontext.instance().reset();
       }

       if (logger.isdebugenabled()) {
         logger.debug("parsed mapper file: '" + mapperlocation + "'");
       }
     }
   } else {
     if (logger.isdebugenabled()) {
       logger.debug("property 'mapperlocations' was not specified or no matching resources found");
     }
   }

    // 将configuration对象实例作为参数,
    // 调用sqlsessionfactorybuilder创建sqlsessionfactory对象实例
   return this.sqlsessionfactorybuilder.build(configuration);
}

buildsqlsessionfactory的核心逻辑:解析mybatis配置文件mybatisconfig.xml和mapper配置文件mapper.xml并封装到configuration对象中,最后调用mybatis的sqlsessionfactorybuilder来创建sqlsessionfactory对象。这一点相当于前面介绍的原生的mybatis的初始化过程。另外,当配置中未指定事务时,mybatis-spring默认采用springmanagedtransaction,这一点非常重要,请大家先在心里做好准备。此时sqlsessionfactory已经创建好了,并且赋值到了sqlsessionfactorybean的sqlsessionfactory属性中。

factorybean的getobject方法定义

factorybean:创建某个类的对象实例的工厂。

spring的ioc容器在启动,创建好bean对象实例后,会检查这个bean对象是否实现了factorybean接口,如果是,则调用该bean对象的getobject方法,在getobject方法中实现创建并返回实际需要的bean对象实例,然后将该实际需要的bean对象实例注册到spring容器;如果不是则直接将该bean对象实例注册到spring容器。

sqlsessionfactorybean的getobject方法实现如下:由于spring在创建sqlsessionfactorybean自身的bean对象时,已经调用了initializingbean的afterpropertiesset方法创建了sqlsessionfactory对象,故可以直接返回sqlsessionfactory对象给spring的ioc容器,从而完成sqlsessionfactory的bean对象的注册,之后可以直接在应用代码注入或者spring在创建其他bean对象时,依赖注入sqlsessionfactory对象。

@override
public sqlsessionfactory getobject() throws exception {
    if (this.sqlsessionfactory == null) {
      afterpropertiesset();
    }
    // 直接返回sqlsessionfactory对象
    // 单例对象,由所有mapper共享
    return this.sqlsessionfactory;
}

总结

由以上分析可知,spring在加载创建sqlsessionfactorybean的bean对象实例时,调用sqlsessionfactorybean的afterpropertiesset方法完成了sqlsessionfactory对象实例的创建;在将sqlsessionfactorybean对象实例注册到spring的ioc容器时,发现sqlsessionfactorybean实现了factorybean接口,故不是sqlsessionfactorybean对象实例自身需要注册到spring的ioc容器,而是sqlsessionfactorybean的getobject方法的返回值对应的对象需要注册到spring的ioc容器,而这个返回值就是sqlsessionfactory对象,故完成了将sqlsessionfactory对象实例注册到spring的ioc容器。创建mapper的代理对象我们下一篇文章再讲