博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
mybaits源码分析(七) SqlSource及动态sql解析详细分析
阅读量:4074 次
发布时间:2019-05-25

本文共 15765 字,大约阅读时间需要 52 分钟。

 

SqlSource及动态sql解析

    mybaits可以通过#{}的方式插值,也可以${}方式拼接,另外一个最强大的功能就是可以动态的解析sql语句,而不管动态解析还是静态解析,sql都是通过不同的SqlSource实现进包装的,本文将深入讲解动态sql解析。

    一、sql解析加载的分析

1、主干解析方法XMLScriptBuilder的parseScriptNode。

 a) 解析<select>语句标签的Node节点

public SqlSource parseScriptNode() {	    // 核心: 解析动态sql , 并且动态sql的存储形式是以SqlNode节点列表的方式	    List
contents = parseDynamicTags(context); // SqlNode的节点 MixedSqlNode rootSqlNode = new MixedSqlNode(contents); SqlSource sqlSource = null; if (isDynamic) { // 创建动态SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { // 创建静态SqlSource (RawSqlSource是StaticSqlSource简单包装) sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }

   b) 解析动态sqlparseDynamicTags

private List
parseDynamicTags(XNode node) { List
contents = new ArrayList
(); NodeList children = node.getNode().getChildNodes(); //

 2、流程总结

        从主干解析流程我们知道,mybaits的sql解析是把语句标签内的node节点,最终解析成SqlNode,而SqlNode包含文本节点、if节点等等,如果我们只有静态SqlNode,那么最终把这些SqlNode会包装成StaticSqlSource,否则就会包装成DynamicSqlSource,下面我们先介绍下SqlNode和SqlSource的主要类结构,方便后续细节分析。

下面用个图演示解析后sql节点的组合方式。

      3、 SqlNode介绍

public interface SqlNode {	 	  // 备注: apply是sql运行时期用的,因为这个时候才有sql参数,才可以解析OGNL,返回false,不进行后续节点的解析。		  boolean apply(DynamicContext context);		}

     这是SqlNode的接口方法,apply方法主要是对SqlNode的sql片段进行处理,组成最终的sql预处理语句用途,解析动态sql主要依赖OGNL解析的。 DynamicContext我们先想象成一个map存储外部运行参数,是拼接sql节点的用途。        

 主要实现:

             IfSqlNode: if节点包装 , apply逻辑是判断表达式用运行参数OGNL解析是否ture,决定后续是否拼接此节点sql。
             TextSqlNode: 文本节点,含有${}占位符,apply逻辑就是运行参数OGNL解析替换占位符
             StaticTextSqlNode:纯静态文本节点,只存储sql片段。
             MixedSqlNode : 多个SqlNode节点的list,apply逻辑就是调用每个SqlNode进行apply。
             ...
        SqlNode的运行时最终解析apply方法是,文本节点用OGNL处理${}占位参数,静态文本节点直接添加,动态节点如果是if节点那么就用OGNL用运行参数解析test值,返回true就添加本节点sql,其他动态节点的apply类似逻辑,另外所有sql都拼接好了,还会处理#{}占位,这个就是直接替换?,解析paramterMapping。SqlSource类介绍时会讲解。

  4 SqlSource介绍

顶级接口:

public interface SqlSource {		  BoundSql getBoundSql(Object parameterObject);		}

主要实现:

        DynamicSqlSource: 动态sqlSource  内部含有组合的MixedSqlNode,调用apply,拼接好sql也会处理#{}

        StaticSqlSource:静态sqlSource  直接含有最终预处理语句。
        RawSqlSource:包装静态sqlSource, 主要是处理#{},从这个解析成?,并且构建出ParameterMapping。

依赖类:

        BoundSql:包装最终sql,paramterMapping、参数值。

public class DynamicSqlSource implements SqlSource {          private Configuration configuration;          private SqlNode rootSqlNode;  // 顶级SqlNode          public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {            this.configuration = configuration;            this.rootSqlNode = rootSqlNode;          }          public BoundSql getBoundSql(Object parameterObject) {            DynamicContext context = new DynamicContext(configuration, parameterObject); // 实际参数添加到DynamicContext            // 所有SqlNode的apply操作,动态node采用CGNL解析,最后拼接sql到context的sql属性            rootSqlNode.apply(context);             SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);            Class
parameterType = parameterObject == null ? Object.class : parameterObject.getClass();            // 因为上面处理的sql没有处理#{}的占位,并且#{}可以解析出ParameterMapping,替换成?            SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());            BoundSql boundSql = sqlSource.getBoundSql(parameterObject);            // 实际参数放到_parameter为key的AdditionalParameter的Map,添加到boundSql            for (Map.Entry
entry : context.getBindings().entrySet()) {               boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());            }            return boundSql;          }        }

 

     二、SqlSource的使用阶段

