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标签的处理。个人认为还是很有学习的必要的,感谢您的阅读,有不对的地方还请指正,谢谢大佬~

相关推荐
这孩子叫逆几秒前
Spring Boot项目的创建与使用
java·spring boot·后端
罗曼蒂克在消亡1 小时前
2.3MyBatis——插件机制
java·mybatis·源码学习
coderWangbuer1 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上1 小时前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志1 小时前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba2 小时前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
千里码aicood3 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
cyt涛3 小时前
MyBatis 学习总结
数据库·sql·学习·mysql·mybatis·jdbc·lombok
程序员-珍3 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455663 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端