mybatis 源码赏析(一)sql解析篇

时间:2023-02-21 22:49:11

本系列主要分为三部分,前两部分主要分析mybatis的实现原理,最后一部分结合spring,来看看mybtais是如何与spring结合的就是就是mybatis-spring的源码。

相较于spring,mybatis源码算是比较容易理解的,因为很少用一层层的抽象,类所做的事一目了然,但是要说质量的话,我还是偏向与spring,只是个人意见,好了我们开始:

为了便于理解,我们分两部分介绍mybatis,本篇着重介绍mybtais是如何解析xml或者注解,并将其结构化为自己的类。

先看mybatis官网上的一个例子:

 public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //本篇只分析到这
SqlSession sqlSession = sqlSessionFactory.openSession();
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
Blog blog = blogMapper.getById();
}

抛开其他框架,我们只用mybatis的话可以看到,核心的几个类:

SqlSessionFactory,
SqlSession,
SqlSessionFactoryBuilder

老规矩,在正真看代码之前,我们先把核心的几个类拿出来解释一下,然后看一下类图,从整体上了解mybatis的设计:

SqlSessionFactory:顾名思义,sqlsession的工厂类,从上面的例子可以看出,一个SqlSessionFactory实例对应一个mybatis-config.xml配置即一个数据源
SqlSessionFactoryBuilder:SqlSessionFactory的组装车间,这里用了类似建造者模式,为啥是类似,因为这不是标准的建造者模式,这里说一句,mybatis里很多地方都用了这种不是很标准的建造者模式。
SqlSession:一次数据库会话对应一个SqlSession实例
Configuration:这个类是本篇的重点,它和sqlsessionfactory的重要产出,我们在xml或者注解中的几乎所有配置都会被解析并装载到configuration的实例中。
MappedStatement:这个类实际上是被configuration持有的,之所以拿出来单独说,是因为它太重要了,我们的sql相关的配置,都会被解析放在这个类的实例中,因此,很多分页插件也是通过改变这个类中的sql,来将分页的逻辑切入正常逻辑中。(当然不仅仅可以做分页,也可以做加密等等)。
 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.
}
}
}

Configuration这个类的实例是在XMLConfigBuilder的构造方法中被创造出来的,我们重点来看解析的逻辑:

public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

顺便提一句,mybatis解析xml用的是java中自带的解析器的,有关xml解析的知识这里不会细讲,有兴趣的同学可以去了解一下dom4j,dom,sax,jdom等的区别和优劣。

 private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(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);
}
}

这里我们主要看这几个方法,typeAliasesElement方法是注册类的别名,我们在xml中指定resultType或paramterType时会用到。这里使用了之前配置的VFS的实现类去装载指定package底下的类的字节码,然后通过反射获取类的信息。

settingsElement方法是将之前的配置赋值给configuration实例,简单的赋值,这里就不上源码了。

typeHandlerElement注册了类型处理器,同样,和typeAliasesElement方法类似,这个方法也可以去扫描指定包路径底下的类,并为这些类创建别名,默认使用的是Class.getSimpleName(),即类名称的缩写。

接下来会解析插件,然后实例化注册到Configuration中,等运行时动态代理目标类, 这部分我们会在下一章重点分析,这里不做介绍,然后会把所有的配置都设置到configuration中,后面的解析datasource和解析typehandler的逻辑这里就不分析了,都是简单的解析赋值操作,我们重点来看mapperElement方法:

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.");
}
}
}
}
}

这里主要是两部分,上面是扫描包,根据注解生成对应的mappedStatement,第二部分是解析xml配置,根据xml配置来生成mappedstatement,我们先看第一部分,根据注解生成mappedstatemnet:

configuration会把工作委托给MapperRegistry去做,MapperRegistry会持有所有被解析的接口(运行时生成动态代理用),而最终解析的产物:mappedstatement依然会被configuration实例持有放在mappedStatements的map中:

  public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}

这里同样是扫描指定包路径地下的所有类,并且根据filter(new ResolverUtil.IsA(superType)),挑选出满足条件的类,这里的条件是Object.class,所以包底下的所有类都会被装进来,接下来就是遍历这些类然后解析了:

  public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

我们看到,这里只解析所有接口,MapperRegistry所持有的是一个knownMappers,这里会有一个工厂类的实例MapperProxyFactory,这个类会在下一章介绍,会在生成接口的动态代理时被调用,我们继续往下看,接下来就是接口的解析工作了:

 public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}

在执行真正解析之前,mybatis又去load了一次xml文件,这是为了防止之前没有装在xml,保证一定是xml被解析完,再解析接口,

  void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect; KeyGenerator keyGenerator;
String keyProperty = null;
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
} if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
} String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} else if (isSelect) {
resultMapId = parseResultMap(method);
} assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}

这洋洋洒洒一堆,目的就是为了解析注解的配置,然后构建一个mappedstatement,我们只看核心逻辑:

首先,mybatis会根据注解生成一个sqlSource,这个接口是承载sql的实例,接口只有一个方法:getBoundSql,这个方法会根据传入的参数,将原来的sql解析,替换为能被数据库识别执行的sql,然后放入boundsql中,同样,这个方法是在运行时才被调用的。这里我们只看生成sqlsource的逻辑:

  private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}

这里只有两种情况,普通的sql,和sqlprovider,我们来看核心方法:

@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}

首先判断是不是脚本,如果是脚本则走解析脚本的逻辑,如果不是,则判断是否是动态sql,判断的逻辑就是sql中是否含有 ”${}" 这样的关键字,如果有则是动态sql,如果不是,则是静态的。静态的话在构造方法中还有一段逻辑:

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}

