MyBatis06-"General Source Code Guide: Detailed Explanation of MyBatis Source Code" notes-scripting package

This series of articles is my notes and summary from the book "General Source Code Guide: Detailed Explanation of MyBatis Source Code
" . This book is based on MyBatis-3.5.2 version. The author of the book is Brother Yi . The link is Brother Yi's Weibo in CSDN. But among all the articles I read, there was only one that briefly introduced this book. It doesn’t reveal too much about the charm of the book. Next, I will record my learning summary. If the author thinks that I have infringed the copyright, please contact me to delete it. Thanks again to Brother Yi for providing learning materials. This explanation will accompany the entire series of articles. Respect the originality . I have purchased the revised book on WeChat Reading.
Copyright statement: This article is an original article by CSDN blogger "Architect Yi Ge" and follows the CC 4.0 BY-SA copyright agreement. Please attach the original source link and this statement when reprinting.
Original link: https://blog.csdn.net/onlinedct/article/details/107306041

MyBatis supports a very flexible way of constructing SQL statements. We can use foreach, where, if and other tags to complete complex statement assembly work when constructing SQL statements.

<select id="selectUsers" resultMap="userMapFull">
    SELECT *
    FROM `user`
    WHERE `id` IN
    <foreach item="id" collection="array" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

The statement will eventually be parsed into the most basic SQL statement before it can be received by the database. This parsing process is mainly completed by the scripting package.

1.OGNL

OGNL (Object Graph Navigation Language) is a powerful expression language (Expression Language, EL). Through it, operations such as selecting objects from collections, reading and writing object properties, calling methods of objects and classes, expression evaluation and judgment, etc. can be completed. OGNL is widely used. For example, it is also possible to obtain the attributes of an object in a Map, which is expressed in Java language as follows.

userMap.get("user2").getName();

The expression using OGNL is:

#user2.name

In addition to being simple and clear, OGNL has higher environmental adaptability. We can apply OGNL expressions to configuration files, XML files, etc., and only use OGNL when parsing these files.
OGNL has a Java toolkit. As long as it is introduced, you can use the functions of OGNL in Java. You can use Java to parse various documents that introduce OGNL. Before introducing the usage of OGNL, let's first introduce three important concepts that need to be touched when parsing OGNL.

  • Expression: It is a string with grammatical meaning and is the core content of the entire OGNL. Use expressions to determine the required OGNL operations.
  • Root object (root): can be understood as the operated object of OGNL. The operation expressed in the expression is expanded on this object.
  • Context: The context of the entire OGNL processing, which is a Map object. Before performing OGNL processing, we can pass in an initialized context environment.
    public static void main(String[] args) {
    
    
        try {
    
    
            User user01 = new User(1, "易哥", 18);
            User user02 = new User(2, "莉莉", 15);
            Map<String, User> userMap = new HashMap<>();
            userMap.put("user1", user01);
            userMap.put("user2", user02);

            // Java方式读取列表中对象的属性值
            String userName = userMap.get("user2").getName();
            System.out.println(userName);

            readAndWriteProps(userMap);
            callFunction();
            runAfterParse(userMap);
        } catch (Exception ex) {
    
    
            ex.printStackTrace();
        }

    }

    public static void readAndWriteProps(Map<String, User> userMap) throws Exception {
    
    
        // 使用表达式读写根对象中信息的示例
        // 该示例中要用到的OGNL函数:
        // getValue(String expression, Object root) :对root内容执行expression中的操作,并返回结果

        // 读取根对象的属性值
        Integer age = (Integer) Ognl.getValue("age", userMap.get("user1"));
        System.out.println("读取根对象属性,得到age:" + age);
        // 设置根对象的属性值
        Ognl.getValue("age = 19", userMap.get("user1"));
        age = (Integer) Ognl.getValue("age", userMap.get("user1"));
        System.out.println("设置根对象属性后,得到age:" + age);

        // 使用表达式读写环境中信息的示例
        // 该示例中要用到的OGNL函数:
        // getValue(String expression, Map context, Object root) :在context环境中对root内容执行expression中的操作,并返回结果

        // 读取环境中的信息
        String userName2 = (String) Ognl.getValue("#user2.name", userMap, new Object());
        System.out.println("读取环境中的信息,得到user2的name:" + userName2);
        // 读取环境中的信息,并进行判断
        Boolean result = (Boolean) Ognl.getValue("#user2.name != '丽丽'", userMap, new Object());
        System.out.println("读取环境中的信息,并进行判断,得到:" + result);
        // 设置环境中的信息
        Ognl.getValue("#user2.name = '小华'", userMap, new Object());
        String newUserName = (String) Ognl.getValue("#user2.name", userMap, new Object());
        System.out.println("设置环境中的信息后,得到user2的name:" + newUserName);
    }
/*
莉莉
读取根对象属性,得到age:18
设置根对象属性后,得到age:19
读取环境中的信息,得到user2的name:莉莉
读取环境中的信息,并进行判断,得到:true
设置环境中的信息后,得到user2的name:小华
*/

