Mybatis源码 - createSqlSource()方法解析 占位符替换 动态sql标签解析

前言

Mybatis初始化的流程 中,有一个关键的方法叫做createSqlSource(),它在XMLStatementBuilder类中的parseStatementNode()方法中被调用,上一层的调用链为XMLMapperBuilder类中的buildStatementFromContext()方法。

顾名思义,buildStatementFromContext 就是从上下文中去构建Statement,来支撑后面jdbc对数据库的操作。

而createSqlSource()这个方法的作用:就是在jdbc操作数据库之前,进行了sql语句占位符的替换,将Mybatis中的占位符#{}替换成了jdbc能够解析的?号,同时将实际的参数值也保存了下来,同时 对于动态sql标签 也进行了解析处理。可以说 没有这一个步骤 后面的逻辑都没有办法执行。

最近学习源码的过程中刚好学到了这个地方,就在这里记录一下,个人学习过程难免有错误或不合理的地方,还希望各路大佬指正,谢谢!下面正文开始。

正文

LanguageDriver语言驱动器

方法的调用位置在XMLStatementBuilder.parseStatementNode()中,部分源码如下

java 复制代码
public class XMLStatementBuilder extends BaseBuilder {
  //省略成员变量及构造器...
  
  public void parseStatementNode() {
    //省略其他代码...
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    //省略其他代码...
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //省略其他代码...
  }

  private LanguageDriver getLanguageDriver(String lang) {
    Class<? extends LanguageDriver> langClass = null;
    if (lang != null) {
      langClass = resolveClass(lang);
    }
    return configuration.getLanguageDriver(langClass);
  }
}

可以看出createSqlSource方法 是由一个langDriver对象来调用的,这里其实就是一个语言驱动器,LanguageDriver是一个接口,下面还有多个实现类

而langDriver对象,是调用getLanguageDriver方法获取的,可以看出,最后是从configuration这个全局配置类中获取的,是调用的configuration的getLanguageDriver方法

这里可以再看一下getLanguageDriver方法,下面代码中可以看出 这里是通过调用一个getDefaultDriver()方法,返回了一个默认的driver

java 复制代码
  public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
    if (langClass == null) {
      return languageRegistry.getDefaultDriver();
    }
    languageRegistry.register(langClass);
    return languageRegistry.getDriver(langClass);
  }

这里再继续往下看一下,看看在哪里配置的默认值

java 复制代码
public class LanguageDriverRegistry {
  private Class<? extends LanguageDriver> defaultDriverClass;

  public LanguageDriver getDefaultDriver() {
    return getDriver(getDefaultDriverClass());
  }
  
  public Class<? extends LanguageDriver> getDefaultDriverClass() {
    return defaultDriverClass;
  }
}

可以看出是LanguageDriverRegistry类中的成员变量defaultDriverClass中,配置了默认的语言驱动器的类对象,那这个defaultDriverClass是在哪里配置的呢?

实际上,是在全局配置对象Configuration的无参构造中,被赋值的,源码如下:

java 复制代码
public class Configuration {
    
    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        typeAliasRegistry.registerAlias("LRU", LruCache.class);
        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

        typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

        typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
        typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

        typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
        typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
        typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
        typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
        typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
        typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
        typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

        typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
        typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

        languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
        languageRegistry.register(RawLanguageDriver.class);
    }
    //省略类中其他代码...
}

上面的源码中可以看出,在全局配置对象Configuration的无参构造中,进行一系列别名的注册、以及一些默认值的赋值

例如4-5行,进行了事务工厂别名的注册,这里决定了在xml配置文件中配置事务工厂的时候,可以直接使用别名JDBC或者MANAGED,来指定事务工厂了。根本原因 就是因为在上面的源码中,框架已经将别名配置好了。

在上面的代码中,可以看到在第33行,调用了languageRegistry.setDefaultDriverClass()方法 为defaultDriverClass赋了值,默认值为XMLLanguageDriver.class ,可以理解为,框架默认配置了一个xml语言驱动器,后续正是调用了这个驱动器中的createSqlSource方法 完成了sql占位符替换,以及动态sql标签的解析等操作。

createSqlSource()方法逻辑

由上面的源码可以看出,LanguageDriver.createSqlSource,实际就是调用的 XMLLanguageDriver 这个子类中的方法,源码如下:

java 复制代码
public class XMLLanguageDriver implements LanguageDriver {
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }
}

createSqlSource方法分为两步,

  1. 构建了XMLScriptBuilder对象
  2. 调用XMLScriptBuilder对象的parseScriptNode()完成占位符替换以及动态sql标签解析工作。

XMLScriptBuilder构造器

XMLScriptBuilder构造器中,除了进行了全局配置对象以及一些参数的传递(类似XMLConfigBuilder和XMLMapperBuilder,可以参考另一篇文章Mybatis源码 - 初始化流程 加载解析配置文件)之外,还调用了一个重要的方法:initNodeHandlerMap,这个方法中 是将一些可能会遇到的动态标签处理器,都设置了别名,并保存到了Map集合中。

java 复制代码
public class XMLScriptBuilder extends BaseBuilder {

  private final XNode context;
  private boolean isDynamic;
  private final Class<?> parameterType;
  private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

  public XMLScriptBuilder(Configuration configuration, XNode context) {
    this(configuration, context, null);
  }

  public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    initNodeHandlerMap();
  }

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

parseScriptNode()方法

该方法源码如下:

java 复制代码
  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

