mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析
序言
在开始正文之前,首先解释dao接口和xml文件里的sql是如何一一对应的?
一句话讲完就是:mybatis 会先解析这些xml 文件,通过 xml 文件里面的命名空间 (namespace)跟dao 建立关系;然后 xml 中的每段 sql 会有一个id 跟 dao 中的接口进行关联。
那么问题来了: "如果 我有两个这个xml 文件 都跟这个dao 建立关系了,那不是就是冲突了?"
带着这个疑问我们就要开始下面的正题了!
一、初始化
首先我们要知道每个基于 mybatis 的应用都是以一个 sqlsessionfactory 的实例为中心的,sqlsessionfactory 的实例可以通过 sqlsessionfactorybuilder 获得。
但sqlsessionfactory
是一个接口,它里面其实就两个方法:opensession
、getconfiguration
其中,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()。
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组成。
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方法的时候,就通过全限定类名+方法名
找到mappedstatement对象,然后解析里面的sql内容,执行即可。