Java uses OGNL to complete the reading and writing operations of object information and context information, and the entire operation process also supports logical operations.
OGNL can not only read and write information, but also call methods in objects and classes.

// 调用对象方法
 Integer hashCode = (Integer) Ognl.getValue("hashCode()", "yeecode");
 System.out.println("对字符串对象调用hashCode方法得到:" + hashCode);
 // 调用类方法
 Double result = (Double)Ognl.getValue("@java.lang.Math@random()", null);
 System.out.println("调用Math类中的静态方法random,得到:" + result);
/*
对字符串对象调用hashCode方法得到:-1293325242
调用Math类中的静态方法random,得到:0.2308049275052454
*/

OGNL supports pre-compilation of expressions. Pre-compilation of expressions avoids the compilation work before each execution of the expression, which can significantly improve the execution efficiency of OGNL.

 String userName;

// 先对表达式解析,然后再执行可以提高效率
long time1 = new Date().getTime();
// 解析表达式
Object expressionTree = Ognl.parseExpression("#user2.name");
// 重复运行多次
for (int i = 0; i < 10000; i++) {
    
    
    userName = (String) Ognl.getValue(expressionTree, userMap, new Object());
}
long time2 = new Date().getTime();
// 直接重复运行多次
for (int i = 0; i < 10000; i++) {
    
    
    userName = (String) Ognl.getValue("#user2.name", userMap, new Object());
}
long time3 = new Date().getTime();

System.out.println("编译之后再执行,共花费" + (time2 - time1) + "ms");
System.out.println("不编译直接执行,共花费" + (time3 - time2) + "ms");

/*
编译之后再执行,共花费19ms
不编译直接执行,共花费306ms
*/

It can be seen that if you want to run an expression multiple times, it is more efficient to compile it first and then run it. We often see OGNL expressions in JSP and XML, and the parsing of these expressions is carried out through the method introduced in this section. It can be seen that OGNL is a broad, convenient and powerful language.

2. Language driver interface and language driver registry

LanguageDriver is the interface of the language driver class, which defines a total of three methods.

// 脚本语言解释器
// 在接口上注解的SQL语句,就是由它进行解析的
// @Select("select * from `user` where id = #{id}")
//User queryUserById(Integer id);
public interface LanguageDriver {
    
    
  /**
   * 创建参数处理器。参数处理器能将实参传递给JDBC statement。
   * @param mappedStatement 完整的数据库操作节点
   * @param parameterObject 参数对象
   * @param boundSql 数据库操作语句转化的BoundSql对象
   * @return 参数处理器
   */
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  /**
   * 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
   * @param configuration 配置信息
   * @param script 映射文件中的数据库操作节点
   * @param parameterType 参数类型
   * @return SqlSource对象
   */
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  /**
   * 创建SqlSource对象(基于注解的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
   * @param configuration 配置信息
   * @param script 注解中的SQL字符串
   * @param parameterType 参数类型
   * @return SqlSource对象,具体来说是DynamicSqlSource和RawSqlSource中的一种
   */
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

The LanguageDriver interface has two implementations by default, namely XMLLanguageDriver and RawLanguageDriver
, and RawLanguageDriver is a subclass of XMLLanguageDriver. All operations of the RawLanguageDriver class are completed by calling the parent class XMLLanguageDriver. And after the XMLLanguageDriver class completes the operation, the SqlSource obtained through checkIsNotDynamic method verification must be RawSqlSource. Therefore, the RawLanguageDriver class actually tailors the functions of the XMLLanguageDriver class through the checkIsNotDynamic method, so that it only supports SqlSource of the RawSqlSource type.

public class RawLanguageDriver extends XMLLanguageDriver {
    
    
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    
    
    // 调用父类方法完成操作
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    // 校验得到的SqlSource是RawSqlSource
    checkIsNotDynamic(source);
    return source;
  }
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    
    
    // 调用父类方法完成操作
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    // 校验得到的SqlSource是RawSqlSource
    checkIsNotDynamic(source);
    return source;
  }
  /**
   * 校验输入的SqlSource是RawSqlSource,否则便抛出异常
   * @param source 输入的SqlSource对象
   */
  private void checkIsNotDynamic(SqlSource source) {
    
    
    if (!RawSqlSource.class.equals(source.getClass())) {
    
    
      throw new BuilderException("Dynamic content is not allowed when using RAW language");
    }
  }
}

In object-oriented design, subclasses usually extend more methods based on inheriting the methods of the parent class, so the functions of the subclass are a superset of the functions of the parent class. The RawLanguageDriver class has tailored the functions of its parent class, XMLLanguageDriver, so that its own functions are a subset of the parent class's functions. This is a design approach that is complex first and then simplified. When we encounter similar needs during development, we can refer to this design method.

MyBatis also allows users to provide their own LanguageDriver implementation class and specify it as the default script driver through the defaultScriptingLanguage attribute in the configuration file. Support for this feature is implemented by the source code in the XMLConfigBuilder class. Here MyBatis will try to set the default language driver based on the user's configuration in defaultScriptingLanguage.

configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
//在Configuration类中给定了默认语言驱动
public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
    
    
 if (driver == null) {
    
    
   driver = XMLLanguageDriver.class;
 }
 getLanguageRegistry().setDefaultDriverClass(driver);
}

LanguageDriverRegistry class, which manages all language drivers as a language driver registry. The LanguageDriverRegistry class mainly includes methods for registering drivers and selecting drivers, and the implementation is relatively simple.

3. Construction of SQL node tree

<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
    SELECT * FROM `user`
    <where>
        <if test="name != null">
            `name` = #{name}
        </if>
        <if test="schoolName != null">
            AND `schoolName` = #{schoolName}
        </if>
    </where>
</select>

To parse this tree, the first thing to do is to read the information in the XML, and then build the XML tree into a SQL node tree in memory. The construction of the SQL node tree is handled by the XMLScriptBuilder class. Property list:

  // 当前要处理的XML节点
  private final XNode context;
  // 当前节点是否为动态节点
  private boolean isDynamic;
  // 输入参数的类型
  private final Class<?> parameterType;
  // 节点类型和对应的处理器组成的Map
  private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

In the XMLScriptBuilder class, an interface NodeHandler is defined. It has a handleNode method responsible for assembling nodes into the node tree.

  private interface NodeHandler {
    
    
    /**
     * 该方法将当前节点拼装到节点树中
     * @param nodeToHandle 要被拼接的节点
     * @param targetContents 节点树
     */
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }

Each type of SQL node has a NodeHandler implementation class. The corresponding relationship between SQL nodes and NodeHandler implementation classes is stored by nodeHandlerMap.
Class diagram of the NodeHandler interface and its implementation class:
Insert image description here
Taking IfHandler as an example, we look at how to build a SQL node tree based on XML information.

/**
 * 该方法将当前节点拼装到节点树中
 * @param nodeToHandle 要被拼接的节点
 * @param targetContents 节点树
 */
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    
    
  // 解析该节点的下级节点
  MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
  // 获取该节点的test属性
  String test = nodeToHandle.getStringAttribute("test");
  // 创建一个IfSqlNode
  IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
  // 将创建的IfSqlNode放入到SQL节点树中
  targetContents.add(ifSqlNode);
}

After understanding the NodeHandler interface and its implementation class, let's take a look at how to build a SQL node tree starting from the root node. The entry method is the parseScriptNode method, and the main operations are carried out in the parseDynamicTags method.

/**
* 解析节点生成SqlSource对象
* @return SqlSource对象
*/
public SqlSource parseScriptNode() {
    
    
 // 解析XML节点节点,得到节点树MixedSqlNode
 MixedSqlNode rootSqlNode = parseDynamicTags(context);
 SqlSource sqlSource;
 // 根据节点树是否为动态,创建对应的SqlSource对象
 if (isDynamic) {
    
    
   sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
 } else {
    
    
   sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
 }
 return sqlSource;
}

/**
* 将XNode对象解析为节点树
* @param node XNode对象,即数据库操作节点
* @return 解析后得到的节点树
*/
protected MixedSqlNode parseDynamicTags(XNode node) {
    
    
 // XNode拆分出的SqlNode列表
 List<SqlNode> contents = new ArrayList<>();
 // 输入XNode的子XNode
 NodeList children = node.getNode().getChildNodes();
 for (int i = 0; i < children.getLength(); i++) {
    
    
   // 循环遍历每一个子XNode
   XNode child = node.newXNode(children.item(i));
   if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
    
     // CDATASection类型或者Text类型的XNode节点
     // 获取XNode内的信息
     String data = child.getStringBody("");
     TextSqlNode textSqlNode = new TextSqlNode(data);
     // 只要有一个TextSqlNode对象是动态的,则整个MixedSqlNode是动态的
     if (textSqlNode.isDynamic()) {
    
    
       contents.add(textSqlNode);
       isDynamic = true;
     } else {
    
    
       contents.add(new StaticTextSqlNode(data));
     }
   } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
    
     // 子XNode仍然是Node类型
     String nodeName = child.getNode().getNodeName();
     // 找到对应的处理器
     NodeHandler handler = nodeHandlerMap.get(nodeName);
     if (handler == null) {
    
    
       throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
     }
     // 用处理器处理节点
     handler.handleNode(child, contents);
     isDynamic = true;
   }
 }
 // 返回一个混合节点,其实就是一个SQL节点树
 return new MixedSqlNode(contents);
}

parseDynamicTags will analyze the nodes in the XML file step by step and use the corresponding NodeHandler implementation to process the node, and finally integrate all the nodes into a MixedSqlNode object. The MixedSqlNode object is a SQL node tree. In the process of integrating the node tree, as long as there is a dynamic node, the SQL node tree is dynamic. The dynamic SQL node tree will be used to create DynamicSqlSource objects, otherwise a RawSqlSource object will be created.

