深入浅出Mybatis源码解析——BoundSql获取流程

前言

停更了一个多月,博主一直在忙于技术的学习与工作的繁忙之间,其实更多的是迷茫于技术中,但是想想还是要把Mybatis系列继续更新下去。博主也给自己在20年立了几个flag:深入学习Java、研究研究c++、第三个就是健身咯,只要是因为回首2019年,感觉自己在技术学习的道路上是在没走多少,因此想着这次必须要对自己狠一点了。

先不多说了,回顾一下前一篇文章,由于时隔太久,笔者都有点记不清了。在前一篇文章中简单的说了一下Mybatis获取Mapper代理对象的流程,当获取到mapper的代理对象后,便是SQLSession的执行流程了,这个稍微熟悉Mybatis的都是了解的。但我们学习Mybatis的时候,只是了解它的一个执行流程,对其内部的代码原理却并不了解,那么接下来就带大家来看看Mybatis中BoundSql获取流程

一、getBoundSql

在执行完SQLSession的流程后,便会进入SQL语句的执行过程了,但是此时我们的SQL还并不是一句可执行的语句,它需要经过参数绑定后才能进行真正的执行,上一篇文章我们说到了关于getBoundSQL的代码入口,如下:

	@Override
	public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
		try {
			// 根据传入的statementId,获取MappedStatement对象
			MappedStatement ms = configuration.getMappedStatement(statement);
			// 调用执行器的查询方法
			// RowBounds是用来逻辑分页(按照条件将数据从数据库查询到内存中,在内存中进行分页)
			// wrapCollection(parameter)是用来装饰集合或者数组参数
			return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
		} catch (Exception e) {
			throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
		} finally {
			ErrorContext.instance().reset();
		}
	}

从上面的代码我们似乎并没有看到getBoundSQL代码的字样,但是在前面我们说了,getBoundSQL是在query执行的前做的,那我们来看看上面的query方法,当你在按住Ctr后点击鼠标时,编辑器会提示你这个方法它有两个实现一个是BaseExecutor类中,一个在CachingExecutor类中,但是不管是这两个里面的哪个类,当你最后跟到的代码还是在MappedStatement这个类中。那我这里就以BaseExecutor中的代码为例了,代码如下:

  @Override
  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

在上面的代码中,看到了getBoundSql这个方法,但是这里并不是getBoundSql的核心代码,那继续看下去吧。代码如下:

  public BoundSql getBoundSql(Object parameterObject) {
	// 
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

上面代码中的逻辑,首先是通过parameterObject参数去获取boundSql对象,然后再通过boundSql去获取parameterMappings这个集合,若所获取到的集合null或为空,那么则new一个BoundSql对象,不管是获取到的还是创建出来的,最后都要进行一个遍历的过程。在遍历的过程中,需要通过rmId来到configuration对象中获取ResultMap对象,如果能获取到便进行这样一个运算hasNestedResultMaps |= rm.hasNestedResultMaps(),关于|=运算符,感兴趣的朋友可以自己去俩复习一下。其实就是哪rm.hasNestedResultMaps()写的二进制去和hasNestedResultMaps 的二进制进行一个运算。

由于我们此时要说的重点是getBoundSql,那么我们继续跟进去看看,此时在你按住Ctr后点击鼠标时,编辑器会提示你这个方法它在以下几个类中有实现:DynamicSqlSource、ProviderSqlSource、RawSqlSource、StaticSqlSource、VelocitySqlSource,在这这几个类中ProviderSqlSource已经被弃用,但是他们都是去实现了SqlSource这个类,StaticSqlSource中的getBoundSql归根结底还是new一个BoundSql对象,DynamicSqlSource和VelocitySqlSource类中的实现稍微复杂点,这里我们就以DynamicSqlSource这个类的实现为例,代码如下:

public class DynamicSqlSource implements SqlSource {

	private final Configuration configuration;
	private final SqlNode rootSqlNode;

	public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
		this.configuration = configuration;
		this.rootSqlNode = rootSqlNode;
	}

	@Override
	public BoundSql getBoundSql(Object parameterObject) {
		DynamicContext context = new DynamicContext(configuration, parameterObject);
		rootSqlNode.apply(context);
		// 创建SQL信息解析器
		SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
		// 获取入参类型
		Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
		// 执行解析:将带有#{}的SQL语句进行解析,然后封装到StaticSqlSource中
		SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
		// 将解析后的SQL语句还有入参绑定到一起(封装到一个对象中,此时还没有将参数替换到SQL占位符?)
		BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
		for (Map.Entry entry : context.getBindings().entrySet()) {
			boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
		}
		return boundSql;
	}

}

