1、流程分析
相信大家了解mybatis的话,一定会知道mybatis是一门orm语言,其中最先要了解的当然是mybatis是如何将dao层的接口函数和sql语句进行绑定,从而能够通过调用接口函数来执行相应的sql语句。
首先,mybatis是通过动态代理的方式将接口语句和sql进行绑定的,mybatis会自己创建一个动态的中介类,并且根据相应的接口返回具有实例的代理类。同时在一开始mybatis会解析xml信息来初始化参数。为之后创建中介类提供原始数据。总结一下mybatis的流程主要是解析xml文件–>dao层动态绑定–>执行语句。dao层是如何进行绑定的我已经在源码分析一种详细介绍了,接下来我首先来主要分析下mybatis是如何解析xml文件的。
2、解析xml文件
2.1、使用方法
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
上面是一般我们获取mybatis-config.xml文件的写法,XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。XML配置文件的格式如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<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="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
然后我们可以通过sqlSessionFactory来得到sqlsession对象。
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
然后利用getMapper函数可以得到相应的代理对象,最后可以利用代理对象来调用相应的函数,具体这个函数是如何一一对应xml中的sql的呢?让我们先来看下xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
这份xml文件mapper标签前面的是xml文件的前缀,我们可以先不需要去关心它,只需要从mapper标签开始看,可以看到mapper标签后面跟随了namespace,这个可以唯一确定一个类,那我们如果确定是这一个类中的哪一个函数呢,那就是下面的id号了,这个id号必须与相对应的类的函数名一致,否则会报错,这样我们可以将下面的sql语句和dao层的接口函数一一对应起来。
2.2、解析xml流程
解析config.xml
首先需要解析mybatis的总体的配置文件,在SqlSessionFactoryBuilder类的build函数中对调用到XMLConfigBuilder类的函数,也就是说,我们在一开始利用resource这个指定总体配置文件的位置,然后调用SqlSessionFactoryBuilder的build函数时就开始读取相应的配置文件信息。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
这边看到XMLConfigBuilder的对象调用了parse函数
ublic Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这边也主要是调用了parseConfiguration函数,parser.evalNode("/configuration")函数的作用是将xml中的信息利用XPathParser类的evalNode函数,将信息组装成XNode的形式。
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
可以看到这些都是mybatis处理配置文件中的顶级标签,相信如果前面的顶级标签大家不熟悉,但是mapper标签一定是了解的,在文章前面的配置文件中也有这个顶级标签。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
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);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
所以解析mapper标签的函数是mapperElement(XNode parent)函数,但是对象的mapper标签的作用是指引到相应的映射的mapper.xml的配置文件中,所以为了方便,mybatis中有相应的处理这个mapper.xml的类,就是XMLMapperBuilder类,在上面函数中一开始也是解析相应的mapper信息,例如上文中的mappers标签的内容
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
在下面利用Resources.getResourceAsStream(resource)可以获取相应的mapper.xml的信息,然后传入到XMLMapperBuilder中进行解析,解析函数时parse函数
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
在configurationElement(parser.evalNode("/mapper"));函数中读取相应的配置信息然后放入到Configure这个mybatis的大管家中。
private void configurationElement(XNode context) {
try {
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"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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);
}
}
到这一步就已经解析到下面xml文档中的那步了
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
前面当然也是处理相应的顶级标签,具体这些标签的作用大家可以到mybatis的官方文档中去查看,这边贴出来文章就太长了。在buildStatementFromContext(context.evalNodes(“select|insert|update|delete”))函数中就是解析了上面的select标签。
/*这边getDatabaseId表示数据库id,可以进行识别,如果我们只有一个数据库,一般可以不设置,直接默认的调用下面那个函数*/
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
/*因为一个mapper.xml映射文件中可以有多个select或者insert这些sql语句,所以为了区分,mybatis处理这些同一个mapper文档中的不同sql语句,
创建了XMLStatementBuilder对象,一个sql语句对应一个XMLStatementBuilder,后面也会有一个sql语句对应一个sqlSource对象
因为sqlSource对象的作用就是为了将动态的sql语句合理组装成一个map对象*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
在XMLStatementBuilder类的parseStatementNode中开始解析sql语句
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 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;
}
/*创建一个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.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
当然这边解析的是下面这个xml内容
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
上面函数中首先主要关注的是SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的。SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql,也就是说Mapper文件中的节点的计算,是由SqlSource对象完成的。
最后主要看下builderAssistant.addMappedStatement函数,后面参数非常多,这个函数创建了一个MappedStatement对象。其实对于mybatis中mapper.xml映射文件,如果这个文件中有多个select,insert等这些sql语句,那么有几个这样的sql语句,就会生成几个MappedStatement对象,这个MappedStatement对象中存储有id这个参数,这个id的组成就是namespace和select标签中的id,也就是唯一指明了一个类中的函数,这个类中也有SqlSource对象,这就已经说明了一个SqlSource是和一个sql语句绑定的。addMappedStatement函数将最后创建好的MappedStatement对象放入configuration配置类中。
从这里我们可以看MappedStatement存储有一个sql语句的所有配置信息。
3、dao层和xml中sql如何进行绑定
这个问题也就是mybatis如何创建中介类,使得创建的中介类可以和xml中的sql语句进行匹配,然后利用Proxy.newProxyInstance函数结合接口的配置信息和中介类返回对应的代理类对象。首先在MapperProxyFactory类中
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
在这里会新建一个MapperProxy对象就是中介类,在其中,调用相应的接口函数会调用到MapperProxy类的invoke函数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
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);
}
}
上面我们的接口函数不是一般直接java中定义的函数,所以会走到下面的cachedInvoker(method).invoke(proxy, method, args, sqlSession);函数中。
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return methodCache.computeIfAbsent(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 {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
m.isDefault()这个函数是用来判断是否是接口的默认函数,接口有两类函数,分别是
- 抽象方法,接口的子类需要实现
- 默认方法,接口的子类不需要实现,可以直接使用。我们只需在方法名前面加个 default 关键字即可实现默认方法。
而我们一般定义的接口函数都是抽象方法,所以函数进入到new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()))这个语句中,首先我们先来看下new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())函数
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
上面SqlCommand是可以去MappedStatement类对象中获取,MethodSignature参数可以通过反射机制,利用方法中的参数来获取,具体如何获取将放到后面的mybatis解析命令分析篇讲解,他的作用主要是获取这个接口函数的各个参数,一对几的情况。最后在生成的PlainMethodInvoker类中调用invoke函数
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
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;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
从而可以知道根据几对几和sqlcommand可以分别调用相应的函数,具体还是利用sqlSession去执行命令,而sqlSession中包含有自己对应的configuration数据,从而可以知道调用哪一个sql语句来执行。
4、总结
mybatis在创建SqlSessionFactory对象时会解析配置文件,并将每一个sql语句配置信息保存各自的MappedStatement中,sqlsource会解析动态sql语句。并将MappedStatement对象数据放入Configuration对象中,同时利用MappedStatement中的id,这个id是namespace和函数id组成的,可以唯一执行一个接口类中的一个函数。这样我们可以利用id来找到sql语句对应的MappedStatement。
同时mybatis利用动态代理主动创建中介类,返回一个代理对象,这样在代理对象调用相应的函数实际上调用的是中介类的invoke函数,最后调用还是sqlsession对象中的方法,并且这个对象中也包含了Configuration对象,这样我们可以在最后读取到Configuration配置中的信息,最终调用合适的sql语句执行。