4. Parsing of SQL node tree

Parsing the assembled SQL node tree is a very important task in MyBatis. This part of the work is mainly completed in the xmltags sub-package of the scripting package.
The parsing of these OGNL expressions is completed based on the OGNL package. You can see this in the pom.xml file of MyBatis:

<dependency>
 <groupId>ognl</groupId>
 <artifactId>ognl</artifactId>
 <version>3.2.10</version>
 <scope>compile</scope>
 <optional>true</optional>
</dependency>

In order to better complete the parsing work of OGNL, three related classes are also set up in the xmltags sub-package.

  1. The DefaultClassResolver class is a class defined in OGNL. OGNL can read classes through this class, that is, convert the class name into a class. The OgnlClassResolver inherits the DefaultClassResolver class and overrides toClassForName in it. OGNL can use the Resources class in MyBatis to complete class reading when working.
@Override
protected Class toClassForName(String className) throws ClassNotFoundException {
    
    
  return Resources.classForName(className);
}
  1. The MemberAccess interface is a hook interface provided by OGNL. OGNL uses this interface to prepare for accessing object properties. The OgnlMemberAccess class implements the MemberAccess interface and provides the function of modifying the accessibility of object attributes based on reflection. OGNL can then prepare to access the object's properties based on these functions.
 /**
  * 设置属性的可访问性
  * @param context 环境上下文
  * @param target 目标对象
  * @param member 目标对象的目标成员
  * @param propertyName 属性名称
  * @return 属性的可访问性
  */
 @Override
 public Object setup(Map context, Object target, Member member, String propertyName) {
    
    
   Object result = null;
   if (isAccessible(context, target, member, propertyName)) {
    
     // 如果允许修改属性的可访问性
     AccessibleObject accessible = (AccessibleObject) member;
     if (!accessible.isAccessible()) {
    
     // 如果属性原本不可访问
       result = Boolean.FALSE;
       // 将属性修改为可访问
       accessible.setAccessible(true);
     }
   }
   return result;
 }
  1. OgnlCache class In order to improve the operating efficiency of OGNL, MyBatis also provides a caching OgnlCache class for OGNL.
public final class OgnlCache {
    
    
  // MyBatis提供的OgnlMemberAccess对象
  private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess();
  // MyBatis提供的OgnlClassResolver对象
  private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver();
  // 缓存解析后的OGNL表达式,用以提高效率
  private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
  private OgnlCache() {
    
    
    // Prevent Instantiation of Static Class
  }

  /**
   * 读取表达式的结果
   * @param expression 表达式
   * @param root 根环境
   * @return 表达式结果
   */
  public static Object getValue(String expression, Object root) {
    
    
    try {
    
    
      // 创建默认的上下文环境
      Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
      // 依次传入表达式树、上下文、根,从而获得表达式的结果
      return Ognl.getValue(parseExpression(expression), context, root);
    } catch (OgnlException e) {
    
    
      throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
    }
  }
  /**
   * 解析表达式,得到解析后的表达式树
   * @param expression 表达式
   * @return 表达式树
   * @throws OgnlException
   */
  private static Object parseExpression(String expression) throws OgnlException {
    
    
    // 先从缓存中获取
    Object node = expressionCache.get(expression);
    if (node == null) {
    
    
      // 缓存没有则直接解析,并放入缓存
      node = Ognl.parseExpression(expression);
      expressionCache.put(expression, node);
    }
    return node;
  }
}

In the OgnlCache class, the expression is pre-parsed using the parseExpression method, and the result of the expression parsing is cached in the expressionCache attribute. In this way, every time an expression is parsed, the parsed results will first be queried from the expressionCache attribute. This avoids repeated parsing and improves the efficiency of OGNL operations.

4.1 Expression evaluator

MyBatis does not directly expose the OGNL tool to each SQL node. Instead, it further encapsulates the OGNL tool for ease of use and obtains the ExpressionEvaluator class, which is an expression evaluator. The ExpressionEvaluator class provides two methods, one is the evaluateBoolean method. This method can evaluate expressions whose results are in the form of true or false. For example, "<if test="name! =null">"The true and false judgments in the node can be completed by directly calling this method.

  /**
   * 对结果为true/false形式的表达式进行求值
   * @param expression 表达式
   * @param parameterObject 参数对象
   * @return 求值结果
   */
  public boolean evaluateBoolean(String expression, Object parameterObject) {
    
    
    // 获取表达式的值
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value instanceof Boolean) {
    
     // 如果确实是Boolean形式的结果
      return (Boolean) value;
    }
    if (value instanceof Number) {
    
     // 如果是数值形式的结果
      return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
    }
    return value != null;
  }

