上篇文章mybatis源码中如何找到xml文件并提取xml中的sql,解决了前两个问题,本篇文章继续追踪源码,尝试答疑后面的两个问题,在开始正式追踪源码,再次确认下我们要研究的问题:
1、mybatis如何找到存储sql的xml文件
2、mybatis如何解析xml中sql相关标签并拼装成完整的sql语句
3、mybatis如何将xml中sql与接口中方法绑定
4、mybatis如何将sql发送出去并获得结果
这里我们还是先从demo样例中看:
可以看到xml解析器解析xml文件后,最后会生成一个默认的sqlsession工厂。因为要根据工厂对象创建sqlsession对象,这里我们再看下opensession方法的实现:
创建sqlsessionfactory的时候返回的是DefaultSqlSessionFactory,所以接下来我们跳入该类中查看:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//从配置类获取当前环境对象,并以此生成事务工厂和事务对象(因为不是此次源码探究的目的,所以事务这里不深入研究)
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
//创建sqlsession会话并返回
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
(看到这里我有个小建议或者个人理解吧,就是纯粹探查mybatis源码流程,可以根据代码流来一行行查看,但是如果想完全理解mybatis,而不是囫囵吞枣,对于configuration、enviroment、mapperRegistry等等类或者属性的作用以及包含嵌套关系也要关注了解下)
至此我们已经拿到了SqlSession,但是后两问的源码答案还没出现,不要急,我们接着向下看:
这个方法比较重要,下面详细介绍下:
首先可以看到该方法是一个泛型方法,入参类型是一个泛型类,返回的是一个泛型对象,实际入参则是我们即将使用的接口类。下面的configuration对象则是解析xml配置文件生成的,包含了配置文件中的信息,并在生成当前sqlsession会话时被传入了当前会话中。所以这个方法的整体作用可以概括为:根据xml配置文件以及接口类生成一个代理对象。
在深入探查如何生成代理对象前,我们可以再大胆猜想一个代理对象的功能,那就是后续所有对接口中方法的调用,都通过该代理对象找到对应的xml中配置的sql,然后根据接口中方法的入参,再动态生成完整的sql,最后再通过jdbc提交查询,接口返回后再通过xml的配置解析成对应的java类并返回。到这对于第三问和第四问是不是有了一点头绪了,至于是不是猜想的这样,我暂时也不知道,后面会再讲。这里我们还是回到现在的流程,继续深入探究下代理对象的生成逻辑:
这个mapperRegistry是不是有点熟悉,它是我们解析xml中生成的对象,该对象有两个属性一个是config,引用了当前Configuration对象,还有一个则是 knownMappers,它是一个map集合,key是接口类,value则是一个包含了当前接口类的mapper代理工厂。
可以看到,首先是从map集合中获取当前接口的mapper代理工厂,随后则通过newInstance方法生成代理对象,我们接着向下看:
public T newInstance(SqlSession sqlSession) {
//创建要代理的mapper对象(注意区分代理对象与要代理的对象)
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
//生成代理对象
return newInstance(mapperProxy);
}
前一个方法主要是创建要代理的mapper对象,并将session会话,接口以及需要缓存的方法等属性信息传入mapper对象中。后一个方法则是创建代理对象的方法,我们继续看一下:
到这里很多人会发懵,生成个动态代理对象怎么没了,这后续的逻辑怎么追踪一探究竟。
不要急,我们到这先大致理一下本文前面的思路,总结起来大致是根据xml配置信息生成sqlsession会话对象,然后该会话依据接口和配置信息动态生成了一个mapper代理对象。后续对接口的访问都会落到mapper代理对象上。因为该对象是动态生成的,所以我们这里看不到后续具体的执行逻辑。具体的执行逻辑只能在执行具体的方法时才可以窥探到。所以这里我们接着向下看:
可以看到此时的接口对象已经是一个mapper代理对象了,而接口方法则只能由该代理对象实现,接下来我们看下queryById的执行逻辑:
因为queryById的执行代码是动态生成的,我们直接点击该方法只会进入到接口类中,如果想进入具体的实现逻辑中,必须要通过debug进入,下面截图是IDEA进入的方式:
可以看到,此时进入到了要代理的mapper对象中。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果代理方法属于Object类进行的处理(不能达到我们探查源码目的,不深究)
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);
}
//获取要代理的mapper方法的对象,从map集合中获取,获取不到则新建并放入集合中
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行代理的mapper方法
return mapperMethod.execute(sqlSession, args);
}
可以看到,核心在最后两行,我们先看mappermethod的获取:
private MapperMethod cachedMapperMethod(Method method) {
//从方法缓存集合中获取,获取不到则新建并放入集合
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
//通过构造器创建 MapperMethod 对象
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//通过配置信息,接口类以及方法信息创建SqlCommand对象(包含了要执行的sql信息)
this.command = new SqlCommand(config, mapperInterface, method);
//创建MethodSignature,主要用于存储结果返回类型相关的信息
this.method = new MethodSignature(config, mapperInterface, method);
}
可以看到先是从一个map集合获取MapperMethod对象,获取不到则通过构造器创建,而MapperMethod也只是创建了两个属性对象,一个是存储了执行sql相关的信息,一个则是包含了返回类型相关的信息。下面我们简单看下这个对象的构造:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//获取方法名和类名
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
//根据方法名类型信息获取要执行sql的信息
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
// 如果当前代理方法找不到对应的sql信息则抛出异常
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 找到当前方法在xml对应的sql信息后,获取sql的唯一名称以及类型信息,并赋值到SqlCommand对象属性中,以便后续使用
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
//根据类信息和方法名构建key
String statementId = mapperInterface.getName() + "." + methodName;
// 如果配置文件中包含当前方法对应的sql信息,则直接返回。(这块可以结合前面一篇文章看,在上一篇中讲到xml文件解析后会将每条sql相关的信息存储到一个集合中,这个集合的key由包名类名方法名组成,value则是sql相关的信息)
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 如果当前类和方法生成的key找不到对应的sql信息,则遍历其父接口信息(未深究)
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
SqlCommand的生成仔细品一品,是不是我们第三问的答案出来了。我来简单说下为什么第三问解决了。上篇文章中解析xml时将sql信息存储到了集合中,其key是由包名类型方法名组成,不过这时的名称都是从xml中解析得到的。而到了具体执行的时候,则根据实际的全路径类名(包含包名)加上方法名组成key,此时再去集合中查找对应的sql信息。所以如果配置文件中的namespace、id信息和接口类、方法名不同,则会抛出异常。所以到这里可以认为第三问的答案已经获得了。
下面我们接着讲:
MapperMethod方法中除了构造SqlCommand对象外,还创建了MethodSignature对象,这个对象主要包含了一些返回类型信息,主要和返回结果的解析相关,这和我们的问题关联不大,所以这里不深究。MapperMethod构建完后则进入正式的执行阶段,这也对应我们最后一个问题,该追踪链路也比较长,这里就放到下一篇文章中讲。