Mybatis的配置文件
<?xml version="1.0" encoding="UTF-8" ?>
Mapper
<?xml version="1.0" encoding="UTF-8"?>
实体类
mapper
public interface StudentMapper { List
实体对象
package com.lc.simple;public class Student { private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + ''' + ", age=" + age + '}'; }}
测试
public class MainTest { public static void main(String[] args) throws IOException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis.xml")); SqlSession sqlSession = sqlSessionFactory.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List
下面会主要分析`sql`和`select`,其余的`insert|update|delete`本质都差不多
在解析Mapper的时候,会通过XMLMapperBuilder来帮助解析,他继承于baseBuilder,在Mybatis中,baseBuilder是一个抽象类,大多数的Builder都继承于他,他主要是有三个基本的属性回来再看,`XMLMapperBuilder`,作为一个Mapper的构建者来说,因为Mapper文件是xml文件,在他里面就需要通过`XPathParser`来解析xml文件,还得保留一个Mapper文件的路径,还有一个`MapperBuilderAssistant`,他是`XMLMapperBuilder`的属性,他是帮助`XMLMapperBuilder`来构建的,在他里面主要判断操作和判断nameSpace和cache还有一些对象的创建。
上面说的XMLMapperBuilder是用来构建Mapper文件的,但是对于Select这种的,有专门对应的XMLStatementBuilder来构建。对应的代码在XMLStatementBuilder#parseStatementNode()中,在这个方法里面会解析Select里面的东西。
主要是看当前的databaseId如果设置了,是否符合。是否要刷新缓存和使用缓存,Select操作就不需要刷新缓存但是会使用缓存,这里说的是二级缓存。处理sql语句中的include标签。确定参数类型,返回类型,ResultMap,等等。解析 动态sql的处理是在XMLStatementBuilder中进行的,主要就是下面的两行代码。先创建LanguageDriver在通过他来创建SqlSource,如果在标签中不指定lang属性,默认的为XMLLanguageDriver。 要记住这里是还是解析Mapper文件,在解析加载Mapper文件的时候,确定动态还是静态的SqlSource,如果是动态,不会组装sql,等真正运行的时候在通过参数来组装最后的sql,如果是静态的sql,在创建SqlSource的时候就确定好了, 在运行的时候就只是填充值就好了。在构建sql的时候也有对应的XMLscriptBuilder,具体的代码在XMLscriptBuilder#parsescriptNode() 问题 动态sql和非动态sql怎么区分? 如果有mybatis规定的那些标签,或者语句中包含${}的,就是动态的sql。注意#{}的不是动态sql。 代码的实现在TextSqlNode#isDynamic()中。因为本质都是Xml文件,就可以解析xml文件,或者xml文件中的各个节点,对应的就是各个标签,文本也是一个节点,对于非文本的标签(这可以通过xml中规定的节点的类型来判断),那肯定就是一个动态,文本标签里面的内容如果有 的 话 , 就 需 要 获 取 文 本 的 内 容 来 判 断 是 否 有 {}的话,就需要获取文本的内容来判断是否有 的话,就需要获取文本的内容来判断是否有{}。这对于之后的我们的学习也是有帮助的,这里介绍一下Mybatis是怎么做的。有两个类,解析器和处理器,解析器就是专门来解析文本中的规定字符串的,对应的就是 , 处 理 器 来 处 理 {},处理器来处理 ,处理器来处理{a}里面的a,也就是解析器解析到的值。对应的是GenericTokenParser和TokenHandler。要注意TokenHandler, 的 填 充 值 也 是 他 来 做 的 , 对 应 的 是 ‘ B i n d i n g T o k e n P a r s e r ‘ , 都 知 道 , {}的填充值也是他来做的,对应的是`BindingTokenParser`,都知道, 的填充值也是他来做的,对应的是‘BindingTokenParser‘,都知道,{}会有sql注入的问题的,就是通过sql拼接的方法来实现的。 解析操作不难,指定开头字符串,和结尾字符串,剩下的就是在文本中找到开头的字符串,从0开始截取到当前位置,在找到末尾,同样截取,就可以获取到了。 下面来具体说说动态sql的具体解析操作 上面说了,在解析sql的时候也会有对应的builder,XMLscriptBuilder就是用来做这个操作的。先看看他长什么样子。 上面的红框是他的内部类,下面的是属性,对于sql文件来说,在Mybatis中,sql是由每一部分来组装起来的,一个逆向的语法树,解析sql的时候,比如说,碰到了where标签,对应的就是where节点,比如遇到了一个什么都标签都没有写的sql,就是一个文本节点,Mybatis用SqlNode来表示节点,在解析的时候不同的标签有不同节点类型。 此外,这只是表示,不同的节点得不同的处理方式吧,他叫做NodeHandler. 得重点说一下MixedSqlNode,他有一个SqlNode的集合,在调用的时候会循环调用SqlNode集合,这样做是因为要支持标签的嵌套,比如说在where标签里面有if标签,if标签里面有forEach标签。forEach里面有正常的sql,正常的sql就是StaticTextSqlNode,forEach里面的就是MixedSqlNode,同样的对于where标签也是MixedSqlNode。建议重点看XMLscriptBuilder#parseDynamicTags(XNode)结合对应的NodeHandler一块看,便于理解。下面举个例子来分析分析,分析的例子是一开始举得例子中的getStudentForTest(StudentQuery)方法。 一上来,发现是文本节点,并且文本里面的内容是select * from t_student没有${},将他封装为StaticTextSqlNode,此外还扽注意,这个方法最后的返回值都是MixedSqlNode 到了下一次的循环,通过节点类型判断,发现不是文本节点,获取对应的Handle来处理,可以发现这里对应的是org.apache.ibatis.scripting.xmltags.XMLscriptBuilder$WhereHandler,注意传递给他的contents是一个sqlNode的列表。下面来看看org.apache.ibatis.scripting.xmltags.XMLscriptBuilder$WhereHandler是怎么处理的。 他上来先调用parseDynamicTags,由掉回去了。因为在where里面我写了if,在走一遍上面的流程,就到了org.apache.ibatis.scripting.xmltags.XMLscriptBuilder$IfHandler。 在他里面也是先调用parseDynamicTags来处理if标签快里面的东西,对于普通的文本就是TextSqlNode。 来看看if里面操作了什么事情,拿到了if标签里面的test,和之前的MixedSqlNode一块封装为IfSqlNode,添加到传递进来的List 对于WhereHandler也是。同样的操作。 到这里,加载mapper文件,解析动态sql的操作已经分析完了 上面只是准备好了语法树,在运行的时候对于动态sql来说,需要判断不同的条件来组装不同的sql。所以,真正的sql是在运行的时候组装好的。 上面说了,sql的表示是SqlSource,通过调用他的BoundSql getBoundSql(Object)创建真正的sql,并且将参数的绑定等等信息封装为BoundSql对象。并且一个Mapper对应的是MapperStatement,所以,调用SqlSource#getBoundSql(Object)的地方一定是MapperStatement。这个内容在之前的Mybatis中说过。对应的代码在MappedStatement#getBoundSql(Object)里面。这里主要说的是DynamicSqlSource。 主要步骤 通过Configuration和传递进来的Object(参数)封装为DynamicContext,此外,在他里面还有StringJoiner用来拼接sql,ContextMap用来表示参数。 组装动态sql。 通过SqlSourceBuilder将#{}替换为?,替换原来的SqlSource,并且创建ParameterMapping用来表示参数之间的映射关系。 这里的替换是通过GenericTokenParser和TokenHandler的实现类(ParameterMappingTokenHandler)来做的。 重新调用SqlSource#getBoundSql(Object)方法。 下面结合例子来看看,例子还是一开头说的getStudentForTest(StudentQuery)方法。 组装之后的就是这个样子,#{}还是存在的,下面就是通过SqlSourceBuilder来替换掉他,并且封装参数。重点看ParameterMappingTokenHandler里面的handleToken方法。我们知道,在原始的写sql的时候是由?的,比如select * from t_student where id = ?,之后通过jdbc来设置参数,执行,这里干的就是这个事情,将#{}替换为?,并且按需封装参数。 从这就可以看到,已经处理完了,参数也搞好了。 到此,运行时通过参数来组装sql已经完成了。 到这里,Mybatis的动态sql的实现结束了。下面分析分析不同的SqlNode的实现逻辑。 和正常的switchCase差不多,没有匹配到,就走default @Override public boolean apply(DynamicContext context) { for (SqlNode sqlNode : ifSqlNodes) { if (sqlNode.apply(context)) { return true; } } if (defaultSqlNode != null) { defaultSqlNode.apply(context); return true; } return false; } 他是bind对应的SqlNode,bind标签会将value对应的属性值绑定在name对应的字段中,从而用在sql中,这里的expression就是value,从Ognl中获取值,放在DynamicContext中。 @Override public boolean apply(DynamicContext context) { final Object value = OgnlCache.getValue(expression, context.getBindings()); context.bind(name, value); return true; } 最简单,最普通的一种了,理论上来说,这是真正存放sql的节点了,别的节点基本都是包装节点。 @Override public boolean apply(DynamicContext context) { context.appendSql(text); return true; } 先通过参数和条件来计算条件是否满足,满足在操作,否则不 public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } 这是这里面比较复杂的一个了,创建FilteredDynamicContext来做代理,一上来还是正常的调用,在组装完了之后,会调用FilteredDynamicContext#applyAll()方法,在这个方法里面,会剔除指定的前缀和后缀,增加指定的前缀和后缀。剔除操作是通过字符串截取来做的。 @Override public boolean apply(DynamicContext context) { FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); boolean result = contents.apply(filteredDynamicContext); filteredDynamicContext.applyAll(); return result; }public void applyAll() { sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); if (trimmedUppercaseSql.length() > 0) { applyPrefix(sqlBuffer, trimmedUppercaseSql); applySuffix(sqlBuffer, trimmedUppercaseSql); } delegate.appendSql(sqlBuffer.toString()); } 很简单,之前说过了,一个混合的SqlNode @Override public boolean apply(DynamicContext context) { contents.forEach(node -> node.apply(context)); return true; } @Override public boolean apply(DynamicContext context) { //参数 Map 对应的是sql中的 , 会 直 接 将 s q l 中 的 {},会直接将sql中的 ,会直接将sql中的{}替换为传递的值 @Override public boolean apply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; }// 解析private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); }// 处理 private static class BindingTokenParser implements TokenHandler { private DynamicContext context; private Pattern injectionFilter; public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { this.context = context; this.injectionFilter = injectionFilter; } @Override public String handleToken(String content) { Object parameter = context.getBindings().get("_parameter"); if (parameter == null) { context.getBindings().put("value", null); } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { context.getBindings().put("value", parameter); } //获取值 Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" checkInjection(srtValue); //直接替换 return srtValue; } } SqlNode这样套娃能干啥?为啥不出错。 到此,结束了。 关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢
运行的时候,通过参数组装最终的sql