The other is the evaluateIterable method. This method evaluates an expression whose result is in iterative form. In this way, the iteration judgment in the "<foreach item="id" collection="array" open="("separator=", "close=")">#{id} </foreach>" node can be directly called. The method is complete.

  /**
   * 对结果为迭代形式的表达式进行求值
   * @param expression 表达式
   * @param parameterObject 参数对象
   * @return 求值结果
   */
  public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
    
    
    // 获取表达式的结果
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value == null) {
    
    
      throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
    }
    if (value instanceof Iterable) {
    
     // 如果结果是Iterable
      return (Iterable<?>) value;
    }
    if (value.getClass().isArray()) {
    
     // 结果是Array
      // 原注释:得到的Array可能是原始的,因此调用Arrays.asList()可能会抛出ClassCastException。因此要手工转为ArrayList
      int size = Array.getLength(value);
      List<Object> answer = new ArrayList<>();
      for (int i = 0; i < size; i++) {
    
    
        Object o = Array.get(value, i);
        answer.add(o);
      }
      return answer;
    }
    if (value instanceof Map) {
    
     // 结果是Map
      return ((Map) value).entrySet();
    }
    throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
  }

The expression evaluator based on OGNL encapsulation is a powerful tool for SQL node tree parsing. It can make correct judgments on the value of expressions based on the context. This is very important for parsing complex database operation statements into pure SQL statements. step.

4.2 Dynamic context

On the one hand, when parsing the SQL node tree, it is necessary to continuously save the parsed SQL fragments; on the other hand, when parsing the SQL node tree, some parameters and environmental information are also needed as the basis for parsing. The above two functions are provided by the dynamic context DynamicContext.

// 上下文环境
  private final ContextMap bindings;
  // 用于拼装SQL语句片段
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  // 解析时的唯一编号,防止解析混乱
  private int uniqueNumber = 0;

  /**
   * DynamicContext的构造方法
   * @param configuration 配置信息
   * @param parameterObject 用户传入的查询参数对象
   */
  public DynamicContext(Configuration configuration, Object parameterObject) {
    
    
    if (parameterObject != null && !(parameterObject instanceof Map)) {
    
    
      // 获得参数对象的元对象
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      // 判断参数对象本身是否有对应的类型处理器
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      // 放入上下文信息
      bindings = new ContextMap(metaObject, existsTypeHandler);
    } else {
    
    
      // 上下文信息为空
      bindings = new ContextMap(null, false);
    }
    // 把参数对象放入上下文信息
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    // 把数据库id放入上下文信息
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

The following information is stored in the context bindings attribute.

  • database id. Therefore, when writing SQL statements, we can directly use the DATABASE_ID_KEY variable to reference the value of the database id.
  • Parameter object. When writing SQL statements, we can directly use the PARAMETER_OBJECT_KEY variable to reference the entire parameter object.
  • Metadata for the parameter object. Metadata based on parameter objects can conveniently reference the attribute values ​​of the parameter object, so you can directly reference the attributes of the parameter object when writing SQL statements.

There is also a ContextMap in DynamicContext, which is a subclass of HashMap. When performing data query, DynamicContext will first query from HashMap. If the query fails, it will query from the properties of the parameter object. It is based on this that we can directly reference the properties of the parameter object when writing SQL statements. Source code of data query operation of DynamicContext class:

    /**
     * 根据键索引值。会尝试从HashMap中寻找,失败后会再尝试从parameterMetaObject中寻找
     * @param key 键
     * @return 值
     */
    @Override
    public Object get(Object key) {
    
    
      String strKey = (String) key;
      // 如果HashMap中包含对应的键,直接返回
      if (super.containsKey(strKey)) {
    
    
        return super.get(strKey);
      }

      // 如果Map中不含有对应的键,尝试从参数对象的原对象中获取
      if (parameterMetaObject == null) {
    
    
        return null;
      }

      if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
    
    
        return parameterMetaObject.getOriginalObject();
      } else {
    
    
        return parameterMetaObject.getValue(strKey);
      }

    }

After reading the source code of the dynamic context environment, we know why we can directly reference both the actual parameters and the properties of the actual parameters when writing the mapping file.

4.3 SQL nodes and their analysis

An important advantage of MyBatis is that it supports dynamic nodes. But the database itself does not know these nodes, so MyBatis will process these nodes first and then hand them over to the database for execution. These nodes are defined as SqlNode in MyBatis.
SqlNode is an interface, and only one apply method is defined in the interface. This method is responsible for completing the parsing of the node itself and merging the parsing results into the context provided by the input parameters.
MyBatis supports many types of nodes in SQL statements, such as if, where, foreach, etc., which are all subclasses of SqlNode.
Insert image description here
Next, take the common and typical IfSqlNode, ForEachSqlNode, and TextSqlNode as examples to introduce the implementation class of the SqlNode interface.

4.3.1 IfSqlNode

IfSqlNode corresponds to the if node in the database operation node. Through the if node, MyBatis can decide whether to write a SQL fragment based on parameters and other information.

<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
    SELECT * FROM `user`
    <where>
        <if test="name != null">
            `name` = #{name}
        </if>
        <if test="schoolName != null">
            AND `schoolName` = #{schoolName}
        </if>
    </where>
