从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的
Posted 犀牛饲养员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的相关的知识,希望对你有一定的参考价值。
为了能清楚的说明问题,源码我尽量加上详细的注释。有些大段的源码我只是截取了一部分能说明问题就好。
xml文件解析
我们知道SqlSessionFactory
是mybatis非常重要的一个类,它是单个数据库映射关系经过编译后的内存镜像.SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象类的build方法创建,而xml文件的解析就是在这个方法里调用的。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());//这里是解析的入口
...
接着看下parse方法,
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));//这里开始解析
return configuration;
}
接着看parseConfiguration方法,
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//package标签
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//mapper标签
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 这里就会加载resource,解析mapper文件,构建mapperStatement对象,
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
...
注意这里还是解析mybatis的配置文件,还没到我们的xml sql文件。有人可能有疑问,这里的package、resource是啥啊,在mybatis的配置文件好像也没看到啊?事实上,mybatis的配置文件是可以这样写的:
<mappers>
<mapper resource="Mapper xml的路径(相对于classes的路径)"/>
</mappers>
或者,
<mappers>
<mapper class="接口的完整类名" />
</mappers>
不过我们大部分是用spring+mybatis的方式,这种配置比较少见了,更多的可能是这样的:
mybatis:
# 配置类型别名
type-aliases-package: com.xxx.xxx.system.model
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapper-locations: 'classpath*:/mybatis/*/**Mapper.xml'
# 加载全局的配置文件
config-location: 'classpath:/mybatis/mybatis-config.xml'
作用其实是一样的。
继续往下看,XMLMapperBuilder#parse方法,
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//xml sql文件都是mapper开始的
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();//尝试通过nameSpace来加载配置文件。
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
configurationElement方法,
private void configurationElement(XNode context) {
try {
解析namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//解析parameterMap,最后添加到 Configuration对象parameterMaps属性里面,全局通用
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap,放在Configuration中,全局通用
resultMapElements(context.evalNodes("/mapper/resultMap"));
解析sql
sqlElement(context.evalNodes("/mapper/sql"));
//真正的开始解析select|insert|update|delete标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
Configuration
类是mybatis非常核心的一个类,由很多全局配置都会解析后放在这里,比如parameterMaps、resultMap等。继续看buildStatementFromContext方法,这个方法最终调用org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode,如下:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
....
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//Mybatis会把每个SQL标签封装成SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//通过builderAssistant,将组装好的MappedStatement添加到configuration里面维护了statement的map,key就是namespace+sql的id、
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
可以看到,最终会生成MappedStatement
对象,并添加到configuration里面维护的一个map,这个map的key就是namespace+sql的id,value是对应的MappedStatement对象。这个MappedStatement对象非常重要,它是连接我们两个部分的关键,记住这个类。
总结下:
XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要。
- id:全限定类名+方法名组成的ID。
- sqlSource:当前SQL标签对应的SqlSource对象。
MappedStatement对象会被缓存到Configuration#mappedStatements中,全局有效。Configuration对象就是Mybatis中的核心类,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。
动态代理
了解了解析的流程,接着看另外一个问题:
我们定义的Dao接口并没有实现类,那么在调用它的时候,它是怎样最终执行到我们的SQL语句的呢?我先给出答案,动态代理。下面就来具体分析下。
这里先引入一个非常重要的类MapperProxy
,来看看它的定义:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -4724728412955527868L;
private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
private static final Constructor<Lookup> lookupConstructor;
private static final Method privateLookupInMethod;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache;
...
这个类继承了InvocationHandler,肯定是动态代理了。如果有小伙伴对动态代理不熟悉,可以先补充下这部分知识,下面的内容会更好理解一些。
思考一个问题:MapperProxy
是什么时候创建的呢?是在SqlSession的getMapper这个抽象方法的实现中调用的,最终调用的是org.apache.ibatis.binding.MapperRegistry#getMapper,代码如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//通过动态代理创建一个Mapper实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
当我们声明一个dao接口的时候,通常是这样做:
@MapperScan("com.fcbox.uniorder.system.dao")
这是在springboot中的用法,或者也可以使用xml配置的方式。这个注解的作用是,将路径下的所有类注册到Spring Bean中,并且将它们的beanClass设置为MapperFactoryBean。MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们注入这个Dao接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。(getObject方法返回就是FactoryBean创建的Bean实例)
org.mybatis.spring.mapper.MapperFactoryBean#getObject代码如下:
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
这个getMapper方法就会一路调用到我们上面说的org.apache.ibatis.binding.MapperRegistry#getMapper方法。
总结下,也就是说我们通过注入Dao接口的时候,注入的就是MapperProxy
这个代理对象,那么自然的,根据动态代理的原理,当
我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法。那我们就来看看这个invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判断接口是否有实现类(一般我们的dao接口没有实现类)
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//所以一般会走这个分支
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
然后会继续cachedInvoker的invoke方法,看看cachedInvoker
是个啥东西,
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
/**
* 会走到这里来
* PlainMethodInvoker是封装的一个mapper调用的工具类
* MapperMethod 对象里面包含了两个对象的引用:
* SqlCommand 包含了方法名(全限定名)和命令类型(insert、delete等等)
*/
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
...
}
所以调用的是org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke方法,
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
这里调用的是MapperMethod的execute方法,继续看,
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
...
MapperMethod源码发现最终还是调用sqlSession中的相关方法,sqlSession再委托给Excutor去执行,比如我们拿update举例,如下:
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
咦?这个MappedStatement
怎么看着这么眼熟,这不就是我们第一部分讲的内容吗?是不是有种柳暗花明又一村的感觉。
总结下,当我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法,最终会通过接口的全路径名从Configuration
这个大管家的某个map里找到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回。
参考:
- https://juejin.cn/post/7004047712664420382
以上是关于从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的的主要内容,如果未能解决你的问题,请参考以下文章
2021阿里最新面试题:Mybatis中的Dao接口和XML文件里的SQL是如何建立关系的?学习完这份资料带你成功上岸阿里(2021最新版)