一、背景
之前在项目中一直使用得是hibernate,对于mybatis一直没有使用过,最近项目负荷不高,终于抽出时间来学习下mybatis。
mybatis的背景知识就不介绍,基础用法在本文也不做过多赘述,不清楚的可以去阅读:https://mybatis.org/mybatis-3/zh_CN/configuration.html
二、SqlSessionFactory的加载过程
SqlSessionFactory是mybatis的一个核心类,它是创建SqlSession的工厂类。而SqlSession是操作数据库的核心类。本文将以一个简单的例子说明SqlSessionFactory的加载过程。
本例中的mybatis配置文件是org/mybatis/example/mybatis-config.xml,SqlSessionFactory初始化的过程实际上就是读取并组织mybatis-config.xml的配置信息的过程。
解析mybatis-config.xml的核心类是XMLConfigBuilder
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//读取外挂的配置文件
propertiesElement(root.evalNode("properties"));
//读取settings标签,并将值注入到Properties中
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
//设置日志
loadCustomLogImpl(settings);
//设置 type别名,注入到configuration.typeAliasRegistry中
typeAliasesElement(root.evalNode("typeAliases"));
// 加载插件 注入到configuration.interceptorChain
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 解析environments 生成TransactionFactory、dataSource ,注入到configuration.environment
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析typehandlers,并且将类型处理器注入到TypeHandlerRegistry中
typeHandlerElement(root.evalNode("typeHandlers"));
//处理mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
通过读取这段源码,可以发现mybatis读取mybatis-config.xml最终时间所有的配置信息解析后存放到Configuration类中。Configuration类就像一个容器。
三、mapper信息的加载
mybatis中的mapper有两种,一种是Mapper.class,一种是配置文件Mapper.xml。两种方式各有优劣。本章节将分别讲述这两种方式的加载。
3.1 Mapper.xml的解析
一个典型的Mapper.xml如下图所示。MyBlogMapper.xml中记录中 数据库字段和java类型的映射关系,以及相关的sql语句。
当需要操作数据库时,通过 namespace+sqlId定位到语句,然后执行。
Mapper.xml的解析过程是:
- 新建一个XMLMapperBuilder对象,然后去解析
- 逐个去解析Mapper.xml中的各个元素。最后将sql语句解析注入到Configuration.mappedStatements中。
3.2 Mapper.class的解析
- mapperClass的解析过程是直接调用Configuration.addMapper()方法
- 调用MapperRegistry的addMapper方法
- MapperRegistry中维护了一个knownMappers的HashMap存放MapperClass和其代理对象工厂(MapperProxyFactory)的映射。MapperProxyFactory实际上是为每个MapperClass生成MapperProxy代理对象。
- 生成一个MapperAnnotationBuilder对象,来解析MapperClass
- 循环解析MapperClass的方法,并将sql操作封装为Statement,注册到Configuration.mappedStatements中。
四、操作数据
4.1 MapperClass接口方法调用
MapperClass接口方法的调用实际上是Mybatis为每个mapperClass生成了一个动态代理对象MapperProxy。对数据库的的操作实际上实在MapperProxy中完成,时序图如上图所示。
MapperProxy.invoke() 方法是代理对象执行的入口,其中会拦截所有非 Object 方法,针对每个被拦截的方法,都会调用 cachedInvoker() 方法获取对应的 MapperMethod 对象,并调用其 invoke() 方法执行代理逻辑以及目标方法。
在 cachedInvoker() 方法中,首先会查询 methodCache 缓存,如果查询的方法为 default 方法,则会根据当前使用的 JDK 版本,获取对应的 MethodHandle 并封装成 DefaultMethodInvoker 对象写入缓存;如果查询的方法是非 default 方法,则创建 PlainMethodInvoker 对象写入缓存。
cachedInvoker() 方法的具体实现如下:
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
// 尝试从methodCache缓存中查询方法对应的MapperMethodInvoker
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
// 如果方法在缓存中没有对应的MapperMethodInvoker,则进行创建
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) { // 针对default方法的处理
// 这里根据JDK版本的不同,获取方法对应的MethodHandle的方式也有所不同
// 在JDK 8中使用的是lookupConstructor字段,而在JDK 9中使用的是
// privateLookupInMethod字段。获取到MethodHandle之后,会使用
// DefaultMethodInvoker进行封装
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} else {
// 对于其他方法,会创建MapperMethod并使用PlainMethodInvoker封装
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
}
其中使用到的 DefaultMethodInvoker 和 PlainMethodInvoker 都是 MapperMethodInvoker 接口的实现,如下图所示:
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 首先将MethodHandle绑定到一个实例对象上,然后调用invokeWithArguments()方法执行目标方法
return methodHandle.bindTo(proxy).invokeWithArguments(args);
}
在 PlainMethodInvoker.invoke() 方法中,会通过底层维护的 MapperMethod 完成方法调用,其核心实现如下:
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 直接执行MapperMethod.execute()方法完成方法调用
return mapperMethod.execute(sqlSession, args);
}
-
MapperMethod
通过对 MapperProxy 的分析我们知道,MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法,其核心字段也围绕这两方面的内容展开。 -
SqlCommand
MapperMethod 的第一个核心字段是 command(SqlCommand 类型),其中维护了关联 SQL 语句的相关信息。在 MapperMethod$SqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERT、UPDATE、DELETE、SELECT 和 FLUSH 五种。
下面我们就来看看 SqlCommand 如何查找 Mapper 接口中一个方法对应的 SQL 语句的信息,该逻辑在 SqlCommand 的构造方法中实现,如下:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 获取Mapper接口中对应的方法名称
final String methodName = method.getName();
// 获取Mapper接口的类型
final Class<?> declaringClass = method.getDeclaringClass();
// 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识,
// 到Configuration这个全局配置对象中查找SQL语句
// MappedStatement对象就是Mapper.xml配置文件中一条SQL语句解析之后得到的对象
MappedStatement ms = resolveMappedStatement(mapperInterface,
methodName, declaringClass, configuration);
if (ms == null) {
// 针对@Flush注解的处理
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else { // 没有@Flush注解,会抛出异常
throw new BindingException("...");
}
} else {
// 记录SQL语句唯一标识
name = ms.getId();
// 记录SQL语句的操作类型
type = ms.getSqlCommandType();
}
}
这里调用的 resolveMappedStatement() 方法不仅会尝试根据 SQL 语句的唯一标识从 Configuration 全局配置对象中查找关联的 MappedStatement 对象,还会尝试顺着 Mapper 接口的继承树进行查找,直至查找成功为止。具体实现如下
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识
String statementId = mapperInterface.getName() + "." + methodName;
// 检测Configuration中是否包含相应的MappedStatement对象
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 如果方法就定义在当前接口中,则证明没有对应的SQL语句,返回null
return null;
}
// 如果当前检查的Mapper接口(mapperInterface)中不是定义该方法的接口(declaringClass),
// 则会从mapperInterface开始,沿着继承关系向上查找递归每个接口,
// 查找该方法对应的MappedStatement对象
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}