SqlSource解析好了以后,是存储到哪里的?在那里使用?我们进入SqlSource使用阶段的解读。

 1、存储在那里?

     SqlSource不管是动态sql还是非动态sql,其存储的位置肯定是对应一个语句对象,而语句对象就是MappedStatement,

    public final class MappedStatement {          private SqlSource sqlSource;

2、在那里使用?

我们回顾mybaits的主要执行过程,创建Sqlsession,执行查询的时候,sqlsession会根据namespace+方法id找到具体的  MappedStatement,从MappedStatement取出SqlSource并且结合外部传递的参数,处理sqlSource解析成预处理语句+参数映射包装成BoundSql就是具体使用的过程。

 Sqlsession的selectList方法

public 
List
selectList(String statement, Object parameter, RowBounds rowBounds) { MappedStatement ms = configuration.getMappedStatement(statement); // 取出MappedStatement List
result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); return result; }

      Executor的query方法

public 
List
query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 解析最终预处理语句+ParameterMapping CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }

   3、动态sqlNode解析

  从SqlSource的getBoundSql可以看到,顶级sqlnode会执行apply,而每个元素节点内部都是由MixedNode组成,这样会从上而下调用每个sqlNode的apply方法,apply方法的参数DynamicContext负责包装运行时sql参数,并且负责拼接sql语句,以及提供了ONGL的一些扩展。下面看下部分sqlNode的apply方法。

