MyBatis04-"General Source Code Guide: Detailed Explanation of MyBatis Source Code" Notes-Configuration Analysis

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

1 Overview

Configuration files in MyBatis are divided into two categories:

  1. Core configuration file, which contains the basic configuration information of MyBatis. There is only one file.
  2. Mapping file, which sets the mapping relationship between Java objects and database properties, database operation statements, etc. There can be multiple files.

Before performing database operations, complete the parsing, conversion and storage of the above two types of files. From a class perspective, classes related to configuration parsing can be divided into:

  • Parser class: Provides configuration parsing function and completes the extraction and transformation of configuration information by copying. There are: XMLConfigBuilder, XMLMapperBuilder.
  • Parsing entity classes: Provides configuration saving function. The structure of the class corresponds to the configuration information, and the configuration information will eventually report an error to the attributes of the parsed entity class. Configuration class, Environment class, DataSource class, etc.

2.binding package

The binding package is a package mainly used to handle the binding relationship between Java methods and SQL statements.
The binding package has two functions:

  1. Maintain the association between abstract methods in the mapping interface and database operation nodes.
  2. Access the corresponding database operations for the abstract methods in the mapping interface

2.1 Access to database operations

Accessing the corresponding database operations for the abstract methods in the mapping interface is implemented by a dynamic proxy based on reflection.
Insert image description here

2.1.1 Methodization of database operations

To connect a database operation to an abstract method, the first thing to do is to convert the database operation node into a method. The MapperMethod object represents the converted method of the database operation. Each MapperMethod object corresponds to a database operation node, and the SQL statement in the node can be started by calling the execute method in the MapperMethod instance.

The MapperMethod class has two attributes, which correspond to its two important internal classes:

  • MethodSignature class: the signature of a specific method
  public static class MethodSignature {
    
    

    // 返回类型是否为集合类型
    private final boolean returnsMany;
    // 返回类型是否是map
    private final boolean returnsMap;
    // 返回类型是否是空
    private final boolean returnsVoid;
    // 返回类型是否是cursor类型
    private final boolean returnsCursor;
    // 返回类型是否是optional类型
    private final boolean returnsOptional;
    // 返回类型
    private final Class<?> returnType;
    // 如果返回为map,这里记录所有的map的key
    private final String mapKey;
    // resultHandler参数的位置
    private final Integer resultHandlerIndex;
    // rowBounds参数的位置
    private final Integer rowBoundsIndex;
    // 引用参数名称解析器
    private final ParamNameResolver paramNameResolver;
}

The properties of MethodSignature describe the details of a method in detail

  • SqlCommand class: a SQL statement
public static class SqlCommand {
    
    
  // SQL语句的名称
  private final String name;
  // SQL语句的种类,一共分为以下六种:增、删、改、查、清缓存、未知
  private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    
    
      // 方法名称
      final String methodName = method.getName();
      // 方法所在的类。可能是mapperInterface,也可能是mapperInterface的子类
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
    
    
        if (method.getAnnotation(Flush.class) != null) {
    
    
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
    
    
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
    
    
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
    
    
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

    public String getName() {
    
    
      return name;
    }

    public SqlCommandType getType() {
    
    
      return type;
    }

    /**
     * 找出指定接口指定方法对应的MappedStatement对象
     * @param mapperInterface 映射接口
     * @param methodName 映射接口中具体操作方法名
     * @param declaringClass 操作方法所在的类。一般是映射接口本身,也可能是映射接口的子类
     * @param configuration 配置信息
     * @return MappedStatement对象
     */
    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
    
    
      // 数据库操作语句的编号是:接口名.方法名
      String statementId = mapperInterface.getName() + "." + methodName;
      // configuration保存了解析后的所有操作语句,去查找该语句
      if (configuration.hasStatement(statementId)) {
    
    
        // 从configuration中找到了对应的语句,返回
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
    
    
        // 说明递归调用已经到终点,但是仍然没有找到匹配的结果
        return null;
      }
      // 从方法的定义类开始,沿着父类向上寻找。找到接口类时停止
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
    
    
        if (declaringClass.isAssignableFrom(superInterface)) {
    
    
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
    
    
            return ms;
          }
        }
      }
      return null;
    }
  }

}

The construction method of SqlCommand is mainly to complete the assignment of name and type fields according to the incoming parameters, and the resolveMappedStatement sub-method is the key to everything. Because the resolveMappedStatement sub-method queries out a MappedStatement object, and MappedStatement completely corresponds to a database operation statement.

Insert image description here
Therefore, as long as the execute method of the MapperMethod object is called, a specific database operation can be triggered, and the database operation is converted into a method.

