Mybatis source code analysis Mapper file registration and binding

Mybatis is a "sql oriented" persistence layer framework. It can dynamically assemble sql, which is extremely flexible. At the same time, it avoids almost all JDBC code, manually setting parameters and obtaining result sets. Its plug-in mechanism allows intercepting calls at a certain point during the execution of mapped statements, and so on.
We all know that Mapper is an interface, and each way of Mapper is the entrance for us to interact with the database. Each Mapper has an XML file corresponding to it. We can write sql freely and happily in XML. Of course, we can also write on the interface method in the form of annotation, but it is still not as flexible as XML after all. Then the problem comes, How does Mybatis register and bind Mapper? Now I'll take you to uncover this mystery.
First, let's look at two methods of executing sql with Mybatis

Direct operation SqlSession method

public User findUserById(Integer userId) {
  SqlSession sqlSession = MyBatisSqlSessionFactory.getSqlSession();
  try {
    // namespace + statementId
    return sqlSession.selectOne("com.objcoding.mybatis.UserMapper.findUserById", userId);
  } finally {
    sqlSession.close();
  }
}

Through Mapper interface

public User findUserById(Integer userId) {
  SqlSession sqlSession = MyBatisSqlSessionFactory.getSqlSession();
  try {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    return userMapper.findUserById(userId);
  } finally {
    sqlSession.close();
  }
}
public class UserMapper {
  User findUserById(@Param("userId") String userId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.objcoding.mybatis.UserMapper">
  <select id="findUserById" resultType="com.objcoding.mybatis.User">
    SELECT * FROM user WHERE user_id=#{userId}
  </select>
</mapper>

Obviously, the second method can greatly reduce the probability of errors in manually writing the namespace, and Mapper can directly operate the method to realize data links, which looks much more elegant.

How Mapper is exemplified? It is a proxy class generated by Java Dynamic Proxy and associated with sqlSession, as shown in the following figure:

It can be seen that Mapper at this time is a Bean in the Spring Bean container. It is a proxy class. What is the generation process of this proxy class? Let's take you to see the source code of mybatis.

Create a SqlSessionFactory instance and inject it into the Bean container:

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
  PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
  SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

  // Some codes are omitted here

  bean.setMapperLocations(resolver.getResources("classpath*:com/**/*Mapper.xml"));//
  return bean.getObject();
}

bean.getObject():

@Override
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
  // Some codes are omitted here
  this.sqlSessionFactory = buildSqlSessionFactory();
}

sqlSessionFactory.buildSqlSessionFactory()

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
  Configuration configuration;
  // Some codes are omitted here
  SqlSessionFactory sqlSessionFactory =this.sqlSessionFactoryBuilder.build(configuration);
  // Some codes are omitted here
  if (!isEmpty(this.mapperLocations)) {
    // Some codes are omitted here
    try {
      /**
       * Parse the xml file in mapperLocation and generate an xml mapperbuilder
       */
      XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments());
      // Perform parsing
      xmlMapperBuilder.parse();
    } catch (Exception e) {
      throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
    } finally {
      ErrorContext.instance().reset(); 
    }
    // Some codes are omitted here
return sqlSessionFactory;
}

XMLMapperBuilder is mainly used to parse the contents in the < mapper > tag in mybatis. Its function is similar to that of XMLConfigBuilder. It parses xml contents. From the source code, get the input stream and configuration of mapperLocation to initialize itself. mapperLocation is the encapsulated class of mapper XML address from the configuration file

XMLMapperBuilder.parse()

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {

    /**
     * 1.Parse the node information in xml and generate MappedStatement
     */
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);

    /**
     * 2.Binding Mapper according to Namespace will also parse the information in Mapper annotation and generate MappedStatement
     */
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

This method is Mapper xml node parsing, Mapper annotation parsing and the entry registered in the binding.

XMLMapperBuilder.configurationElement(XNode context)

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // Parsing sql fragments in xml 
    sqlElement(context.evalNodes("/mapper/sql"));
    // Parsing sql corresponding to Mapper method
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

