MyBatis source code analysis

Keywords: Programming Session Java Mybatis xml

Phase source code details:

Through a simple demo for single step debugging, it is convenient to have a deep understanding of each step of Mybatis. The example is as follows:

public class MybatisLearn {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);// Load profile
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// Get the session factory according to the configuration file
        SqlSession session = sqlSessionFactory.openSession();// Open a session
        try {
            UserMapper mapper = session.getMapper(UserMapper.class);// Get a mapper by class
            System.out.println(mapper.selectById("sunwukong"));// Call methods in mapper to query data
        } finally {
            session.close();// Closing session
        }
    }
}

Analyze the logic behind each step in the demo in detail. First, get the session factory according to the configuration file, which loads all the configuration information. There are many steps. See the source code below.

(1).SqlSessionFactoryBuilder.java
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);// Load all the specified configuration files into memory
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
(2).XMLConfigBuilder.java
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration")); // Get all the configurations under this node and analyze them
  return configuration;
}
private void parseConfiguration(XNode root) {
  try {
    propertiesElement(root.evalNode("properties")); //First read the properties information
    typeAliasesElement(root.evalNode("typeAliases")); // Read class rename information
    pluginElement(root.evalNode("plugins"));// Read plug-in information
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    settingsElement(root.evalNode("settings"));
    environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
    databaseIdProviderElement(root.evalNode("databaseIdProvider")); // Read database vendor ID
    typeHandlerElement(root.evalNode("typeHandlers")); // Read type converter
    mapperElement(root.evalNode("mappers")); // Read mappers configuration
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
private void propertiesElement(XNode context) throws Exception {//First read the properties information
  if (context != null) {
    Properties defaults = context.getChildrenAsProperties();
    String resource = context.getStringAttribute("resource");
    String url = context.getStringAttribute("url");
    if (resource != null && url != null) {
      throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
    }
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    configuration.setVariables(defaults); // set the read information to Mybatis Configuration information
  }
}
private void mapperElement(XNode parent) throws Exception {// Read mappers configuration
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {// Get package information
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");//Get more information about mapper configuration
        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 mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();// Further analysis of the configured mapper.xml
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();// Further analysis of the configured mapper.xml
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);// If the mapper interface is specified, it will be added to the configuration information.
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}
(3).XMLMapperBuilder.java
public void parse() { // Further analysis of the configured mapper.xml
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));// Read mapper label
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");// Get namespace information
    if (namespace.equals("")) {
  	  throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));// Get cache related configuration
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));// Get information about parameters
    resultMapElements(context.evalNodes("/mapper/resultMap"));// Get information about return value encapsulation
    sqlElement(context.evalNodes("/mapper/sql"));// Get information about encapsulated sql segments
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));// Building sql in mapper
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}
(4).Configuration.java  
public <T> void addMapper(Class<T> type) {// Mapper interface added to mapper registration information
  mapperRegistry.addMapper(type);
}

The next step is to open a session. The source code is as follows:

DefaultSqlSessionFactory.java
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);// Default execution type, non auto commit transaction
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType); // Create an actuator from the configuration information
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);// Batch actuator
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);// Reusable actuator
  } else {
    executor = new SimpleExecutor(this, transaction);// Simple actuator
  }
  if (cacheEnabled) { // Open cache or not
    executor = new CachingExecutor(executor);// With cache actuator
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

The Executor has a variety of executors. The diagram is as follows:

Get the specified mapper by class:

(1).DefaultSqlSession.java
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);// Get the corresponding mapper from the configuration information according to the type
}
(2).Configuration.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
(3).MapperRegistry.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);// According to the class of mapper, get the corresponding mapper agent
    if (mapperProxyFactory == null)
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    try {
        return mapperProxyFactory.newInstance(sqlSession);// Bind this session to mapper and return a mapper
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}
(4).MapperProxyFactory.java
public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();// The details of each method in the mapper are saved.

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

}
(5).MapperMethod.java
public class MapperMethod {// Corresponding method details

  private final SqlCommand command;
  private final MethodSignature method;
  
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {// Practical implementation method
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
  ......
}

The order of key nodes of MyBatis is as follows:

About caching:

Mybatis cache is divided into level 1 (SqlSession level) and level 2 (namespace level). By default, only level 1 cache is enabled; Level 2 cache needs to be manually enabled and configured.
1. For a session, query a piece of data, which will be placed in the first level cache of the current session;
2. If the session is closed, the data in the first level cache will be saved in the second level cache. The new session query information can refer to the content in the second level cache.
3. The data detected by different namespace s will be placed in their own corresponding cache (map), and the data detected will be placed in the first level cache by default. Only after the session is committed or closed, the data in the first level cache will be transferred to the second level cache.

public class CachingExecutor implements Executor {// Cache related execution classes

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
  ......
  
  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);// Refresh cache when updating data
    return delegate.update(ms, parameterObject);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// Generate cached key according to series of rules
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) { // Using caching
      flushCacheIfRequired(ms);// Refresh cache
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql); // Data meets cache requirements
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key); // Get data from cache first
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // Data not available in cache, query database
          tcm.putObject(cache, key, list); // Put data into cache
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  
  private void flushCacheIfRequired(MappedStatement ms) { // Refresh cache
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);// Clear cache
    }
  }
  ......
}

Posted by buck2bcr on Wed, 30 Oct 2019 08:51:49 -0700