这段的作用,是继续解析sql中的 “#{}” ,将其替换为 ? ,然后返回一个StaticSqlSource的实例。其实DynamicSqlSource最中也是转化为StaticSqlSource的,只不过它是在getBoundSql被调用的时候才做的,而且这里还会把"${}"替换为相应的参数:

  public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}

所以我们拿到的BoudSql的实例实际上已经经过了解析。

解析完sqlsource后,mybatis会生成相应的mappedstatement,为了区分不同的mappedstatement,mysql为其创建了一个Id:

final String mappedStatementId = type.getName() + "." + method.getName();

这里可以看到,Id是类名加上方法名,这里就有一个问题,当类中的方法被重载时,mybatis会认为有问题的,可以看到,虽然方法被重载,mappedStatementId依然是同一个,所以mybatis中sql的接口是不能重载的。

下面就是根据注解的配置,创建相应的对象,然后一起组装成mappedstatement对象,然后放入configuration实例中的mappedstatements中。

至此,mybaits的sql解析篇就到此结束了,当然,mybatis的功能还远远不止如此,我们将在下一篇,mybatis的执行中,看到mybatis在运行时是如何代理接口,mybatis的各种插件有事如何介入的。

转载请注明出处,谢谢~

mybatis 源码赏析(一)sql解析篇的更多相关文章

  1. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  2. Spring mybatis源码篇章-动态SQL节点源码深入

    通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-动态SQL基础语法以及原理 前话 前文描述到通过mybatis默认的解析驱动类org.apache.ibat ...

  3. 手把手带你阅读Mybatis源码(三)缓存篇

    前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...

  4. MyBatis源码分析之环境准备篇

    前言 之前一段时间写了[Spring源码分析]系列的文章,感觉对Spring的原理及使用各方面都掌握了不少,趁热打铁,开始下一个系列的文章[MyBatis源码分析],在[MyBatis源码分析]文章的 ...

  5. Spring mybatis源码篇章-MybatisDAO文件解析&lpar;二&rpar;

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(一) 默认加载mybatis主文件方式 XMLConfigBuilder ...

  6. Spring mybatis源码篇章-MybatisDAO文件解析&lpar;一&rpar;

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-SqlSessionFactory 加载指定的mybatis主文件 Mybatis模板文件,其中的属性 ...

  7. Spring mybatis源码篇章-动态SQL基础语法以及原理

    通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-Mybatis的XML文件加载 前话 前文通过Spring中配置mapperLocations属性来进行对m ...

  8. 手把手带你阅读Mybatis源码(一)构造篇

    前言 今天会给大家分享我们常用的持久层框架——MyBatis的工作原理和源码解析,后续会围绕Mybatis框架做一些比较深入的讲解,之后这部分内容会归置到公众号菜单栏:连载中…-框架分析中,欢迎探讨! ...

  9. 手把手带你阅读Mybatis源码(二)执行篇

    前言 上一篇文章提到了MyBatis是如何构建配置类的,也说了MyBatis在运行过程中主要分为两个阶段,第一是构建,第二就是执行,所以这篇文章会带大家来了解一下MyBatis是如何从构建完毕,到执行 ...

随机推荐

  1. WebService &quot&semi;因 URL 意外地以 结束,请求格式无法识别&quot&semi; 的解决方法

    最近在做一个图片上传的功能,js调用用webservice进行异步访问服务器,对于不是经常用webservice的菜鸟来说,经常会遇到以下的问题(起码我是遇到了) 在页面上写了js调用代码如下所示: ...

  2. php技术之路

    按照了解的很多PHP/LNMP程序员的发展轨迹,结合个人经验体会,抽象出很多程序员对未来的迷漫,特别对技术学习的盲目和慌乱,简单梳理了这个每个阶段PHP程序员的技术要求,来帮助很多PHP程序做对照设定 ...

  3. x01&period;FileProcessor&colon; 文件处理

    姚贝娜落选,意味着好声音失败.“我们在一起”的精彩亮相,正如同她的歌声,愈唱愈高,直入云霄. 文件处理,无外乎加解密,加解压,分割合并.本着“快舟"精神,花了两天时间,写了个小程序,基本能满 ...

  4. 正则匹配中文&period;PHP不兼容的问题

    不使用: ^[\u4e00-\u9fa5_a-zA-Z0-9_]+$ 有可能兼容有问题 if(!preg_match_all("/^[\\x7f-\\xff_a-zA-Z0-9]+$/&qu ...

  5. MDEV Primer

    /************************************************************************** * MDEV Primer * 说明: * 本文 ...

  6. Linux - 引用

    双引号 如果把文本放在双引号中,那么 shell 使用的所有特殊字符都将失去它们的特殊含义,而被看成普通字符.字符 "$"(美元符号)."\"(反斜杠).&qu ...

  7. POJ2485 最小生成树

    问题:POJ2485 本题求解生成树最大边的最小值 分析: 首先证明生成树最大边的最小值即最小生成树的最大边. 假设:生成树最大边的最小值比最小生成树的最大边更小. 不妨设C为G的一个最小生成树,e是 ...

  8. Java字符串格式化记录

    最近打log的时候用到了字符串的格式化. Java中String格式化和C语言的很类似.把情况都列出来,以后好查询. public static void main(String[] args) { ...

  9. Hibernate5&period;3 &plus; mysql8&period;0遇到的问题

    今天学习Hibernate看的是旧版本的视频Hibernate4.0版本 遇到几个新旧版本的区别. 1.方言,这个是因为SQL不是因为Hibernate 新版方言 2.将编译的类配置进congifur ...

  10. &lbrack;记录&rsqb;MySQL 查询无法导出到文件

    很多时候我们需要将数据导出到 xls文件, 然后交给数据分析师分析. 而这个查询数据+导出的动作,理应使用一个有只读权限的用户使用. 但查询某表时: select * from table ,此用户可 ...