/**
   * 执行映射接口中的方法
   * @param sqlSession sqlSession接口的实例,通过它可以进行数据库的操作
   * @param args 执行接口方法时传入的参数
   * @return 数据库操作结果
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    
    
    Object result;
    switch (command.getType()) {
    
     // 根据SQL语句类型,执行不同操作
      case INSERT: {
    
     // 如果是插入语句
        // 将参数顺序与实参对应好
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行操作并返回结果
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
    
     // 如果是更新语句
        // 将参数顺序与实参对应好
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行操作并返回结果
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
    
     // 如果是删除语句MappedStatement
        // 将参数顺序与实参对应好
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行操作并返回结果
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT: // 如果是查询语句
        if (method.returnsVoid() && method.hasResultHandler()) {
    
     // 方法返回值为void,且有结果处理器
          // 使用结果处理器执行查询
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
    
     // 多条结果查询
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
    
     // Map结果查询
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
    
     // 游标类型结果查询
          result = executeForCursor(sqlSession, args);
        } else {
    
     // 单条结果查询
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
    
    
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH: // 清空缓存语句
        result = sqlSession.flushStatements();
        break;
      default: // 未知语句类型,抛出异常
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    
    
      // 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

The source code of the execute method of the MapperMethod class. It can be seen that the execute method triggers different database operations based on the type of its own SQL statement. With the help of the MapperMethod class, as long as we can convert the call to the Java mapping interface into a call to the execute method of the MapperMethod object, we can complete the specified database operation when calling a Java mapping interface.

There is also an inner class ParamMap in the MapperMethod class. The ParamMap internal class is used to store parameters. It is a subclass of HashMap, but is more strict than HashMap: if you try to obtain a key value that does not exist, it will directly throw an exception. This is because when we reference a non-existent input parameter in a database operation, such an error cannot be resolved.

  public static class ParamMap<V> extends HashMap<String, V> {
    
    

    private static final long serialVersionUID = -2212268410512043556L;

    @Override
    public V get(Object key) {
    
    
      if (!super.containsKey(key)) {
    
    
        throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
      }
      return super.get(key);
    }
  }

2.1.2 Access to database operation methods

In the previous section, we have converted a database operation into a method (here refers to the execute method of the MapperMethod object), but how can this method be called?
When calling a method in the mapping interface, such as "List<User>queryUserBySchoolName (User user)", Java will find and execute the method in the implementation class of the interface. Our mapping interface does not have an implementation class, so calling the method in the mapping interface should result in an error. How can we instead call the execute method in the MapperMethod class?

The above work requires the help of the MapperProxy class, which transfers the method call for the mapping interface into a call to the execute method of the MapperMethod object based on the dynamic proxy, thereby realizing the database operation. MapperProxy inherits the InvocationHandler interface and is a dynamic proxy class. This means that when an instance of it is used instead of the proxied object, the method call on the proxied object will be transferred to the invoke method in MapperProxy.

public class MapperProxy<T> implements InvocationHandler, Serializable {
    
    
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
    try {
    
    
      if (Object.class.equals(method.getDeclaringClass())) {
    
     // 继承自Object的方法
        // 直接执行原有方法
        return method.invoke(this, args);
      } else if (method.isDefault()) {
    
     // 默认方法
        // 执行默认方法
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
    
    
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 找对对应的MapperMethod对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用MapperMethod中的execute方法
    return mapperMethod.execute(sqlSession, args);
  }
}

The MapperProxyFactory is the production factory of MapperProxy, and the newInstance core method generates a MapperProxy object. At this point, we know that as long as the corresponding MapperProxy object is used as the implementation of the mapping interface, the function of accessing the database operation for the mapping interface can be completely realized.

2.1.3 Abstract methods are associated with database operation nodes

How does an abstract method in a mapping interface determine which MapperMethod object it wants to access?
MyBatis solves this problem in two steps.

  • In the first step, MyBatis associates the mapping interface with MapperProxyFactory. This association is maintained in the knownMappers attribute of the MapperRegistry class. knownMappers is a HashMap whose key is the mapping interface and the value is the corresponding MapperProxyFactory object.
public class MapperRegistry {
    
    

  private final Configuration config;
  // 已知的所有映射
  // key:mapperInterface,即dao的数据库接口,不是方法
  // value:MapperProxyFactory,即映射器代理工厂
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  /**
   * 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口生成一个代理实现
   * @param type 映射接口
   * @param sqlSession sqlSession
   * @param <T> 映射接口类型
   * @return 代理实现对象
   */
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    
    
    // 找出指定映射接口的代理工厂
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
    
    
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
    
    
      // 通过mapperProxyFactory给出对应代理器的实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
    
    
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

The constructor of MapperProxyFactory has only one parameter, which is the mapping interface. The other attributes of MapperProxyFactory are not allowed to be modified, so the MapperProxy object it produces is unique. Therefore, as long as the MapperProxyFactory object is determined, the MapperProxy object is also determined. Therefore, the knownMappers attribute in MapperRegistry indirectly associates the mapping interface with the MapperProxy object.

public class MapperProxyFactory<T> {
    
    
 /**
   * MapperProxyFactory构造方法
   * @param mapperInterface 映射接口
   */
  public MapperProxyFactory(Class<T> mapperInterface) {
    
    
    this.mapperInterface = mapperInterface;
  }

Because MapperRegistry stores the corresponding relationship between the mapping interface and MapperProxy, its getMapper method can directly find the corresponding proxy object for the mapping interface. Through MapperRegistry, the corresponding relationship between the mapping interface and the mapping file is established.

  • In the second step, the scope at this time has been reduced to a mapping interface or MapperProxy object. The corresponding relationship between interface methods and MapperMethod objects is maintained by the methodCache attribute in MapperProxy.
    In this way, any abstract method in the mapping interface corresponds to the MapperMethod object associated with a MapperProxy object.
  // 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
  private final Map<Method, MapperMethod> methodCache;

Insert image description here
The MapperProxy class is a proxy class for the mapping interface. After the proxy relationship is established, as long as the method in the mapping interface is called, it will be intercepted by the corresponding MapperProxy, and the MapperProxy will create or select the appropriate MapperMethod object and trigger its execute method. As a result, calls to abstract methods in the mapping interface are transformed into specific database operations.

2.1.4 Summary of database operation access

  1. initialization phase

MyBatis will parse each mapping file during the initialization phase, and then record the information of each database operation node into the mappedStatements attribute of the Configuration object. Its structure is a StrictMap (a HashMap that does not allow overwriting of key values). The key of this StrictMap is the "namespace value. Statement id value" of the SQL statement (if the statement id value is not ambiguous, the statement id value will be used separately. key into a copy of data), and the value is the detailed information of the database operation node.

MyBatis will also scan all mapping interfaces during the initialization phase and create a MapperProxyFactory associated with it based on the mapping interface. The relationship between the two is maintained by MapperRegistry. When the getMapper method of MapperRegistry is called (the getMapper method of SqlSession will eventually be called here), MapperProxyFactory will produce a MapperProxy object as a proxy for the mapping interface.

