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

mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析

程序员文章站 2022-06-11 16:26:56
mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?文章带你一步一步解析这个过程 ......

序言

在开始正文之前,首先解释dao接口和xml文件里的sql是如何一一对应的?

一句话讲完就是:mybatis 会先解析这些xml 文件,通过 xml 文件里面的命名空间 (namespace)跟dao 建立关系;然后 xml 中的每段 sql 会有一个id 跟 dao 中的接口进行关联。

那么问题来了: "如果 我有两个这个xml 文件 都跟这个dao 建立关系了,那不是就是冲突了?"

带着这个疑问我们就要开始下面的正题了!

一、初始化

首先我们要知道每个基于 mybatis 的应用都是以一个 sqlsessionfactory 的实例为中心的,sqlsessionfactory 的实例可以通过 sqlsessionfactorybuilder 获得。

sqlsessionfactory是一个接口,它里面其实就两个方法:opensessiongetconfiguration

其中,opensession方法是为了获取一个sqlsession对象,完成必要数据库增删改查功能。但是,sqlsessionfactory属性太少了,所以需要getconfiguration的配合;来配置mapper映射文件、sql参数、返回值类型、缓存等属性。

/**
 * creates an {@link sqlsession} out of a connection or a datasource
 * 
 * @author clinton begin
 */
public interface sqlsessionfactory {

  sqlsession opensession();

  sqlsession opensession(boolean autocommit);
  sqlsession opensession(connection connection);
  sqlsession opensession(transactionisolationlevel level);

  sqlsession opensession(executortype exectype);
  sqlsession opensession(executortype exectype, boolean autocommit);
  sqlsession opensession(executortype exectype, transactionisolationlevel level);
  sqlsession opensession(executortype exectype, connection connection);

  configuration getconfiguration();

}

可以看到getconfiguration是属于configuration类的一个方法。你可以把它当成一个配置管家。mybatis所有的配置信息都维持在configuration对象之中,基本每个对象都会持有它的引用。

但日常开发中我们都是将mybatis与spring一起使用的,所以把实例化交给spring处理。

因此我们可以看下org.mybatis.spring.sqlsessionfactorybean,它实现了initializingbean接口。这说明,在这个类被实例化之后会调用到afterpropertiesset()。它只有一个方法

public void afterpropertiesset() throws exception {
    this.sqlsessionfactory = buildsqlsessionfactory();
}

而这个afterpropertiesset方法只有一个动作,就是buildsqlsessionfactory。它可以分为两部分来看:

  • 1、从配置文件的property属性中加载各种组件,解析配置到configuration中
  • 2、加载mapper文件,解析sql语句,封装成mappedstatement对象,配置到configuration中。

二、mapper接口方法是怎样被调用到的?

大致有如下两种方式:

  • mybatis提供的api

使用mybatis提供的api进行操作,通过获取sqlsession对象,然后根据statement id 和参数来操作数据库。

string statement = "com.mmzsblog.business.dao.membermapper.getmemberlist";
list<member> result = sqlsession.selectlist(statement);
  • mapper接口

定义mapper接口,并在里面定义一系列业务数据操作方法。在service层通过注入mapper属性,调用其方法就可以执行数据库操作。就像下面这样

public interface membermapper { 
    list<member> getmemberlist();
}

@service
public class memberserviceimpl implements memberservice{
    @resource
    private membermapper membermapper;
    
    @override
    public list<member> getmemberlist() {
        return membermapper.getmemberlist();
    }
}

那么,membermapper 只是个接口,并没有任何实现类。我们在调用它的时候,它是怎样最终执行到我们的sql语句的呢?

三、mapper接口的代理创建过程

3.1、首先我们会配置需要扫描的基本包路径

通过注解的方式配置:

@mapperscan({"com.mmzsblog.business.dao"})

或者xml的方式配置:

<bean class="org.mybatis.spring.mapper.mapperscannerconfigurer">
    <property name="basepackage" value="com.mmzsblog.business.dao" />
    <property name="sqlsessionfactorybeanname" value="sqlsessionfactory"></property>
</bean>

3.2、开始扫描

我们来到org.mybatis.spring.mapper.mapperscannerconfigurer这个类,可以看到它实现了几个接口。