表面上能看到的逻辑

  1. 通过调用parseDynamicTags方法,传入context,返回了一个MixedSqlNode
  2. 根据一个标识isDynamic,创建了不同类型的SqlSource对象,进行了返回

下面就对这个过程 进行一个具体的分析

parseDynamicTags()方法

这个方法的名称 顾名思义 就是"解析动态标签",在Mybatis的sql语句规则中,分为两种节点:文本节点、元素节点。

  • 文本节点:就是纯文本的sql语句,包含${}和#{}的也算
  • 元素节点:带有动态sql标签的部分

下面这张图更直观一点:

当前方法的返回值是一个MixedSqlNode类型,顾名思义这个一个混合sql节点对象,里面有一个属性contents

java 复制代码
public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

在当前方法中,就是根据节点的不同,分别进行了处理,然后将多个节点,封装成不同的形式,保存到了contents这个属性中,然后将MixedSqlNode对象返回。

源码如下:

java 复制代码
  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        //如果当前节点为文本节点:创建了TextSqlNode对象
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //sql是否为动态Sql,即sql中是否包含${}
        if (textSqlNode.isDynamic()) {
          //是:直接将TextSqlNode添加到集合中,同时设置一个标识isDynamic,方便后续的处理
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          //否:直接将sql封装到一个StaticTextSqlNode,添加到集合中
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        //如果当前节点为元素节点
        //去nodeHandlerMap集合中查找,看是否存在和节点匹配的处理器,如果没有 直接抛出异常
        //nodeHandlerMap就是在XMLScriptBuilder的构造器中,封装了一系列动态标签的处理器
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //由匹配的处理器处理节点,并设置动态标识isDynamic
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    //最后将处理好的contents 封装到MixedSqlNode中,返回MixedSqlNode对象。
    return new MixedSqlNode(contents);
  }

上述代码的逻辑总结如下,代码中也有注释

  1. 判断当前循环到的sql节点类型,如果为文本节点,且不包含{}:返回StaticTextSqlNode对象;如果包含{}:返回TextSqlNode对象,并设置isDynamic标识。
  2. 如果当前为元素节点,先去判断当前节点名称,是否存在匹配的解析器,不存在:抛出异常;存在:获取到匹配的解析器进行处理,并设置isDynamic标识。
  3. 最后将处理好的contents 封装到MixedSqlNode中,返回MixedSqlNode对象。

封装SqlSource

在上一步中,已经封装好了所有的Sql节点,下面就要封装SqlSource对象并返回了,相关源码如下

java 复制代码
  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //如果sql为动态sql,封装DynamicSqlSource对象返回
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //如果sql不是动态sql,封装RawSqlSource对象返回
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

可以看出,封装的过程中 根据isDynamic这个动态标识的值,会封装成不同类型的SqlSource子类,下面简单说明一下SqlSource接口的结构,方便后续的阅读。

SqlSource介绍

SqlSource是一个接口,里面提供了一个getBoundSql方法,可以通过这个方法 来获取到解析后的sql,解析后的sql信息封装到了BoundSql对象中。

java 复制代码
public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}
BoundSql介绍
java 复制代码
public class BoundSql {
  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;
}

BoundSql重要属性的含义如下:

  • sql

    jdbc可以直接识别的sql语句,即使用?作为占位符。例如:select * from user where id = ?

  • parameterMappings

    封装了原来sql中 每个#{}里面属性的值,例如下面这个sql中,存在两个参数:id、name

    sql 复制代码
    select * from user where id = #{id} and name = #{name}

    这两个参数的值 会按顺序封装到parameterMappings这个集合中,这里还做了一层封装,封装为了ParameterMapping对象。

RawSqlSource

RawSqlSource类 是当sql不包含${}以及动态sql标签,仅作为一个纯文本sql时,封装而成的SqlSource对象,这里重点看一下它的解析过程

java 复制代码
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) {
    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);
  }

}

构造器中,显示通过getSql方法中的context.getSql()这一句 获取到了原本的Sql语句,然后调用了同名的构造方法,然后调用了sqlSourceParser.parse()方法

java 复制代码
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这个方法中使用了一个标签处理器ParameterMappingTokenHandler和标签解析器GenericTokenParser,去sql语句中解析以#{开头、以}结尾的部分,很明显这个部分就是查询参数的名称

parser.parse()方法中,进行了以#{开头、以}结尾的部分的解析

java 复制代码
  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    do {
      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);
            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);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

45行,有一处关键的代码:handler.handleToken(expression.toString());点开看一下

java 复制代码
    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

可以看出 这里就是将参数的值 封装成了一个ParameterMapping对象,然后添加到了parameterMappings这个集合中,而这个集合 就会传递到BoundSql中的parameterMappings属性。同时方法返回了? 号,完成了字符串的替换。

至此 就是RawSqlSource大概的处理过程了。

DynamicSqlSource

DynamicSqlSource类 是当sql为动态sql时,封装成的类型,它的结构如下:

java 复制代码
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);
    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);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }
}

它和RawSqlSource类的区别在于,它没有解析的过程,而是在每次调用getBoundSql方法获取sql的时候,都重新进行解析,这是因为动态sql标签有不确定性,例如传入标签内的变量值都不可控,所以每次获取 都要重新解析。

总结

综上所述,介绍了一下SqlSource对占位符的替换、对参数值的保存、对动态Sql标签的处理。个人认为还是很有学习的必要的,感谢您的阅读,有不对的地方还请指正,谢谢大佬~

相关推荐
IT_陈寒2 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码3 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo3 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志4 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..4 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects4 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰4 小时前
一些基础和问题解决
后端
陈随易5 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN5 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana5 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端