  1. Data reading and writing phase
    When a method in the mapping interface is called, it will be hijacked by the proxy object MapperProxy, which in turn triggers the invoke method in the MapperProxy object. The invoke method in the MapperProxy object will create or retrieve the MapperMethod object corresponding to the mapping interface method. During the process of creating the MapperMethod object, the constructor of the SqlCommand subclass in the MapperMethod will go to the mappedStatements attribute of the Configuration object according to the current mapping interface name and method. The SQL statement information that has been stored in the name index in the early stage is stored. Then, the execute method of the MapperMethod object is triggered, and different database operations are performed according to different SQL statement types in the execute method. In this way, a method call in the mapping interface is finally converted into the corresponding database operation.

3.builder package

The builder package is a package divided by type. There are many builder classes in the package.
Although the builder package is a package divided by type, the following two relatively complete functions are also completed in this package.

  1. Parse XML configuration files and mapping files, this part of the function is in the xml sub-package;
  2. Parse Mapper declarations in the form of annotations. This part of the function is in the annotation sub-package.

Advantages of builder pattern:

  • It is very flexible when using the builder, and you can set the properties of the built object once or multiple times;
  • The caller only needs to call the main process of the builder and does not need to worry about the details of the built object;
  • The builder's behavior can be easily modified to create different objects.

Builder classes generally contain two types of methods:

  • One type is the attribute setting method. There are usually multiple methods of this type that can accept different types of parameters to set the builder's properties.
  • One type is the target object generation method. This type of method generally has only one method, which is to create a target object based on the properties in the current builder.
    The advantages of the builder pattern will be more obvious when creating complex objects. Therefore, the builder pattern is very common in some large systems.

3.1 Builder base class and tool class

BaseBuilder is the base class of all builder classes.
Insert image description here
Although the BaseBuilder class is declared as an abstract class, it does not contain any abstract methods, so its subclasses do not need to implement any of its methods. The BaseBuilder class is more like a utility class, providing many practical utility methods for builder classes that inherit it. Of course, there are indeed many builder classes that do not need the tool methods provided by BaseBuilder, and therefore do not inherit BaseBuilder. These classes include MapperAnnotationBuilder, SelectBuilder, etc.

The tool methods provided by the BaseBuilder class are roughly divided into the following categories.

  • *ValueOf: Type conversion function, responsible for converting input parameters into specified types and supporting default value settings;
  • resolve*: String to enumeration type function, find the specified enumeration type based on the string and return it;
  • createInstance: Create a type instance based on the type alias;
  • resolveTypeHandler: Returns a type handler instance based on the type handler alias.

Among the subclasses of the BaseBuilder class, the MapperBuilderAssistant class is the most special because it is not a builder class itself but a builder auxiliary class. The reason it inherits from the BaseBuilder class is simply to use the methods in the BaseBuilder class.

There are many settings in the MyBatis mapping file, including namespace, cache sharing, result mapping, etc. Eventually these settings will be parsed to generate different classes, and the MapperBuilderAssistant class is an auxiliary class for these parsed classes. The MapperBuilderAssistant class provides many auxiliary methods, such as Mapper namespace settings, cache creation, discriminator creation, etc.

3.2 SqlSourceBuilder class and StaticSqlSource class

SqlSourceBuilder is a builder class, but its name is somewhat ambiguous. It cannot be used to create all SqlSource objects (SqlSource is an interface with four implementations), but can only produce StaticSqlSource objects through the parse method.

To be precise, the SqlSourceBuilder class can replace the "#{}" symbols in DynamicSqlSource and RawSqlSource to convert them into StaticSqlSource. This conversion process occurs in the parse method. Therefore, it is more appropriate to call the SqlSourceBuilder class a parser or converter. In fact, many places that reference the SqlSourceBuilder object name the object's variable as "sqlSourceParser" (this variable can be found in the DynamicSqlSource and RawSqlSource classes).

 /**
   * 将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());
  }

StaticSqlSource is one of the four subclasses of SqlSource. The two symbols "${}" and "#{}" no longer exist in the SQL statements it contains, but only "?"

public class StaticSqlSource implements SqlSource {
    
    

  // 经过解析后,不存在${}和#{}这两种符号,只剩下?符号的SQL语句
  private final String sql;
  // SQL语句对应的参数列表
  private final List<ParameterMapping> parameterMappings;
  // 配置信息
  private final Configuration configuration;
  /**
   * 组建一个BoundSql对象
   * @param parameterObject 参数对象
   * @return 组件的BoundSql对象
   */
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }
}

StaticSqlSource has a very important function, which is to give a BoundSql object. The getBoundSql method in StaticSqlSource is responsible for completing this function.

3.3 CacheRefResolver class and ResultMapResolver class

The CacheRefResolver class and the ResultMapResolver class are somewhat similar, not only in class names, but also in structure and function. They are all parser classes of certain classes. The attributes contain relevant attributes of the parsed class and also contain a parser. The parser in the class can complete the parsing of the parsed class attributes. These integrated classes with parsing functions have standardized names in MyBatis: if the name of the parsed object is A, the integrated self-parsing class is called AResolver.

When you encounter a class named like this in subsequent analysis, you can directly analyze its composition and function. This naming method and function is relatively universal, but not absolute. For example, the MethodResolver in the annotation subpackage conforms to this pattern, including the properties and parser of the parsed object; while the ParamNameResolver does not conform to this pattern, because its parsing function is implemented through its own methods and does not need to rely on other parser.

<mapper namespace="com.github.yeecode.mybatisdemo.UserDao">
    <cache-ref namespace="com.github.yeecode.mybatisdemo"/>

MyBatis supports shared cache between multiple namespaces. In the namespace of "com.github.yeecode.mybatisdemo.dao.UserDao" we declare another namespace "com.github.yeecode.mybatisdemo.dao.TaskDao" through the <cache-ref> tag, then the former will use The latter cache.

3.3.1 CacheRefResolver class

