How does Mybatis work

Objectives:

  1. Clarify the process of mybatis loading and parsing mapper file;
  2. Clarify the process of executing SQL by mybatis.

When analyzing the source code of mybatis loading configuration, the previous article mentioned org.apache.ibatis.builder.xml.xmlconfigbuilder ා parseconfiguration method, and now continues to analyze mapperElement method. First look at the source code:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          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);
            // Build XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // Resolve profile
            mapperParser.parse();
          } 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();
          } else if (resource == null && url == null && mapperClass != null) {
            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.");
          }
        }
      }
    }
  }

Considering the configuration of the project, look at the code that generates XMLMapperBuilder and mapperParser.parse().
In the process of building XMLMapperBuilder, mapperbuilder assistant is used. This class inherits BaseBuilder. TypeAliasRegistry and TypeHandlerRegistry are loaded in the constructor of this class.
Let's focus on mapperParserparse()

  public void parse() {
    // Determine whether the resource has been loaded
    if (!configuration.isResourceLoaded(resource)) {
      // Configure child nodes under / mapper node
      configurationElement(parser.evalNode("/mapper"));
      // Load resource resources
      configuration.addLoadedResource(resource);
      // Binding namespace
      bindMapperForNamespace();
    }
    // Load incomplete resources
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

Here is the code of configurationElement:

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

You can see that it is mainly to parse different nodes and put them into builder assistant.
Let's look at the process of executing SQL.

            ClipsDAO clipsDAO = session.getMapper(ClipsDAO.class);
            ClipsEntity clipsEntity = clipsDAO.selectById(1);

Check the implementation of session.getMapper():

  // org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }
  
  // org.apache.ibatis.session.Configuration#getMapper
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
  
  // org.apache.ibatis.binding.MapperRegistry#getMapper
    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 {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  

As you can see, mybatis generates a proxy Class for the interface through dynamic proxy. We know that when loading the configuration, the bindMapperForNamespace method calls the configuration.addMapper() method to map the Class to org. Apache. Ibatis. Binding. Mapperregistry ා knownmappers.
Take a look at MapperProxy Code:

  @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);
    }
    // Get MapperMethod object from cache
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // Implementation of SQL
    return mapperMethod.execute(sqlSession, args);
  }

Here is MapperMethod.execute() method:

  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:
        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 if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        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()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

At this point, the SQL execution is complete.

Tags: Java Apache SQL Mybatis Session

Posted on Sun, 01 Dec 2019 23:30:46 -0500 by eon201