其中的重点是beandefinitionregistrypostprocessor。它可以动态的注册bean信息,方法为postprocessbeandefinitionregistry()

    public void postprocessbeandefinitionregistry(beandefinitionregistry registry) {
        if (this.processpropertyplaceholders) {
            this.processpropertyplaceholders();
        }
        
        // 创建classpath扫描器,设置属性,然后调用扫描方法
        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);
        // 创建classpath扫描器,设置属性,然后调用扫描方法
        scanner.registerfilters();
        scanner.scan(stringutils.tokenizetostringarray(this.basepackage, ",; \t\n"));
    }

classpathmapperscanner继承自spring中的类classpathbeandefinitionscanner,所以它的scan方法会调用到父类classpathbeandefinitionscanner的scan方法,

public class classpathbeandefinitionscanner extends classpathscanningcandidatecomponentprovider {
    ……
    public int scan(string... basepackages) {
        // 
        int beancountatscanstart = this.registry.getbeandefinitioncount();
        this.doscan(basepackages);
        if (this.includeannotationconfig) {
            annotationconfigutils.registerannotationconfigprocessors(this.registry);
        }

        return this.registry.getbeandefinitioncount() - beancountatscanstart;
    }
    ……
}    

而在父类的scan方法中又调用到子类classpathmapperscanner重写的doscan方法。

public class classpathmapperscanner extends classpathbeandefinitionscanner {
    ……
    public set<beandefinitionholder> doscan(string... basepackages) {
        set<beandefinitionholder> beandefinitions = super.doscan(basepackages);
        if (beandefinitions.isempty()) {
            this.logger.warn("no mybatis mapper was found in '" + arrays.tostring(basepackages) + "' package. please check your configuration.");
        } else {
            this.processbeandefinitions(beandefinitions);
        }

        return beandefinitions;
    }
    ……
}    

此处super.doscan(basepackages)是spring中的方法,就不贴代码多叙述了,想详细了解的话,可以自己翻一下源码哦。

3.3、bean注册完成并创建sqlsession代理

并且经过上面这些步骤,此时已经扫描到了所有的mapper接口,并将其注册为beandefinition对象。而注册的时候就是用到了上面doscan方法中的processbeandefinitions方法。

public class classpathmapperscanner extends classpathbeandefinitionscanner {
    ……
    // 设置beanclass
    private mapperfactorybean<?> mapperfactorybean = new mapperfactorybean();
    ……
    
    private void processbeandefinitions(set<beandefinitionholder> beandefinitions) {
        iterator var3 = beandefinitions.iterator();

        while(var3.hasnext()) {
            beandefinitionholder holder = (beandefinitionholder)var3.next();
            genericbeandefinition definition = (genericbeandefinition)holder.getbeandefinition();
            if (this.logger.isdebugenabled()) {
                this.logger.debug("creating mapperfactorybean with name '" + holder.getbeanname() + "' and '" + definition.getbeanclassname() + "' mapperinterface");
            }
            // 将mapper接口的名称添加到构造参数
            definition.getconstructorargumentvalues().addgenericargumentvalue(definition.getbeanclassname());
            // 设置beandefinition的class
            definition.setbeanclass(this.mapperfactorybean.getclass());
            // 添加属性addtoconfig
            definition.getpropertyvalues().add("addtoconfig", this.addtoconfig);
            boolean explicitfactoryused = false;
            // 添加属性sqlsessionfactory
            if (stringutils.hastext(this.sqlsessionfactorybeanname)) {
                definition.getpropertyvalues().add("sqlsessionfactory", new runtimebeanreference(this.sqlsessionfactorybeanname));
                explicitfactoryused = true;
            } else if (this.sqlsessionfactory != null) {
                definition.getpropertyvalues().add("sqlsessionfactory", this.sqlsessionfactory);
                explicitfactoryused = true;
            }

            if (stringutils.hastext(this.sqlsessiontemplatebeanname)) {
                if (explicitfactoryused) {
                    this.logger.warn("cannot use both: sqlsessiontemplate and sqlsessionfactory together. sqlsessionfactory is ignored.");
                }

                definition.getpropertyvalues().add("sqlsessiontemplate", new runtimebeanreference(this.sqlsessiontemplatebeanname));
                explicitfactoryused = true;
            } else if (this.sqlsessiontemplate != null) {
                if (explicitfactoryused) {
                    this.logger.warn("cannot use both: sqlsessiontemplate and sqlsessionfactory together. sqlsessionfactory is ignored.");
                }

                definition.getpropertyvalues().add("sqlsessiontemplate", this.sqlsessiontemplate);
                explicitfactoryused = true;
            }

            if (!explicitfactoryused) {
                if (this.logger.isdebugenabled()) {
                    this.logger.debug("enabling autowire by type for mapperfactorybean with name '" + holder.getbeanname() + "'.");
                }

                definition.setautowiremode(2);
            }
        }
    }
    ……
}    