CacheRefResolver is used to handle the problem of multiple namespaces sharing cache. It has two properties of its own. Among these two attributes, assistant is the parser and cacheRefNamespace is the parsed object.

/**
 * @author Clinton Begin
 *
 * 缓存引用解析器
 *
 * 包含了被解析的对象cacheRefNamespace 和对应的解析器MapperBuilderAssistant 因此具有自解析功能。
 */
public class CacheRefResolver {
    
    
  // Mapper建造者辅助类
  private final MapperBuilderAssistant assistant;
  // 被应用的namespace,即使用cacheRefNamespace的缓存空间
  private final String cacheRefNamespace;

  public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
    
    
    this.assistant = assistant;
    this.cacheRefNamespace = cacheRefNamespace;
  }

  public Cache resolveCacheRef() {
    
    
    return assistant.useCacheRef(cacheRefNamespace);
  }
}

With the useCacheRef method of MapperBuilderAssistant, the CacheRefResolver class can resolve cache sharing issues.

  /**
   * 使用其他namespace的缓存
   * @param namespace 其他的namespace
   * @return  其他namespace的缓存
   */
  public Cache useCacheRef(String namespace) {
    
    
    if (namespace == null) {
    
    
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
    
    
      unresolvedCacheRef = true;
      // 获取其他namespace的缓存
      Cache cache = configuration.getCache(namespace);
      if (cache == null) {
    
    
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      // 修改当前缓存为其他namespace的缓存,从而实现缓存共享
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
    
    
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

3.3.2 ResultMapResolver class

MyBatis's resultMap tag supports inheritance. As shown in Code 14-13, "girlUserMap" inherits the attribute mapping set in "userMap" by setting "extends="userMap"".

    <resultMap id="userMap" type="User" autoMapping="false">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <discriminator javaType="int" column="sex">
            <case value="0" resultMap="boyUserMap"/>
            <case value="1" resultMap="girlUserMap"/>
        </discriminator>
    </resultMap>

    <resultMap id="girlUserMap" type="Girl" extends="userMap">
        <result property="email" column="email"/>
    </resultMap>

The resolution of the resultMap inheritance relationship is completed by the ResultMapResolver class. The attribute assistant attribute of the ResultMapResolver class is the parser, and the other attributes are the parsed attributes.

public class ResultMapResolver {
    
    
  // Mapper建造者辅助类
  private final MapperBuilderAssistant assistant;
  // ResultMap的id
  private final String id;
  // ResultMap的type属性,即目标对象类型
  private final Class<?> type;
  // ResultMap的extends属性,即继承属性
  private final String extend;
  // ResultMap中的Discriminator节点,即鉴别器
  private final Discriminator discriminator;
  // ResultMap中的属性映射列表
  private final List<ResultMapping> resultMappings;
  // ResultMap的autoMapping属性,即是否开启自动映射
  private final Boolean autoMapping;

With the help of the addResultMap method of MapperBuilderAssistant, ResultMapResolver completes the inheritance relationship analysis of ResultMap, and finally gives a ResultMap object after the inheritance relationship is resolved.


  /**
   * 创建结果映射对象
   * 入参参照ResultMap属性
   * @return ResultMap对象
   */
  public ResultMap addResultMap(
      String id,
      Class<?> type,
      String extend,
      Discriminator discriminator,
      List<ResultMapping> resultMappings,
      Boolean autoMapping) {
    
    
    id = applyCurrentNamespace(id, false);
    extend = applyCurrentNamespace(extend, true);

    // 解析ResultMap的继承关系
    if (extend != null) {
    
     // 如果存在ResultMap的继承
      if (!configuration.hasResultMap(extend)) {
    
    
        throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
      }
      // 获取父级的ResultMap
      ResultMap resultMap = configuration.getResultMap(extend);
      // 获取父级的属性映射
      List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
      // 删除当前ResultMap中已有的父级属性映射,为当前属性映射覆盖父级属性属性创造条件
      extendedResultMappings.removeAll(resultMappings);
      // 如果当前ResultMap设置有构建器,则移除父级构建器
      boolean declaresConstructor = false;
      for (ResultMapping resultMapping : resultMappings) {
    
    
        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
    
    
          declaresConstructor = true;
          break;
        }
      }
      if (declaresConstructor) {
    
    
        extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
      }
      // 最终从父级继承而来的所有属性映射
      resultMappings.addAll(extendedResultMappings);
    }
    // 创建当前的ResultMap
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    // 将当期的ResultMap加入到Configuration
    configuration.addResultMap(resultMap);
    return resultMap;
  }

3.4 ParameterExpression class

ParameterExpression is an attribute parser that parses strings describing attributes into key-value pairs. The constructor of ParameterExpression is the general entry point for attribute parsing and is also the only public method in the entire class. The ParameterExpression class inherits HashMap and can internally save the final parsing result in the form of key-value pairs.

/**
 * 一个属性解析器
 * 能够将属性拆解开来
 */
public class ParameterExpression extends HashMap<String, String> {
    
    

  private static final long serialVersionUID = -2417552199605158680L;

  public ParameterExpression(String expression) {
    
    
    parse(expression);
  }

  // content = id, javaType= int, jdbcType=NUMERIC, typeHandler=DemoTypeHandler ;
  private void parse(String expression) {
    
    
    // 跳过空格
    int p = skipWS(expression, 0);
    // 跳过左括号
    if (expression.charAt(p) == '(') {
    
    
      expression(expression, p + 1);
    } else {
    
    
      // 处理参数
      property(expression, p);
    }
  }

3.5 XML file parsing

The configuration files and mapping files of MyBatis are XML files, and ultimately these XML files need to be parsed into corresponding classes. The xml sub-package of the builder package is used to complete the parsing of XML files. MyBatis's configuration files and mapping files contain many nodes. The parsing of these nodes is completed layer by layer by the five parser classes in the xml sub-package.
Insert image description here

3.5.1 Parsing of XML file declarations

XML files can reference external DTD files to verify XML files. In the DOCTYPE statement in the above figure, it indicates that the address of the DTD file referenced by the current XML file is "http://mybatis.org/dtd/mybatis-3-config.dtd". MyBatis may run in a non-network environment and cannot download DTD files through the Internet. XMLMapperEntityResolver is used to solve this problem.
There is a resolveEntity method in the "org.xml.sax.EntityResolver" interface. You can implement this method to customize the way the DTD document stream is given, instead of only downloading the DTD document from the Internet.

  /**
   * 在一个XML文件的头部是这样的:
   * <!DOCTYPE configuration
   *         PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
   *         "http://mybatis.org/dtd/mybatis-3-config.dtd">
   *  那么上述例子中,
   * @param publicId 为-//mybatis.org//DTD Config 3.0//EN
   * @param systemId 为http://mybatis.org/dtd/mybatis-3-config.dtd
   * @return 对应DTD文档的输入流
   * @throws SAXException
   */
  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
    
    
    try {
    
    
      if (systemId != null) {
    
    
        // 将systemId转为全小写
        String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
    
    
          // 说明这个是配置文档
          // 直接把本地配置文档的dtd文件返回
          return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
    
    
          // 说明这个是映射文档
          // 直接把本地映射文档的dtd文件返回
          return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
        }
      }
      return null;
    } catch (Exception e) {
    
    
      throw new SAXException(e.toString());
    }
  }

3.5.2 Analysis of configuration files

The XMLConfigBuilder class is responsible for parsing the configuration file, and this class will use the parsing results to build a Configuration object. The entry method of the XMLConfigBuilder class is the parse method, which calls the parseConfiguration method to formally start the layer-by-layer parsing of the configuration file.

  /**
   * 从根节点configuration开始解析下层节点
   * @param root 根节点configuration节点
   */
  private void parseConfiguration(XNode root) {
    
    
    try {
    
    
      // 解析信息放入Configuration
      // 首先解析properties,以保证在解析其他节点时便可以生效
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
    
    
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

The parseConfiguration method will call different sub-methods to parse subordinate nodes, and these methods are similar. Let's take the mapperElement method of parsing the "/configuration/mappers" node as an example to introduce it.

/**
   * 解析mappers节点,例如:
   * <mappers>
   *    <mapper resource="com/github/yeecode/mybatisDemo/UserDao.xml"/>
   *    <package name="com.github.yeecode.mybatisDemo" />
   * </mappers>
   * @param parent mappers节点
   * @throws Exception
   */
  private void mapperElement(XNode parent) throws Exception {
    
    

    if (parent != null) {
    
    
      for (XNode child : parent.getChildren()) {
    
    
        // 处理mappers的子节点,即mapper节点或者package节点
        if ("package".equals(child.getName())) {
    
     // package节点
          // 取出包的路径
          String mapperPackage = child.getStringAttribute("name");
          // 全部加入Mappers中
          configuration.addMappers(mapperPackage);
        } else {
    
    
          // resource、url、class这三个属性只有一个生效
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
    
    
            ErrorContext.instance().resource(resource);
            // 获取文件的输入流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 使用XMLMapperBuilder解析Mapper文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
    
    
            ErrorContext.instance().resource(url);
            // 从网络获得输入流
            InputStream inputStream = Resources.getUrlAsStream(url);
            // 使用XMLMapperBuilder解析Mapper文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
    
    
            // 配置的不是Mapper文件,而是Mapper接口
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
    
    
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

3.5.3 Database operation statement analysis

The XMLMapperBuilder class is responsible for parsing the mapping file, and the structure of this class is very similar to the XMLConfigBuilder class. The parse method is the entry method for parsing, and then the configurationElement method is called to complete the parsing layer by layer.

  /**
   * 解析Mapper文件
   */
  public void parse() {
    
    
    // 该节点是否被解析过
    if (!configuration.isResourceLoaded(resource)) {
    
    
      // 处理mapper节点
      configurationElement(parser.evalNode("/mapper"));
      // 加入到已经解析的列表,防止重复解析
      configuration.addLoadedResource(resource);
      // 将mapper注册给Configuration
      bindMapperForNamespace();
    }

    // 下面分别用来处理失败的<resultMap>、<cache-ref>、SQL语句
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

  /**
   * 解析Mapper文件的下层节点
   * @param context Mapper文件的根节点
   */
  private void configurationElement(XNode context) {
    
    
    try {
    
    
      // 读取当前Mapper文件的命名空间
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
    
    
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // mapper文件中其他配置节点的解析
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      // 处理各个数据库操作语句
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
    
    
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

Unlike the parse method in the XMLConfigBuilder class, there are three parsePending* methods at the end of the parse method of XMLMapperBuilder. They are used to handle temporary errors during parsing. After being triggered by the configurationElement(parser.evalNode("/mapper")) statement, the system will parse each node of the mapping file in sequence. When parsing, the file is read and parsed from top to bottom. A node may be parsed, but the node it refers to has not been defined yet.

    <resultMap id="userMap" type="User" autoMapping="false">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <discriminator javaType="int" column="sex">
            <case value="0" resultMap="boyUserMap"/>
            <case value="1" resultMap="girlUserMap"/>
        </discriminator>
    </resultMap>

	<resultMap id="girlUserMap" type="Girl" extends="userMap">
        <result property="email" column="email"/>
    </resultMap>

When parsing the resultMap of "id="girlUserMap"", the resultMap of "id="userMap"" that it refers to through "extends="userMap"" has not yet been read. A temporary error will occur at this time.
There are several attributes of the code in Configuration, which are used to store nodes with temporary errors.

  // 暂存未处理完成的一些节点
  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

The above-mentioned situation where dependencies cannot be confirmed is temporary. You only need to process these error nodes again after the first parsing is completed. A common way to resolve out-of-order dependencies is to try the first round of parsing and read in all nodes while parsing. Then a second round of parsing is performed, and the nodes that failed to find the first round of parsing are relied upon. Since all nodes have been read in the first pass, the dependencies of the second pass can always be found.

There is another method, which is more direct and simple, that is, only read in all nodes in the first round of parsing, but do not process dependencies, and then only process dependencies in the second round of parsing. This is how Spring handles dependencies between beans during initialization.

3.5.4 Statement analysis

In the parsing of mapping files, an important task is to parse the database operation nodes, that is, the four types of nodes: select, insert, update, and delete. The parsing of database operation nodes is completed by XMLStatementBuilder. The parseStatementNode method in the XMLStatementBuilder class completes the main parsing process.

  /**
   * 解析select、insert、update、delete这四类节点
   */
  public void parseStatementNode() {
    
    
    // 读取当前节点的id与databaseId
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    // 验证id与databaseId是否匹配。MyBatis允许多数据库配置,因此有些语句只对特定数据库生效
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    
    
      return;
    }

    // 读取节点名
    String nodeName = context.getNode().getNodeName();
    // 读取和判断语句类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 处理语句中的Include节点
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    // 参数类型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 语句类型
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除,开始进行SQL解析
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // 判断是否已经有解析好的KeyGenerator
    if (configuration.hasKeyGenerator(keyStatementId)) {
    
    
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
    
    
      // 全局或者本语句只要启用自动key生成,则使用key生成
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 读取各个配置属性
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
    
    
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    // 在MapperBuilderAssistant的帮助下创建MappedStatement对象,并写入到Configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

3.5.5 Application parsing include

MyBatis supports citing statement fragments in writing database operation statements. Improve the efficiency of writing database operation statements in MyBatis.

    <sql id="bySchool">
        AND `schoolName` = #{
    
    schoolName}
    </sql>

    <select id="selectUserByNameAndSchoolName" parameterMap="userParam01" resultType="User">
        SELECT * FROM `user` WHERE `name` = #{
    
    name}
        <include refid="bySchool"/>
    </select>

    <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>

The two methods selectUserByNameAndSchoolName in the code are equivalent. The parsing of the include node is handled by XMLIncludeTransformer, which replaces the include node in the SQL statement with the quoted SQL fragment. The applyIncludes(Node) method in the XMLIncludeTransformer class is the entry method for parsing include nodes, and the applyIncludes(Node, Properties, boolean) method is the core method.

/**
   * 解析数据库操作节点中的include节点
   * @param source 数据库操作节点或其子节点
   * @param variablesContext 全局属性信息
   * @param included 是否嵌套
   */
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    
    
    if (source.getNodeName().equals("include")) {
    
     // 当前节点是include节点
      // 找出被应用的节点
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      // 递归处理被引用节点中的include节点
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
    
    
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 完成include节点的替换
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
    
    
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
    
     // 元素节点
      if (included && !variablesContext.isEmpty()) {
    
    
        // 用属性值替代变量
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
    
    
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      // 循环到下层节点递归处理下层的include节点
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
    
    
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && source.getNodeType() == Node.TEXT_NODE
        && !variablesContext.isEmpty()) {
    
     // 文本节点
      // 用属性值替代变量
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

Insert image description here

3.6 Analysis of annotation mapping

Usually we use XML mapping files to complete MyBatis mapping configuration. At the same time, MyBatis also supports the use of annotations to configure mapping. The annotation sub-package in the builder package can be used to complete this form of mapping parsing work. The method of using annotations to configure mapping may be less used. In this section, we will first introduce this configuration method, and then read the source code of the annotation sub-package to understand how MyBatis parses annotation mapping.

3.6.1 Use of annotations

You can declare the database operation statements associated with the abstract method by adding annotations to the abstract method in the mapping interface.
In addition to the @Select annotation, the @Insert, @Update, and @Delete annotations can also achieve similar functions.

    @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);

MyBatis also supports a more flexible annotation method

@SelectProvider(type = UserProvider.class, method = "queryUsersBySchoolName")
List<User> queryUsersBySchoolName(String schoolName);

In this way, you can add the @SelectProvider annotation to the abstract method. The type field in the annotation points to a class, and the method points to a method in the class. Finally, the string returned by the method method in the type class will be used as the SQL statement bound by the queryUserBySchoolName method.

Similarly, in addition to the @SelectProvider annotation, there are three annotations: @InsertProvider, @UpdateProvider, and @DeleteProvider.
The four annotation methods @Select, @Insert, @Update, and @Delete are called direct annotation mapping, and the
four annotation methods @SelectProvider, @InsertProvider, @UpdateProvider, and @DeleteProvider are called indirect annotation mapping.

3.6.2 Analysis of annotation mapping

Annotation mapping parsing starts from the parse method in the MapperAnnotationBuilder class. Before this method is triggered, the MapperAnnotationBuilder class has completed some initialization work in the static code block: the four annotations of the direct annotation mapping are put into the SQL_ANNOTATION_TYPES constant; the four annotations of the indirect annotation mapping are put into the SQL_PROVIDER_ANNOTATION_TYPES constant. .

  static {
    
    
    SQL_ANNOTATION_TYPES.add(Select.class);
    SQL_ANNOTATION_TYPES.add(Insert.class);
    SQL_ANNOTATION_TYPES.add(Update.class);
    SQL_ANNOTATION_TYPES.add(Delete.class);

    SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
    SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
    SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
    SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
  }

When the configuration of the mappers tag exists in the configuration file, the parse method in the MapperAnnotationBuilder class will be triggered to start parsing the mapping interface file.

<mappers>
    <mapper resource="com/github/yeecode/mybatisdemo/UserDao.xml"/>
</mappers>
 /**
   * 解析包含注解的接口文档
   */
  public void parse() {
    
    
    String resource = type.toString();
    // 防止重复分析
    if (!configuration.isResourceLoaded(resource)) {
    
    
      // 寻找类名对应的resource路径下是否有xml配置,如果有则解析掉。这样就支持注解和xml混合使用
      loadXmlResource();
      // 记录资源路径
      configuration.addLoadedResource(resource);
      // 设置命名空间
      assistant.setCurrentNamespace(type.getName());
      // 处理缓存
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
    
    
        try {
    
    
          // 排除桥接方法
          // JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法,这个就是桥接方法。
          // 就是说一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法
          if (!method.isBridge()) {
    
    
            // 解析该方法
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
    
    
          // 解析异常的方法暂存起来
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 处理解析异常的方法
    parsePendingMethods();
  }

The "!method.isBridge()" statement in the code is to exclude the bridge method. The bridge method is automatically introduced by the compiler to match the type erasure of generics. It is not a user-written method, so it should be excluded.
parsePendingMethods method, when parsing the interface method, you may encounter some other information that has not been read, such as ResultMap information that has not been parsed, namespace that has not been parsed, etc., then the method will be put into the incompleteMethods attribute in the Configuration class , processed again at the end. When processing again, the MethodResolver.parseStatement method is used to parse the interface method that failed to parse again.

 /**
   * 解析该方法。主要是解析该方法上的注解信息
   * @param method 要解析的方法
   */
  void parseStatement(Method method) {
    
    
    // 通过子方法获取参数类型
    Class<?> parameterTypeClass = getParameterType(method);
    // 获取方法的脚本语言驱动
    LanguageDriver languageDriver = getLanguageDriver(method);
    // 通过注解获取SqlSource
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
    
    
      // 获取方法上可能存在的配置信息,配置信息由@Options注解指定
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      // 用默认值初始化各项设置
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = configuration.getDefaultResultSetType();
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      // 主键自动生成的处理
      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
    
    
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
    
    
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
    
    
          // 这里不能单独配置,因此查看全局配置
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
    
    
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
    
    
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
    
    
        // 根据@Options中的配置信息重新设置配置
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
    
    
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
    
    
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        if (options.resultSetType() != ResultSetType.DEFAULT) {
    
    
          resultSetType = options.resultSetType();
        }
      }

      // 返回结果ResultMap处理
      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
    
    
        resultMapId = String.join(",", resultMapAnnotation.value());
      } else if (isSelect) {
    
    
        resultMapId = parseResultMap(method);
      }

      // 将获取的映射信息存入Configuration
      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

The parseStatement method handles additional information such as parameters and configuration information. The most critical one is to call the getSqlSourceFromAnnotations method to obtain the SqlSource object.

 /**
   * 通过注解获取SqlSource对象
   * @param method 含有注解的方法
   * @param parameterType 参数类型
   * @param languageDriver 语言驱动
   * @return SqlSource对象
   */
  private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    
    
    try {
    
    
      // 遍历寻找是否有Select、Insert、Update、Delete 四个注解之一
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      // 遍历寻找是否有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
    
    
        if (sqlProviderAnnotationType != null) {
    
    
          // 两类注解不可同时使用
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        // 含有Select、Insert、Update、Delete 四个注解之一
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        // 取出value值
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        // 基于字符串构建SqlSource
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
    
    
        // 含有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        // 根据对应的方法获取SqlSource
        return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
      }
      return null;
    } catch (Exception e) {
    
    
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
  }

The SqlSource object of direct annotation mapping is generated by the buildSqlSourceFromStrings method; the SqlSource object of indirect annotation mapping is generated by the ProviderSqlSource class.

3.6.3 Analysis of direct annotation mapping

Direct annotation mapping is completed by the buildSqlSourceFromStrings method of the MapperAnnotationBuilder object.

  /**
   * 基于字符串创建SqlSource对象
   * @param strings 字符串,即直接映射注解中的字符串
   * @param parameterTypeClass 参数类型
   * @param languageDriver 语言驱动
   * @return 创建出来的SqlSource对象
   */
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    
    
    final StringBuilder sql = new StringBuilder();
    for (String fragment : strings) {
    
    
      sql.append(fragment);
      sql.append(" ");
    }
    return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
  }

The processing of the buildSqlSourceFromStrings method is very simple. It directly concatenates the strings describing the SQL statements and hands them to LanguageDriver for processing.

3.6.4 Analysis of indirect annotation mapping

The parsing of indirect annotation mapping is completed by ProviderSqlSource. Before introducing it, two auxiliary classes are introduced: ProviderContext class and ProviderMethodResolver class.

  1. ProviderContext The
    ProviderContext class is very simple. It integrates three properties internally. The function of this class is to integrate the three internal attributes into a whole for easy transfer and use.
  // 提供映射信息的类
  private final Class<?> mapperType;
  // 提供映射信息的方法,该方法属于mapperType类
  private final Method mapperMethod;
  // 数据库编号
  private final String databaseId;
  1. ProviderMethodResolver
    ProviderMethodResolver is an interface with a default method resolveMethod. The function of this method is to find the method specified in the method attribute from the class pointed to by the type attribute of the @*Provider annotation.
  /**
   * 从@*Provider注解的type属性所指向的类中找出method属性中所指的方法
   * @param context 包含@*Provider注解中的type值和method值
   * @return 找出的指定方法
   */
  default Method resolveMethod(ProviderContext context) {
    
    
    // 找出同名方法
    List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
        .filter(m -> m.getName().equals(context.getMapperMethod().getName()))
        .collect(Collectors.toList());

    // 如果没有找到指定的方法,则@*Provider注解中的type属性所指向的类中不含有method属性中所指的方法。
    if (sameNameMethods.isEmpty()) {
    
    
      throw new BuilderException("Cannot resolve the provider method because '"
          + context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'.");
    }
    // 根据返回类型再次判断,返回类型必须是CharSequence类或其子类
    List<Method> targetMethods = sameNameMethods.stream()
        .filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType()))
        .collect(Collectors.toList());
    if (targetMethods.size() == 1) {
    
    
      // 方法唯一,返回该方法
      return targetMethods.get(0);
    }

    if (targetMethods.isEmpty()) {
    
    
      throw new BuilderException("Cannot resolve the provider method because '"
          + context.getMapperMethod().getName() + "' does not return the CharSequence or its subclass in SqlProvider '"
          + getClass().getName() + "'.");
    } else {
    
    
      throw new BuilderException("Cannot resolve the provider method because '"
          + context.getMapperMethod().getName() + "' is found multiple in SqlProvider '" + getClass().getName() + "'.");
    }
  }

The process of resolveMethod finding the specified method is mainly divided into two steps:

  • The first step is to find all methods that match the method name;
  • The second step is to perform further verification based on the return value of the method.

When reading and analyzing the source code of an interface, be sure to pay attention to the reference of this in the default method of the interface. In the resolveMethod method, this refers to the entity object calling the method, not the ProviderMethodResolver interface.

// 找出同名方法
    List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
        .filter(m -> m.getName().equals(context.getMapperMethod().getName()))
        .collect(Collectors.toList());

The "getClass().getMethods()" statement involved in the above code can be written as "this.getClass().getMethods()". The statement that calls the resolveMethod method is the constructor of the ProviderSqlSource class.

  if (providerMethodName.length() == 0 && ProviderMethodResolver.class.isAssignableFrom(this.providerType)) {
    
    
        this.providerMethod = ((ProviderMethodResolver) this.providerType.getDeclaredConstructor().newInstance())
            .resolveMethod(new ProviderContext(mapperType, mapperMethod, configuration.getDatabaseId()));
      }

Therefore, this in the resolveMethod method refers to "this.providerType.getDeclaredConstructor().newInstance()", which refers to the providerType object. Further analysis of the assignment statement of providerType can lead to the conclusion that providerType refers to the instance of the class pointed to by the type attribute of the @*Provider annotation.

3.6.5 ProviderSqlSource class

// SqlSource的子类,能够根据*Provider的信息初始化得到
  // 调用入口唯一,在MapperAnnotationBuilder:getSqlSourceFromAnnotations中
public class ProviderSqlSource implements SqlSource {
    
    
  // Configuration对象
  private final Configuration configuration;
  // *Provider注解上type属性所指的类
  private final Class<?> providerType;
  // 语言驱动
  private final LanguageDriver languageDriver;
  // 含有注解的接口方法
  private final Method mapperMethod;
  // *Provider注解上method属性所指的方法
  private Method providerMethod;
  // 给定SQL语句的方法对应的参数
  private String[] providerMethodArgumentNames;
  // 给定SQL语句的方法对应的参数类型
  private Class<?>[] providerMethodParameterTypes;
  // ProviderContext对象
  private ProviderContext providerContext;
  // ProviderContext编号
  private Integer providerContextIndex;
}  

As a subclass of the SqlSource interface, the ProviderSqlSource class implements the getBoundSql method (the abstract method in the SqlSource interface). Its implementation process is included in the two methods getBoundSql and createSqlSource.

  /**
   * 获取一个BoundSql对象
   * @param parameterObject 参数对象
   * @return BoundSql对象
   */
  public BoundSql getBoundSql(Object parameterObject) {
    
    
    // 获取SqlSource对象
    SqlSource sqlSource = createSqlSource(parameterObject);
    // 从SqlSource中获取BoundSql对象
    return sqlSource.getBoundSql(parameterObject);
  }

  /**
   * 获取一个BoundSql对象
   * @param parameterObject 参数对象
   * @return SqlSource对象
   */
  private SqlSource createSqlSource(Object parameterObject) {
    
    
    try {
    
    
      // SQL字符串信息
      String sql;
      if (parameterObject instanceof Map) {
    
     // 参数是Map
        int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
        if (bindParameterCount == 1 &&
          (providerMethodParameterTypes[Integer.valueOf(0).equals(providerContextIndex) ? 1 : 0].isAssignableFrom(parameterObject.getClass()))) {
    
    
          // 调用*Provider注解的type类中的method方法,从而获得SQL字符串
          sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
        } else {
    
    
          @SuppressWarnings("unchecked")
          Map<String, Object> params = (Map<String, Object>) parameterObject;
          // 调用*Provider注解的type类中的method方法,从而获得SQL字符串
          sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
        }
      } else if (providerMethodParameterTypes.length == 0) {
    
    
        // *Provider注解的type类中的method方法无需入参
        sql = invokeProviderMethod();
      } else if (providerMethodParameterTypes.length == 1) {
    
    
        if (providerContext == null) {
    
    
          // *Provider注解的type类中的method方法有一个入参
          sql = invokeProviderMethod(parameterObject);
        } else {
    
    
          // *Provider注解的type类中的method方法入参为providerContext对象
          sql = invokeProviderMethod(providerContext);
        }
      } else if (providerMethodParameterTypes.length == 2) {
    
    
        sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
      } else {
    
    
        throw new BuilderException("Cannot invoke SqlProvider method '" + providerMethod
          + "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass())
          + "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination.");
      }
      Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
      // 调用languageDriver生成SqlSource对象
      return languageDriver.createSqlSource(configuration, sql, parameterType);
    } catch (BuilderException e) {
    
    
      throw e;
    } catch (Exception e) {
    
    
      throw new BuilderException("Error invoking SqlProvider method '" + providerMethod
          + "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass()) + "'.  Cause: " + extractRootCause(e), e);
    }
  }

The entire implementation process can be summarized into the following three steps.

  1. Call the method method in the type class annotated by *Provider to obtain the SQL string.
  2. Pass in the SQL string and other parameters to the createSqlSource method of languageDriver to generate a new SqlSource object.
  3. Call the getBoundSql method of the newly generated SqlSource object to obtain the BoundSql object.

Guess you like

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