mybatis全局配置执行流程源码分析
轻量级的框架mybatis
mybatis在日常的开发中,使用的算是比较多的,它的底层也还是采用了JDBC的实现原理,但是总要的是它是一个ORM框架,就是对象关系数据映射框架,你可以简单理解就是你的sql语句写在配置文件中,然后mybatis启动的时候会解析配置文件,拿到你的操作信息,包括操作ID也就是方法ID,还有一些设置,比如是否开启缓存,操作的参数返回信息,sql语句等,放入一个缓存中,当你执行的数据的操作的时候,它通过你传入的ID从缓存中找到指定的方法信息,然后拿出sql,进行一系列的骚操作,最终 调用JDBC的数据库操作API,得到数据库返回的数据,然后进行对象数据映射,最终封装到返回对象中。其实它的工作原理从宏观上来说是比较简单的,也是比较好理解的,涉及的东西不多,但是五脏俱全,而且是轻量级的,好用,使用也比较简单,比较符合大众的需求,这也就是为什么现在企业都原理使用的原因;比如说当你要使用一种框架来实现你的项目需求的时候,你往往对这种框架了解过后的第一反应就是集成简单,使用方便,而不是使用了这种框架过后还依赖其他框架,而其他框架的使用又依赖某种框架,就是框架的耦合比较重,那么你可能光整框架都整很久,而且总体态臃肿,所以现在的微服务架构好是好,但是我的理解就是它永远存在问题,存在问题的同时又在解决问题,所以整个生态中都是围绕到微服务这个理念在不停的在出解决方案,仿佛就是永无止境一样。比如像传统的应用中,像锁、事物等都可以在一个应用中而且很好的解决,出现了微服务过后,不得不考虑分布式锁分布式事物等问题,所以又有框架可以实现这种功能,它的出现当然能解决很多问题,但是同时也带来了其他问题,而针对这些问题又有新的技术来解决这个问题,也就类似于我们生活一样,每天都不停的在解决问题和发现问题。好了,这里不多说了,所以mybatis本身而言是一个轻量级的而且使用能够非常简单,也能和spring完美整合的,所以了解的它的工作原理和底层原理是很有必要的,今天这里就简单来分析下mybatis的底层工作原理和源码。
全局配置文件解析
在上一篇笔记中大概分析了mybatis是如何解析配置文件的,这里就详细分析下mybatis解析配置文件的全过程,当然这里就挑几个比较重要的来分下,其他的我基本上没有使用过,就不分析了;看源码一定要注意的就是你是框架的使用者不是框架的研发者,所以切记不要专到一个死角出不来,那么这样你用于没有办法看懂框架源码,我们要有一个思想就是,mybatis没看过源码,但是用过,心里有了一个总体的流程,带着这种思想去看,去证明你的猜想,而不是钻到一坨代码中出不来,所以这里我也只是分析比较重要的部分;
看下面的几行代码:
InputStream input = Resources.getResourceAsStream("MyBatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
Object one = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println(one);
sessionFactory是mybatis中一个比较重要的session工厂类,就是这个sessionFactory可以打开一个sessionn区操作mybatis的CRUD操作,如果你打开的session在系统中只有一个,并且永远不会关闭,那么它也就一直存在,如果说你关闭了session,那么要操作数据库,还的重新open一个。所以说sessionFactory是非常重要的,而sessionFactory也是去加载了全局配置文件MyBatis.xml的,所以这里要分析的就是怎么来构建的sessionFactory。进入源码过后,有些是上一篇已经分析了,所以这里就直接看XmlConfigBuilder中的parse方法。
public Configuration parse() {
/**
* 这个parsed代表的意思就是说mybatis上下文配置文件是否已经被解析过了,如果在一个线程中
* 被多次解析,那么就要报错,因为不能创建多个Configuration对象,比如像我们的dataSouce,相同
* 的数据库连接可能在一个系统中存在多个吗,显然是没有必要的,所以这里
* 用了一个parsed参数来控制,万一你调用了多次的parse方法,那么肯定有一次是失败的
* 简单来说就是一个XMLConfigBuilder对象只能调用一次parse方法进行解析配置初始化Configuration
*/
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
//如果没有被解析过,下面马上就要解析了,所以设置是否解析过的参数为true,代表已经解析过
parsed = true;
//这个就是核心的解析方法,主要解析configuration标签
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
mybtis的核心全局配置文件中最核心的就在配置文件中的configuration这个标签,所有的核心都在里面,所以这个parseConfiguration肯定是核心所在;
parseConfiguration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
/**
* 这里是解析的properties,这个是干什么的呢,像在一般的系统中,mybatis的配置文件中常常
* 会引入一个必然jdbc.properties文件,所以这里就是解析引入了外部的properties文件的
* 解析了jdbc.properties文件过后,会将mybatis中使用了properties中的参数进行替换
*
* 下面的代码其实都是解析mybatis的配置文件,也就是configuration标签的
* 从下面的代码可以看出这个配置文件的标签是有顺序的,不可以乱写顺序的,比如说properties
* 这个标签必须要在最开始写,如果写到中间或者最后是要报错的,因为代码就是这样写的
* 但是从宏观来看,也是必须的,你首先要将引入的文件解析到变量表中,然后下面引用它的变量才能拿的到
*/
propertiesElement(root.evalNode("properties"));
/**
* 解析settigns,解析出的settings会放入settings对象中
* 最后设置到全局配置中
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
/**
* 解析typeAliases
*/
typeAliasesElement(root.evalNode("typeAliases"));
/**
* 解析plugins
*/
pluginElement(root.evalNode("plugins"));
/**
* 解析objectFactory
*/
objectFactoryElement(root.evalNode("objectFactory"));
/**
* 解析objectWrapperFactory
*/
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
/**
* 解析reflectorFactory
*/
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
/**
* 解析environments,这个就比较重要了,这里就是去解析我们的数据源那些
*/
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
/**
* 这里就是解析我们的mappers,mappers就是配置了项目中的mybatis的sql配置文件
*
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这上面的方法就是解析configuration标签下面的所有的子标签,也是mybatis的核心所在的一些配置,先从properties开始,properties是mybatis可以引入外部配置文件的一种 方式;而且properties在configuration下面只能是第一个,它与约束的,只能是第一个,这个约束也是配合代码来实现的,因为引入的外部配置文件是要设置参数值,如果不是第一个,别的配置都已经解析了,要你何用。
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//这里是获取到在 <properties>中配置的<property>标签中的内容
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
//resource和url不能同时为空至少要有一个有值
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
//解析,放入defaults,如果在<properties>自定义的有<property>,如果有文件中的key重名,会被覆盖
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
官网的用法如下:
其实用法就特别简单,比如我们写一个jdbc.properties的配置文件,将数据库的相关信息都配置到这个配置文件中,然后在全局配置文件中导入;
jdbc.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8
username=root
password=111111
在全局配置文件中使用
<configuration>
<properties resource="jdbc.properties" />
<!-- autoMappingBehavior should be set in each test case -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<package name="com.mybatis.example.entity" />
</typeAliases>
<plugins>
<plugin interceptor="com.mybatis.example.plugs.MyPlugs">
<property name="type" value="123"/>
<property name="name" value="bml"/>
<property name="foo" value="567"/>
</plugin>
<plugin interceptor="com.mybatis.example.plugs.ParameterHandlerPlug"></plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
</transactionManager>
<!-- 这里的type代表的就是数据域的类型,通过类型找到指定的数据源对象-->
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapping/UserMapper.xml" />
</mappers>
</configuration>
settings配置为解析
settings这个也是必须要在前面进行引入,mybtis中有很多参数可以进行控制的,如果你没有设置,那么mybatis会有一个默认的值去配置,当然这个参数设置了肯定是有效果,有效果就代表有一个类或者相关的方法去实现你的这个配置代表的含义,比如mybatis中的sql执行器有三种,简单、复用和批量,那么mybatis默认的是简单的方式,而如果你想用复用或者批量的模式,你就可以在settings中进行配置,我们先看下源码实现:
在解析了settings过后,会调用settingsElement这个方法进行全局配置,比如上图中的二级缓存的开启和执行器的配置,比如说如果我都不配置,那么我看下获取的执行器
但是如果我在全局配置文件中配置了我要使用的执行器,配置如下:
debug看下:
执行器就会使用了复用的执行器,通过简单配置就可以达到自己想要配置的,这就是mybatis自身提高的扩展吧
别名解析
别名这个怎么说呢,看个人的使用情况,我觉得也就那样,你觉得好就好,觉得鸡嘞就不用,一般我们在mapper配置文件中需要制定一下你的实体的全限定名,如果使用了别名就不需要制定了,制定别名可以自定义制定,也可以是使用包的方式制定,如果使用包的方式制定,那么mybatis会扫描这个包下面的所有类,如果你的类上没有加@Alias注解,那么就使用类名作为别名,如果指定了,就使用你制定的名字作为别名使用,先看源码:
/**
* 在MyBatis中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输
* 入别名即可,无需再把全限定的类名写出来。在MyBatis中,我们有两种方式进行别名配
* 置。第一种是仅配置包名,让MyBatis去扫描包中的类型,并根据类型得到相应的别名。
* 这种方式可配合Alias注解使用,即通过注解为某个类配置别名,而不是让MyBatis按照
* 默认规则生成别名。这种方式的配置如下:
* 1<typeAliases>
* 2<packagename="domain.blog"/>
* 3</typeAliases>
* @param parent
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//以package命名的 优先级最高,最先读取,会将这个package下面的所有类都
//扫描一遍,会将所有的类都生成一个别名,key=类的名字 value=类的class
//如果加了@Alias注解,那么会获取@Alias设置的别名
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
别名使用方式一
全局配置文件:
<typeAliases>
<typeAlias type="com.mybatis.example.entity.User" alias="u1"/>
</typeAliases>
mapper配置文件
<select id="queryUser" resultMap="all" resultType="u1" >
SELECT * FROM USER where id=#{id}
</select>
别名使用方式二
不指定别名的方式:
全局配置文件
<typeAliases>
<package name="com.mybatis.example.entity"/>
</typeAliases>
mapper配置文件:
<select id="queryUser" resultMap="all" resultType="User" >
SELECT * FROM USER where id=#{id}
</select>
直接使用类名作为别名,这种方式,我个人建议,如果项目中设计的时候所有的数据库映射实体都在这个包下面,那么可以采用这种方式,很简便,而且自动识别,并且以类的名字作为别名,这样看上去也很好理解。
指定个性化的别名:
这种方式的全局配置不变,也是使用package的方式,但是如果说在配置的包下面需要指定一个实体类的别名,不想使用默认的,那么你可以使用@Alias来指定,比如:
package com.mybatis.example.entity;
import org.apache.ibatis.type.Alias;
@Alias("uu")
public class User {
......
mapper配置文件:
<select id="queryUser" resultMap="all" resultType="uu" >
SELECT * FROM USER where id=#{id}
</select>
所以使用是很简单的
插件解析
在mybatis中还提供了一种的新的功能,就是插件机制,在官方上所写的就是插件,我喜欢将它理解成一个拦截器,和spring的aop很像,mybatis提供了几种插件机制,分别在执行器的创建、创建参数处理器、创建声明处理器、创建结果集对象的时候进行调用,所以如果我们要编写mybatis的插件,都可以在这4种情况中进行拦截,如果你配置了相关的插件并且实现了,那么在创建这些处理器的时候会产生代理对象,而真正返回的是就是代理对象,你在执行操作的时候,就可以被拦截,所以我说mybatis的插件机制其实就是代理模式实现的拦截器,拦截你定义好的处理器中的方法,从而达到嵌入插件实现你的个性化功能,使用场景有很多,比如像统计每个sql的执行时间,从而统计出那条sql执行时间比较长从而优化sql的执行,还比如说现在使用比较多的分页插件,我们这里先看下mybatis中的插件机制是如何实现的
/**
* 解析plugs标签
* 插件,其实这里也可以叫做拦截器,就是在一些操作
* @param parent
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
//得到plug中设置的属性property,然后封装成一个Properties对象,最后调用Plug中的setProperties,也就是说
//实现了插件的类可以得到配置的属性
Properties properties = child.getChildrenAsProperties();
//得到interceptor过后,加入过滤器
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//将插件添加到Intercept中去
configuration.addInterceptor(interceptorInstance);
}
}
}
比如我这里配置了两个插件,分别是拦截了执行器和参数处理器的操作,先看下在mybatis中是如何配置的,还是在全局配置文件中进行配置
<plugins>
<plugin interceptor="com.mybatis.example.plugs.MyPlugs">
<property name="type" value="123"/>
<property name="name" value="bml"/>
<property name="foo" value="567"/>
</plugin>
<plugin interceptor="com.mybatis.example.plugs.ParameterHandlerPlug"></plugin>
</plugins>
在第一个插件中我还是设置了一些属性,那么在插件的实现中也可以得到这些属性
@Intercepts(value={
@Signature( type= Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPlugs implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object object = invocation.proceed();
System.out.println("我的插件");
return object;
}
@Override
public void setProperties(Properties properties) {
properties.forEach((k,v)->{
System.out.printf("key=%s,value=%s\n", k,v);
});
}
}
@Intercepts(value = {
@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})
})
public class ParameterHandlerPlug implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("---------------------------------参数插件----------------------");
DefaultParameterHandler handler = (DefaultParameterHandler) invocation.getTarget();
System.out.println("parameterObject:"+handler.getParameterObject());
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
System.out.println("数据库预处理对象:"+ ps);
System.out.println("---------------------------------参数插件----------------------");
return invocation.proceed();
}
}
上面是我自定义的两个插件,插件中需要定义@Intercepts,这个是必须的,mybatis需要知道你要拦截的那种类型,这种类型下面的那个方法需要被拦截,也就是方法签名@Signature
type:具体拦截的处理器
method:处理器中的方法
args:方法中的参数类型
这样配置好了以后就能够让插件生效了
比如上面的运行
MyPlugs 是拦截的执行器,所以在执行器执行sql之前会被拦截,而ParameterHandlerPlug 是参数处理器是,是在设置参数的时候会被拦截,使用时非常简单的;上面分析了配置了拦截器到全局配置文件文件中以后,mybatis会读取这个配置,将拦截器读取出来,然后添加到拦截器列表中,这里可以看下添加的逻辑:
在Configuration的方法中进行添加
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
所以所有的拦截器都在interceptorChain这个对象中,而interceptorChain是一个拦截器对象
InterceptorChain
/**
* @author Clinton Begin
*/
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
/**
* 这个是mybatis的插件执行的地方,mybatis有几个地方会执行这个插件呢?
* 从源码里面我们可以知道插件执行的地方有:
* 1.创建执行器的时候:Configuration.newExecutor方法;
* 2.创建参数处理器的时候:Configuration.newParameterHandler方法;
* 3.创建sql处理声明的时候:Configuration.newStatementHandler方法;
* 4.创建处理结果集的时候:Configuration.newResultSetHandler方法。
* 所以说我们编写的插件可以从这几个地方去做拦截
* * 当在上面的4中情况的执行过程中,会来根据目标的类,比如创建的4大接口的过程中会来调用,
* * 如果目标类上在插件中满足拦截的条件,就会返回一个代理对象,而当最终在调用这4大接口的方法过程中
* * 如果调用的方法满足拦截的条件,会调用这里的invoke方法,也就是代理调用了
* @param target
* @return
*/
public Object pluginAll(Object target) {
//获取所有的拦截器,执行拦截器的方法
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
在上面的4中情况下,mybatis会来调用pluginAll方法,看具体的处理器,比如说Executor执行器有没有添加执行器,如果添加了拦截器,那么会为这个执行器创建一个代理类,代理的对象就是传进来的Executor执行器的实例对象。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
这里面提供了两个默认的方法,一个plugin和一个setProperties方法,都是给子类去使用的,其中plugin是去搜索针对target,是否有实现的插件,如果有,就创建一个代理对象,代理target返回而setProperties是将你配置在插件中的属性放给你,也就是你可以拿到配置的参数信息
/**
* @author Clinton Begin
*/
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
/**
* mybatis的四大插件
* 也就是插件的调用地方
* 1.创建执行器的时候:Configuration.newExecutor方法;
* 2.创建参数处理器的时候:Configuration.newParameterHandler方法;
* 3.创建sql处理声明的时候:Configuration.newStatementHandler方法;
* 4.创建处理结果集的时候:Configuration.newResultSetHandler方法。
* 所以四大插件就是:
* Executor 拦截执行器的方法
* ParameterHandler 拦截参数的处理
* ResultSetHandler 拦截结果集的处理
* StatementHandler 拦截Sql语法构建的处理
*
*
* @param target
* @param interceptor
* @return
*/
public static Object wrap(Object target, Interceptor interceptor) {
//这里是去获取插件中的签名,也就是判断当前的插件是否满足执行条件
//这里的signatureMap得到的是插件类型对应的插件列表,必须是要匹配到指定的方法和参数才满足拦截的条件
/**
* 也就是说signatureMap返回的是比如Map<Executor.class,Set<Method>>
* 而target是实现了Executor的实现类
*/
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//设置判断下实现类是否实现了拦截者的接口
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
/**
* 如果是就创建一个代理,这个代理Plugin是一个jdk的动态代理,
* 最终会调用Plugin中的invoke方法
*/
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* 插件的最终调用方法
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
//这里调用自定义插件的方法,最终自定义插件在调用Invocation.proceed方法完成后续方法的调用
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
/**
* 这里是获取所有的签名集合
*
* @param interceptor
* @return
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[0]);
}
}
Plug这个类就是具体的实现,它也是一个代理类,如果满足代理的创建,那么如果你写的插件是拦截Executor中的query方法,那么在执行查询的时候会来调用Plug中的invoke方法,从而找到你的插件,调用你插件的拦截方法;插件的使用时非常简单的。
环境配置的解析Environment
这个在上一篇笔记中已经介绍了,这里就做做分析了
Mapper配置解析
mapper配置解析是比较重要的一环了,这个在第一节笔记中也写了,但是有个点我们没有分析到,我们知道mappers中可以配置
package、url、resource,其中url和resoure只是来源不一样,而package配置就不一样了,并且package配置的解析优先级是最高的,
使用场景就是如果package的配置适用于注解的模式,而resource这种适用于mapper配置文件的这种,但是不管是那种模式,最终都会调用
MapperRegistry中的addMapper添加到一个map knownMappers中;
package的模式:
这种模式就是配置一个包的路径,mybatis会在这个包下面找到所有属于Object类型的calss,然后添加到Mapper中,当然了添加这个,还需要添加声明到缓存中,并且会处理它的注解信息。
url或者resource的方式:
这种方式是要去解析mapper的配置文件,将里面的所有的标签进行解析,先添加到声明的缓存中,然后还是要添加mapper的
为什么呢?看下面的程序
InputStream input = Resources.getResourceAsStream("MyBatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
Object one = sqlSession.selectOne("com.mybatis.example.mapper.UserMapper.queryUser", 1);
System.out.println(one);
sqlSession.getMapper(UserMapper.class).queryUser(1);
这里面的其实sqlSessionn和getMapper调用都是同一个方法,通过getMapper调用,其实是调用的代理中的方法,代理中的方法最终也会来调用selectOne的方法,代码太多了,我这里就不贴了,有兴趣可以去看MapperMethod中execue方法
org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
》org.apache.ibatis.session.Configuration#getMapper
》org.apache.ibatis.binding.MapperRegistry#getMapper
》org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy<T>)
》org.apache.ibatis.binding.MapperProxy#invoke
》org.apache.ibatis.binding.MapperProxy#cachedInvoker
》org.apache.ibatis.binding.MapperMethod#execute
代码跟下去就会找到selectOne的方法
所以添加Mapper的逻辑是在哪里的?还是在解析的parse方法中
public void parse() {
//isResourceLoaded去重的功能 ,代表已经解析过的资源文件
if (!configuration.isResourceLoaded(resource)) {
//解析mapper,将配置中的所有标签都解析出来
/**
* 比如sql标签,resultMap标签,parameterMap、select|insert|delete|update
* 等标签都解析出来创建相应的对象放入到configuration中
*/
configurationElement(parser.evalNode("/mapper"));
//添加进去,表示已经被解析过了,这里添加的是resource,就是mapper的xml文件
//表示已经被解析过了,下次就不会被解析了
configuration.addLoadedResource(resource);
/**
* 上面的是解析mapper中的标签,下面的这个方法解析的是
* 将mapper配置文件中对应的Mapper解析出来,然后添加到
* mapperRegistry中,这样我们通过SqlSession就可以通过.getMapper()来获取指定的
* Mapper,比如你定义了一个Mapper接口,里面有个selectById的方法,那么通过
* selSession.getMapper(Class),那么就可以调用selectById方法
* 所以这里就仅仅只是为了将配置文件中的Mapper添加到
* MapperRegistry中的knownMappers集合中
*
*/
bindMapperForNamespace();
}
....
所以添加到Mapper中的方法就是bindMapperForNamespace
bindMapperForNamespace
private void bindMapperForNamespace() {
//这里的namespeace就是mapper的的全限定名,就是Mapper接口的全路径名称,是在configurationElement
//这个方法中设置进去的
String namespace = builderAssistant.getCurrentNamespace();
//首先肯定是Mapper需要配置了才能添加Mapper,所以要先判断是否为空
if (namespace != null) {
Class<?> boundType = null;
try {
//通过Mapper的全限定名得到这个Mapper的实例,也就是通过反射得到Mapper的实例
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
//如果说在configuration中还没有这个Mapper,添加Mapper
if (boundType != null && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
//添加去重功能
configuration.addLoadedResource("namespace:" + namespace);
//添加Mapper
configuration.addMapper(boundType);
}
}
}