</select>
public class IfSqlNode implements SqlNode {
    
    
  // 表达式评估器
  private final ExpressionEvaluator evaluator;
  // if判断时的测试条件
  private final String test;
  // if成立时,要被拼接的SQL片段信息
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    
    
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  /**
   * 完成该节点自身的解析
   * @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
   * @return 解析是否成功
   */
  @Override
  public boolean apply(DynamicContext context) {
    
    
    // 判断if条件是否成立
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
    
    
      // 将contents拼接到context
      contents.apply(context);
      return true;
    }
    return false;
  }
}

The apply method of IfSqlNode is very simple: directly call the expression evaluator to calculate the value of the expression in the if node. If the value of the expression is true, the contents of the if node are added to the end of the environment context.

4.3.2 ForEachSqlNode

The ForEachSqlNode node corresponds to the foreach node in the database operation node. This node can traverse the individual elements in the collection and assemble each element into a new SQL fragment.

<select id="selectUsers" resultMap="userMapFull">
    SELECT *
    FROM `user`
    WHERE `id` IN
    <foreach item="id" collection="array" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

The apply method of the ForEachSqlNode class. The main process is to parse the iterated element to obtain the iteration object, then add the information of the iteration object to the context, and then splice the string according to the context information. Finally, after the string splicing is completed, the temporary variables generated by this operation will be cleaned up to avoid any impact on the context environment.

  public static final String ITEM_PREFIX = "__frch_";

  // 表达式求值器
  private final ExpressionEvaluator evaluator;
  // collection属性的值
  private final String collectionExpression;
  // 节点内的内容
  private final SqlNode contents;
  // open属性的值,即元素左侧插入的字符串
  private final String open;
  // close属性的值,即元素右侧插入的字符串
  private final String close;
  // separator属性的值,即元素分隔符
  private final String separator;
  // item属性的值,即元素
  private final String item;
  // index属性的值,即元素的编号
  private final String index;
  // 配置信息
  private final Configuration configuration;

  /**
   * 完成该节点自身的解析
   * @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
   * @return 解析是否成功
   */
  @Override
  public boolean apply(DynamicContext context) {
    
    
    // 获取环境上下文信息
    Map<String, Object> bindings = context.getBindings();
    // 交给表达式求值器解析表达式,从而获得迭代器
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
    
     // 没有可以迭代的元素
      // 不需要拼接信息,直接返回
      return true;
    }
    boolean first = true;
    // 添加open字符串
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
    
    
      DynamicContext oldContext = context;
      if (first || separator == null) {
    
     // 第一个元素
        // 添加元素
        context = new PrefixedContext(context, "");
      } else {
    
    
        // 添加间隔符
        context = new PrefixedContext(context, separator);
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
    
     // 被迭代对象是Map.Entry
        // 将被迭代对象放入上下文环境中
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
    
    
        // 将被迭代对象放入上下文环境中
        applyIndex(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
      // 根据上下文环境等信息构建内容
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
    
    
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    // 添加close字符串
    applyClose(context);
    // 清理此次操作对环境的影响
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

4.3.3 TextSqlNode

The TextSqlNode class corresponds to string nodes. String nodes are widely used. String nodes are also included in if nodes and foreach nodes.

<select id="selectUser_B" resultMap="userMap">
  select * FROM `user` WHERE `id` = #{id}
</select>

It seems that the TextSqlNode object itself is very pure and does not need to be parsed, but it is not. Parsing of the TextSqlNode object is necessary because it replaces the "${}" placeholders.

Before introducing the parsing of TextSqlNode objects, we first introduce its two internal classes: BindingTokenParser class and DynamicCheckerTokenParser class. The BindingTokenParser class and the DynamicCheckerTokenParser class are both subclasses of the TokenHandler interface. The
Insert image description here
TokenHandler interface of the class diagram of the TextSqlNode related class will be used in conjunction with the general placeholder parser GenericTokenParser. When GenericTokenParser parses a matching placeholder, it will The content is handed over to the handleToken method of the TokenHandler object for processing. In the TextSqlNode object, the placeholder is the " " symbol. Then the "{}" symbol is encountered. Then encounter "" symbol. So when encountering the " {}" symbol, what will happen to the BindingTokenParser object and the DynamicCheckerTokenParser object respectively?

  • BindingTokenParser: The handleToken method of this object will take out the variable in the placeholder, and then use the variable as a key to find the corresponding value in the context environment. Afterwards, the placeholder is replaced with the found value. Therefore, this object can complete the placeholder replacement work.
  • DynamicCheckerTokenParser: The handleToken method of this object assigns the member attribute isDynamic. Therefore the object can record whether it has encountered a placeholder.

The apply method of TextSqlNode class:

  /**
   * 完成该节点自身的解析
   * @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
   * @return 解析是否成功
   */
  @Override
  public boolean apply(DynamicContext context) {
    
    
    // 创建通用的占位符解析器
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 替换掉其中的${}占位符
    context.appendSql(parser.parse(text));
    return true;
  }
 /**
   * 创建一个通用的占位符解析器,用来解析${}占位符
   * @param handler 用来处理${}占位符的专用处理器
   * @return 占位符解析器
   */
  private GenericTokenParser createParser(TokenHandler handler) {
    
    
    return new GenericTokenParser("${", "}", handler);
  }

When replacing the " " placeholder, the Binding Token Parser internal class is used, which can take it out from the context. When replacing the " {}" placeholder, the BindingTokenParser internal class is used, which can take it out from the context. take out"" When replacing the placeholder, the B in d in g To k e n P a rser internal class is used, which can take out the variable value corresponding to the variable name in the " {}" placeholder from the context . And TextSqlNode There is also an isDynamic method in the class, which is used to determine whether the current TextSqlNode is dynamic. For the TextSqlNode object, if it contains the "${}" placeholder inside, then it is dynamic, otherwise it is not dynamic. .

  /**
   * 判断当前节点是不是动态的
   * @return 节点是否为动态
   */
  public boolean isDynamic() {
    
    
    // 占位符处理器,该处理器并不会处理占位符,而是判断是不是含有占位符
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    // 使用占位符处理器。如果节点内容中含有占位符,则DynamicCheckerTokenParser对象的isDynamic属性将会被置为true
    parser.parse(text);
    return checker.isDynamic();
  }

Therefore, the BindingTokenParser internal class has the ability to replace strings and will play a role in the parsing method apply of the TextSqlNode class; the DynamicCheckerTokenParser internal class has the recording capability and will play a role in the isDynamic method of the TextSqlNode class to determine whether it is a dynamic method.

5.SqlSource

The main job completed by the language driver class is to generate SqlSource. Among the three methods of the language driver interface LanguageDriver, two methods are used to generate SqlSource. The conversion work of SqlSource subclasses is mainly completed in the scripting package.

The four implementation classes of the SqlSource interface and their differences:

  • DynamicSqlSource: dynamic SQL statement. The so-called dynamic SQL statement refers to a statement that contains dynamic SQL nodes (such as if nodes) or contains "${}" placeholders.
  • RawSqlSource: native SQL statement. Refers to a non-dynamic statement, which may contain "#{}" placeholders, but does not contain dynamic SQL nodes or "${}" placeholders.
  • StaticSqlSource: static statement. The statement may contain "?" and can be submitted directly to the database for execution.
  • ProviderSqlSource: The above are all SQL statements obtained through XML files, while ProviderSqlSource is a SQL statement obtained through annotation mapping.

5.1 Generation of SqlSource

  1. Parse the mapping file to generate
    the interface createSqlSource method in LanguageDriver to parse the node information in the mapping file and obtain the SqlSource object.
  /**
   * 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
   * @param configuration 配置信息
   * @param script 映射文件中的数据库操作节点
   * @param parameterType 参数类型
   * @return SqlSource对象
   */
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

As can be seen in the XMLLanguageDriver class, the SqlSource object is mainly generated by the parseScriptNode method of XMLScriptBuilder, and the SqlSource object generated by this method is a DynamicSqlSource object or RawSqlSource object.

  /**
   * 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
   * @param configuration 配置信息
   * @param script 映射文件中的数据库操作节点
   * @param parameterType 参数类型
   * @return SqlSource对象
   */
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    
    
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }
  /**
   * 解析节点生成SqlSource对象
   * @return SqlSource对象
   */
  public SqlSource parseScriptNode() {
    
    
    // 解析XML节点节点,得到节点树MixedSqlNode
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 根据节点树是否为动态,创建对应的SqlSource对象
    if (isDynamic) {
    
    
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
    
    
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

Therefore, the SqlSource object generated by parsing the mapping file is one of the DynamicSqlSource object and the RawSqlSource object.

  1. Parse annotation information to generate SqlSource.
    The ProviderSqlSource class is a subclass of the SqlSource interface. Moreover, the ProviderSqlSource class gives another SqlSource subclass by calling the createSqlSource(Configuration, String, Class<?>) method in the LanguageDriver interface.
    createSqlSource (Configuration, String, Class<?>) in the LanguageDriver interface can generate SqlSource based on the information in the annotation.
/**
   * 创建SqlSource对象(基于注解的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
   * @param configuration 配置信息
   * @param script 注解中的SQL字符串
   * @param parameterType 参数类型
   * @return SqlSource对象,具体来说是DynamicSqlSource和RawSqlSource中的一种
   */
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

Implemented in the XMLLanguageDriver class:

  // 创建SQL源码(注解方式)
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    
    
    if (script.startsWith("<script>")) {
    
    
      // 如果注解中的内容以<script>开头
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
    
    
      // 如果注解中的内容不以<script>开头
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
    
    
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
    
    
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

The SQL information in the annotation is divided into two categories according to whether the string in the annotation starts with "<script>", so as to handle the two situations in the following code respectively.

@Select("SELECT * FROM `user` WHERE `id` = #{id}")
User queryUserById(Integer id);
@Select("<script>" +
        "        SELECT *\n" +
        "        FROM `user`\n" +
        "        WHERE id IN\n" +
        "        <foreach item=\"id\" collection=\"array\" open=\"(\" separator=\",\" close=\")\">\n" +
        "            #{id}\n" +
        "        </foreach>\n" +
        "    </script>")
List<User> queryUsersByIds(int[] ids);
  • For SQL statements starting with "<script>", the same parsing method as the mapping file will be used to generate a DynamicSqlSource object or RawSqlSource object;
  • For SQL statements that do not start with "<script>", DynamicSqlSource objects or RawSqlSource objects are directly generated.

First, the SqlSource object generated by parsing the annotation information is a ProviderSqlSource object; then, the ProviderSqlSource object is converted into a DynamicSqlSource object or RawSqlSource object through the createSqlSource (Configuration, String, Class<?>) method in the LanguageDriver interface.

Insert image description here

5.2 Conversion of DynamicSqlSource

The DynamicSqlSource class is in the xmltags sub-package of the scripting package. It represents statements containing dynamic SQL nodes (such as if nodes) or "${}" placeholders, that is, dynamic SQL statements. Both DynamicSqlSource and RawSqlSource will be converted to StaticSqlSource before a BoundSql object can be given.

/**
 * SqlSource的重要实现,用以解析动态SQL语句。
 */
public class DynamicSqlSource implements SqlSource {
    
    
  /**
   * 获取一个BoundSql对象
   * @param parameterObject 参数对象
   * @return BoundSql对象
   */
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
    // 创建DynamicSqlSource的辅助类,用来记录DynamicSqlSource解析出来的
    // * SQL片段信息
    // * 参数信息
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 这里会逐层(对于mix的node而言)调用apply。最终不同的节点会调用到不同的apply,完成各自的解析
    // 解析完成的东西拼接到DynamicContext中,里面含有#{}
    // 在这里,动态节点和${}都被替换掉了。
    rootSqlNode.apply(context);
    // 处理占位符、汇总参数信息
    // RawSqlSource也会焦勇这一步
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 使用SqlSourceBuilder处理#{},将其转化为?
    // 相关参数放进了context.bindings
    // *** 最终生成了StaticSqlSource对象,然后由它生成BoundSql
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 把context.getBindings()的参数放到boundSql的metaParameters中进行保存
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }
}

The getBoundSql method of the DynamicSqlSource class has two very critical operations:

  1. Here the apply method will be called on each node layer by layer starting from the root node. After this step, all dynamic nodes and "${}" will be replaced. In this way, DynamicSqlSource is no longer dynamic, but static.
rootSqlNode.apply(context);
  1. This will complete the replacement of the "#{}" symbol and return a StaticSqlSource object
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  /**
   * 将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource
   * @param originalSql sqlNode.apply()拼接之后的sql语句。已经不包含<if> <where>等节点,也不含有${}符号
   * @param parameterType 实参类型
   * @param additionalParameters 附加参数
   * @return 解析结束的StaticSqlSource
   */
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    
    
    // 用来完成#{}处理的处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 通用的占位符解析器,用来进行占位符替换
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 将#{}替换为?的SQL语句
    String sql = parser.parse(originalSql);
    // 生成新的StaticSqlSource对象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

5.3 Conversion of RawSqlSource

Compared with the DynamicSqlSource class, the RawSqlSource class is simpler because it does not contain dynamic nodes and "${}" placeholders, only "#{}" placeholders. The RawSqlSource class completes the conversion to StaticSqlSource in the construction method.

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    
    
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 处理RawSqlSource中的“#{}”占位符,得到StaticSqlSource
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

It can be seen that RawSqlSource has completed the processing of the "#{}" placeholder in the construction method, obtained the StaticSqlSource object and put it into its own sqlSource property. In the subsequent getBoundSql operation, the BoundSql object is returned directly from the StaticSqlSource object held in the sqlSource property.

5.4 Summary of SqlSource interface implementation classes

The SqlSource interface has four implementation classes, of which the objects of three implementation classes are transformed into StaticSqlSource objects through layers of transformation. Then, the getBoundSql abstract method defined in the SqlSource interface is actually implemented by the StaticSqlSource object.
Insert image description here
The conversion process between implementation classes of the SqlSource interface is actually the parsing process of database operation statements. During this conversion process, the SQL statements in the annotations are classified and processed, the dynamic statements are expanded, the "${}" placeholder is assigned a value, and the "#{}" placeholder is replaced, and finally the result can be handed over to the database driver. The executed SQL statement contains only the parameter placeholder "?"

By sorting out the transformation process of classes and summarizing it into a picture, we can have a clear and intuitive understanding of the transformation process of the entire database operation statement. In the process of reading source code, summarizing the process of class transformation, state transformation, information transfer and other processes into a picture is a good way to avoid getting lost in messy logic.

Guess you like

Origin blog.csdn.net/d495435207/article/details/130900892