在上面的代码中首先通过configuration对象和传入的参数parameterObject来创建一个DynamicContext对象,然后把这个上下文对象交给rootSqlNode,其实就是一个SqlNode节点,这个节点类主要实现的就是SQL语句中一些类似if、foreach等标签的节点使用。当把上下文对象交给rootSqlNode后,又通过configuration对象来创建一个SqlSourceBuilder对象,这个对象是个SQL信息解析器,主要是用于带有#{}的SQL语句进行解析,然后封装到StaticSqlSource中,最后通过sqlSource来获取BoundSql,然后遍历BoundSql,来进行参数赋值,最后返回boundSql对象。

其实上面那段代码中,其主要的核心代码是parse方法,接下来我们继续看看看这个方法。

二、执行解析

	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);
		// 将解析之后的SQL信息,封装到StaticSqlSource对象中
		return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
	}

代码很简单,大体步骤就是:

  1. 创建ParameterMappingTokenHandler
  2. 创建GenericTokenParser
  3. 然后解析originalSql
  4. 最后将解析之后的SQL信息,封装到StaticSqlSource对象中

1).ParameterMappingTokenHandler

ParameterMappingTokenHandler其实是一个内部类,它继承了BaseBuilder,并且实现了TokenHandler,我们简单看下它的构造函数:

public ParameterMappingTokenHandler(Configuration configuration, Class parameterType,
		Map additionalParameters) {
	super(configuration);
	this.parameterType = parameterType;
	this.metaParameters = configuration.newMetaObject(additionalParameters);
}

这个构造函数里调了父类的方法,然后通过configuration创建了一个newMetaObject。

2).GenericTokenParser

GenericTokenParser这个类主要就是分词解析器,在它的构造函数里指定了待分析的openTokencloseToken,并指定处理 器。

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

3).解析#{}或${}

关于解析#{}代码比较复杂,我们直接看代码:

  /**
   * 解析${}和#{}
   * @param text
   * @return
   */
  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

这段代码主要就是解析${}和#{}的,具体细节童鞋们可以自己debug时细细品味,其实说到这里本篇文章也将要结束了,本篇文章主要讲了getBoundSql和执行解析。顺便补一下RawSqlSource的类实现。

 

三、RawSqlSource

public class RawSqlSource implements SqlSource {

	private final SqlSource sqlSource;

	public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) {
		this(configuration, getSql(configuration, rootSqlNode), parameterType);
	}

	public RawSqlSource(Configuration configuration, String sql, Class parameterType) {
		// 解析SQL语句
		SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
		// 获取入参类型
		Class clazz = parameterType == null ? Object.class : parameterType;
		// 开始解析
		sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
	}

	private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
		DynamicContext context = new DynamicContext(configuration, null);
		rootSqlNode.apply(context);
		return context.getSql();
	}

	@Override
	public BoundSql getBoundSql(Object parameterObject) {
		return sqlSource.getBoundSql(parameterObject);
	}

}

其实其原理大同小异,只是因为不同的需求实现方式有些诧异。

 

接下来和大家简单总结一下这个代码执行流程:

  • DynamicSqlSource#getBoundSql
    • SqlSourceBuilder#parse:解析SQL语句中的#{},并将对应的参数信息封装到ParameterMapping对象集合中,然后封装到StaticSqlSource
      • ParameterMappingTokenHandler#构造方法
      • GenericTokenParser#构造方法:指定待分析的openTokencloseToken,并指定处理
      • GenericTokenParser#parse:解析SQL语句,处理openTokencloseToken中的内容
        • ParameterMappingTokenHandler#handleToken:处理token#{}/${}
          • ParameterMappingTokenHandler#buildParameterMapping:创建ParameterMapping对象 
      • StaticSqlSource#构造方法:将解析之后的SQL信息,封装到StaticSqlSource
  • RawSqlSource#getBoundSql
    • StaticSqlSource#getBoundSql
      • BoundSql#构造方法:将解析后的sql信息、参数映射信息、入参对象组合到BoundSql对象

你可能感兴趣的:(Mybatis源码解析)