This method reads each node of Mapper xml, generates MapperStatement, adds it to Configuration, and registers and binds Mapper according to Namespace.

XMLMapperBuilder.bindMapperForNamespace()

private void bindMapperForNamespace() {
  // Get mapper Mapper class name of namespace in XML
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      // Load class objects based on class names
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {
        configuration.addLoadedResource("namespace:" + namespace);
        // Binding operation
        configuration.addMapper(boundType);
      }
    }
  }
}

This method finds mapper XML mapper class name, find the loaded class object according to the class name, and finally bind:

MapperRegistry.addMapper()

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // mapper maps with MapperProxyFactory
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      // mapper annotation builder
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      // analysis
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

MapperRegistry class is a Mapper class registration factory, which adds the Mapper class mapped with MapperProxyFactory to its property knownMappers;

MapperProxyFactory class is the factory that produces Mapper proxy class, which is implemented by Java Dynamic Proxy:

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

Finally, it can be seen from the newInstance method that the Mapper proxy class produced here is associated with SqlSession. Let's continue:

MapperProxy.invoke()

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (isDefaultMethod(method)) {
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

mapperMethod.execute(sqlSession, args)

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    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: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      // Some codes are omitted here
  }
  return result;
}

The answer is revealed. Every time we call Mapper's method, we actually call the execute method, and this method actually calls the SqlSession method to interact with the database through cachedMapperMethod(method); This method obtains the information related to the execution of sql. In fact, it obtains it from the attribute MappedStatement of congconfiguration class:

MapperMethod.resolveMappedStatement()

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) {
  String statementId = mapperInterface.getName() + "." + methodName;
  if (configuration.hasStatement(statementId)) {
    // Get MappedStatement 
    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;
}

MappedStatement class is a node (select/insert/delete/update) that stores Mapper's execution method mapping, including the configured sql, sql id, cache information, resultMap, parameterType, resultType and other important configuration contents.

How does Mybatis add the method node information in Mapper to the MappedStatement attribute of configuration? Let's go back to mapperregistry Using the addmapper () method, take a look at the final analysis of MapperAnnotationBuilder:

MapperAnnotationBuilder.parse()

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    // Give priority to parsing xml statements,
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // issue #237
        if (!method.isBridge()) {
          // Parse a method to generate the corresponding MapperedStatement object
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

MapperAnnotationBuilder.parse() the ultimate purpose of this method is to encapsulate the sql, mapper method and other related information into a MapperStatement and add it to the Configuration, so that the mapper proxy class can find the corresponding MapperStatement, take out the corresponding information, and then call SqlSession according to these information.

MapperAnnotationBuilder.parseStatement(Method method)

void parseStatement(Method method) {
  Class<?> parameterTypeClass = getParameterType(method);
  // Loading LanguageDriver for annotation @ Lang
  LanguageDriver languageDriver = getLanguageDriver(method);

  /**
   * Get sql resource class from method 
   */
  SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
  if (sqlSource != null) {

    // Some codes are omitted here

    /**
     * Add MappedStatement to Configuration
     */
    assistant.addMappedStatement(mappedStatementId,sqlSource,statementType,sqlCommandType, fetchSize,timeout,null,parameterTypeClass,resultMapId,getReturnType(method),resultSetType,flushCache,useCache,false,keyGenerator,keyProperty,keyColumn,null,languageDriver,options != null ? nullOrEmpty(options.resultSets()) : null);
  }
}

The purpose of this method is to generate a MapperStatement from the annotation information in Mapper and add the MapperStatement to the Configuration.

From the above source code analysis process, it can be concluded that Mybatis mainly does two things in the process of generating a SqlSessionFactory:

Registration: the node information in Mapper xml and the annotation information in Mapper class are corresponding to the methods of Mapper class one by one. Each method generates a MapperStatement and adds it to Configuration;

Binding: generate a Mapper class object according to the namespace in Mapper xml, which corresponds to a MapperProxyFactory proxy factory for the generation of Mapper proxy objects.

Finally, a simple brain map is attached:

Keywords: Database Mybatis SQL

Added by TimC on Sun, 23 Jan 2022 10:49:22 +0200