DynamicContext 拼接sql的方法 public void appendSql(String sql) {    sqlBuilder.append(sql);    sqlBuilder.append(" ");  }  public String getSql() {    return sqlBuilder.toString().trim();  }
组合节点,内部调用每个组合节点applypublic class MixedSqlNode implements SqlNode {  private List
contents; public MixedSqlNode(List
contents) { this.contents = contents; } public boolean apply(DynamicContext context) { for (SqlNode sqlNode : contents) { sqlNode.apply(context); } return true; }}
文本节点(含${})public class TextSqlNode implements SqlNode {  private String text;  public TextSqlNode(String text) {    this.text = text;  }    public boolean apply(DynamicContext context) {   // 处理${}    GenericTokenParser parser = createParser(new BindingTokenParser(context));   // 拼接sql    context.appendSql(parser.parse(text));    return true;  }    // 处理${}  private GenericTokenParser createParser(TokenHandler handler) {     return new GenericTokenParser("${", "}", handler);  }  // handler方法实现  private static class BindingTokenParser implements TokenHandler {    private DynamicContext context;    public BindingTokenParser(DynamicContext context) {      this.context = context;    }        public String handleToken(String content) {      // 从map的_parameter属性取出外部参数      Object parameter = context.getBindings().get("_parameter");      if (parameter == null) {        context.getBindings().put("value", null);      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {        // 设置简单类型的参数到value        context.getBindings().put("value", parameter);      }      // 用ognl取值      Object value = OgnlCache.getValue(content, context.getBindings());      return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"    }  }
if节点public class IfSqlNode implements SqlNode {  private ExpressionEvaluator evaluator;   private String test;  private SqlNode contents;  public IfSqlNode(SqlNode contents, String test) {    this.test = test;    this.contents = contents;    this.evaluator = new ExpressionEvaluator();  }  public boolean apply(DynamicContext context) {    // ognl取test的表达式计算结果    if (evaluator.evaluateBoolean(test, context.getBindings())) {      // 此if节点的内部MixedNode全部applye      contents.apply(context);      return true;    }    return false;  }}
纯静态节点StaticTextSqlNodepublic class StaticTextSqlNode implements SqlNode {  private String text;  public StaticTextSqlNode(String text) {    this.text = text;  }   public boolean apply(DynamicContext context) {    // 直接拼接sql    context.appendSql(text);    return true;  }}

(OGNL的使用后面会补充一个基本测试案例)

4、解析参数的具体实现

动态sqlsource的getBoundSql方法上面已经做了基本介绍,我们关注运行时,参数如何解析成ParameterMapping。

// getBoundSql方法片段

 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

  // 解析#{}

public SqlSource parse(String originalSql, Class
parameterType, Map
additionalParameters) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }

 // GenericTokenParser以前已经介绍过,就是根据占位符调用合适的处理器处理,我们直接看处理器实现。

public String handleToken(String content) {	      parameterMappings.add(buildParameterMapping(content)); // 每找到一个#{}创建一个parameterMapping	      return "?";	    }		    private ParameterMapping buildParameterMapping(String content) {	      Map
propertiesMap = parseParameterMapping(content); // 创建一个map String property = propertiesMap.get("property"); Class
propertyType; if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params propertyType = metaParameters.getGetterType(property); } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; // 存在注册类型处理器的类型直接赋值给propertyType } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { propertyType = java.sql.ResultSet.class; } else if (property != null) { // 不存在的类型,但是值不为空 MetaClass metaClass = MetaClass.forClass(parameterType); // 反射class if (metaClass.hasGetter(property)) { // 存在get方法 propertyType = metaClass.getGetterType(property); // 设置类型为get方法的类型 } else { propertyType = Object.class; // 其他情况就是Object的类型 } } else { propertyType = Object.class; } 拿到参数值和参数类型,下面就是构建ParameterMapping的过程。 ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); // ..... return builder.build(); }

  总结
    

    mybaits的动态sql实现,分为创建时,将sql片段包装成一个个的SqlNode节点,这个SqlNode节点如果是元素节点对应解析的,那么就是一个组合的SqlNode,否则就是单SqlNode,SqlNode会包装到DynamicSqlSource中,如果是运行时的时候,会调用顶级节点的apply方法,即所有节点都会执行这个apply方法,最终利用Ognl将实际参数解析这些动点,并且这个过程还存在拼接sql的过程,拼接完所有的sql节点后,最终进行#{}的处理,解析出ParameterMapping,替换#{}为?,最终将预处理语句和ParameterMapping包装成BoundSql供后续使用。'

 

 补充:Ognl的基本使用测试。

/** * Ognl表达式测试 *  * 概述.Ognl表达式语言 *  * 从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 , * 它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,java 中很多可以 * 做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用  * OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个“路径”来完 * 成对象信息的导航,这个“路径”可以是到 java bean 的某个属性,或者集合中的某个索 * 引的对象,等等,而不是直接使用 get 或者 set 方法来完成。 *  * 首先来介绍下OGNL的三要素: *  * 1、表达式:表达式(Expression)是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析 * 后进行的。通过表达式来告诉OGNL操作到底要干些什么。因此,表达式其实是一个带有语法含义 * 的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量的表达式,如“链式访问对象”、表达式计算s *  * 2、Root对象: *  OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是 * 哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root * 对象来进行OGNL表达式的计算并且返回结果。 *  * 3、上下文环境 *  有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在 * OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下 * 文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。 *  */public class LearnOgnl {	/**	 * 1、访问root对象, 使用Api是Ognl.getValue(express, root)	 */	@Test	public void test1() {		Person person = new Person("id", "name");		MetaObject meta = MetaObject.forObject(person, new DefaultObjectFactory(), new DefaultObjectWrapperFactory());		meta.setValue("child.name", "haha"); // 简化构建对象操作,用MetaObject创建子对象属性。		try {			System.out.println(Ognl.getValue("name", person)); // name			System.out.println(Ognl.getValue("child", person)); // Person [id=null, name=haha, children=[], child=null]			System.out.println(Ognl.getValue("child.name", person)); // haha		} catch (OgnlException e) {			e.printStackTrace();		}	}		/**	 * 2、上下文对象访问,上下文对象访问需要用#获取,使用Api是Ognl.getValue(express, context, root)	 */	@Test	public void test2() throws OgnlException {		Person person = new Person("id", "name");		MetaObject meta = MetaObject.forObject(person, new DefaultObjectFactory(), new DefaultObjectWrapperFactory());		meta.setValue("child.name", "haha");		Map
context = new HashMap
(); context.put("person", person); System.out.println(Ognl.getValue("#person.id", context, person)); // id System.out.println(Ognl.getValue("#person.name", context, person)); // name System.out.println(Ognl.getValue("name", context, person)); // name } /** * 3、方法调用, ognl可以调用方法,并且传入上下文参数。 */ @Test public void test3() throws OgnlException { Person person = new Person(); Map
context = new HashMap
(); context.put("name", "张三"); Ognl.getValue("setName(#name)", context, person); System.out.println(Ognl.getValue("getName()", context, person)); // 张三 } /** * 4、数组集合的访问 ,从此案例可以看出对一些运算符能够支持, * 运算符常见支持如下: * 减法、乘法、除法、取余 (2+5、6/2、6%2) * 字符串相加 ( "str1" + "str2") * 逻辑判断 (i == j) */ @Test public void test4() { Person person = new Person(); Map
context = new HashMap
(); String[] strings = { "aa", "bb" }; ArrayList
list = new ArrayList
(); list.add("aa"); list.add("bb"); Map
map = new HashMap
(); map.put("key1", "value1"); map.put("key2", "value2"); context.put("list", list); context.put("strings", strings); context.put("map", map); try { System.out.println(Ognl.getValue("#strings[0]", context, person)); System.out.println(Ognl.getValue("#list[0]", context, person)); System.out.println(Ognl.getValue("#list[0 + 1]", context, person)); System.out.println(Ognl.getValue("#map['key1']", context, person)); System.out.println(Ognl.getValue("#map['key' + '2']", context, person)); } catch (OgnlException e) { e.printStackTrace(); } } /** * 5、演示mybaits解析test表达的表达式 */ @Test public void test() throws OgnlException { String express = "begin!=null and end!=null"; Map
map = new HashMap
(); map.put("begin", null); map.put("end", null); Object ok = Ognl.getValue(express, map, map); System.out.println(ok); // false map.put("begin", ""); map.put("end", ""); ok = Ognl.getValue(express, map, map); System.out.println(ok); // true } }class Person{ private String id; private String name; private List
children = new ArrayList
(); private Person child; public Person(String id, String name) { this.id = id; this.name = name; } public Person() { super(); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List
getChildren() { return children; } public void setChildren(List
children) { this.children = children; } public Person getChild() { return child; } public void setChild(Person child) { this.child = child; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + ", children=" + children + ", child=" + child + "]"; } }

 

 

 

 

end !

 

 

转载地址:http://sruni.baihongyu.com/

你可能感兴趣的文章
JVM并发机制探讨—内存模型、内存可见性和指令重排序
查看>>
nginx+tomcat+memcached (msm)实现 session同步复制
查看>>
WAV文件解析
查看>>
WPF中PATH使用AI导出SVG的方法
查看>>
QT打开项目提示no valid settings file could be found
查看>>
android 代码实现圆角
查看>>
java LinkedList与ArrayList迭代器遍历和for遍历对比
查看>>
drat中构造方法
查看>>
JavaScript的一些基础-数据类型
查看>>
coursesa课程 Python 3 programming 统计文件有多少单词
查看>>
coursesa课程 Python 3 programming course_2_assessment_7 多参数函数练习题
查看>>
coursesa课程 Python 3 programming course_2_assessment_8 sorted练习题
查看>>
多线程使用随机函数需要注意的一点
查看>>
getpeername,getsockname
查看>>
所谓的进步和提升,就是完成认知升级
查看>>
如何用好碎片化时间,让思维更有效率?
查看>>
No.182 - LeetCode1325 - C指针的魅力
查看>>
Encoding Schemes
查看>>
带WiringPi库的交叉笔译如何处理二之软链接概念
查看>>
Java8 HashMap集合解析
查看>>