处理的过程相对比较简单,只是往beandefinition对象中设置了一些属性。例如:

  • 设置beanclass

设置beandefinition对象的beanclass为mapperfactorybean<?>。这就相当于使用membermapper注册时:当前的mapper接口在spring容器中,beanname是membermapper,beanclass是mapperfactorybean.class。故在spring的ioc初始化的时候,实例化的对象就是mapperfactorybean对象。

  • 设置sqlsessionfactory属性

为beandefinition对象添加属性sqlsessionfactory,是为了beandefinition对象设置propertyvalue的时候,方便调用到setsqlsessionfactory()。

mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析

3.4、创建sqlsession代理类

最终在setsqlsessionfactory这个方法里,sqlsession获取到的是sqlsessiontemplate实例。而在sqlsessiontemplate对象中,主要包含sqlsessionfactory和sqlsessionproxy,而sqlsessionproxy实际上是sqlsession接口的代理对象。实际调用的是代理类的invoke方法。

public class mapperproxy<t> implements invocationhandler, serializable {
  ……
  @override
  public object invoke(object proxy, method method, object[] args) throws throwable {
    try {
      if (object.class.equals(method.getdeclaringclass())) {
        return method.invoke(this, args);
      } else if (isdefaultmethod(method)) {
        return invokedefaultmethod(proxy, method, args);
      }
    } catch (throwable t) {
      throw exceptionutil.unwrapthrowable(t);
    }
    final mappermethod mappermethod = cachedmappermethod(method);
    return mappermethod.execute(sqlsession, args);
  }
  ……
}  

3.5、小结

mapper接口的代理创建过程大致如下:

  • 1、扫描mapper接口基本包路径下的所有对象,将其注册为beandefinition对象
  • 2、设置beandefinition的对象的beanclass和sqlsessionfactory属性(而其中获取beandefinition对象的时候,调用其工厂方法getobject,返回mapper接口的代理类)
  • 3、设置sqlsessionfactory属性的时候,会调用sqlsessiontemplate的构造方法,创建sqlsession接口的代理类

最后我们在service层,通过

@resource 
private membermapper memberdao;

注入属性的时候,返回的就是代理类。执行memberdao的方法的时候,实际调用的也是代理类的invoke方法。

四、回答最开始的问题

mybatis在初始化sqlsessionfactorybean的时候,找到配置需要扫描的基本包路径去解析里面所有的xml文件。重点就在如下两个地方:

1、创建sqlsource

mybatis会把每个sql标签封装成sqlsource对象。然后根据sql语句的不同,又分为动态sql和静态sql。其中,静态sql包含一段string类型的sql语句;而动态sql则是由一个个sqlnode组成。
mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析

2、创建mappedstatement

xml文件中的每一个sql标签就对应一个mappedstatement对象,这里面有两个属性很重要。

  • id

全限定类名+方法名组成的id。

  • sqlsource

当前sql标签对应的sqlsource对象。
创建完mappedstatement对象,会将它缓存到configuration#mappedstatements中。

前面初始化中提到的configuration对象,我们知道它就是mybatis中的配置大管家,基本所有的配置信息都维护在这里。

例如下面这样一段代码:

<!-- namespace的值就是全限定类名 -->
<mapper namespace="com.java.mmzsblog.dao.membermapper">
    ……
    <!-- select标签中id的值就是方法名,它和全限定类中的方法名是对应的 -->
    <select id="getmemberbyid" resulttype="com.java.mmzsblog.entity.member">
        select * from member
        <where>
            <if test="memberid!=null">
                and member_id=#{memberid}
            </if>
        </where>
    </select>
    ……
</mapper>    

把所有的xml都解析完成之后,configuration就包含了所有的sql信息。然后解析完成的xml大概就是这样了:

mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析

看到上面的图示,聪明如你,也许就大概知道了。当我们执行mybatis方法的时候,就通过全限定类名+方法名找到mappedstatement对象,然后解析里面的sql内容,执行即可。