In order to clearly explain the problem, I try to add detailed comments to the source code. Some large sections of the source code, I just intercepted a part, can explain the problem.
xml file parsing
We know that SqlSessionFactory is a very important class of mybatis. It is a compiled memory image of a single database mapping relationship. The instance of SqlSessionFactory object can be created through the build method of SqlSessionFactoryBuilder object class, and the parsing of xml file is called in this method.
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse());//Here is the entrance of parsing ...
Then look at the parse method,
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration"));//Start parsing here return configuration; }
Next, look at the parseConfiguration method,
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { //package label String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { //mapper tag 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); try(InputStream inputStream = Resources.getResourceAsStream(resource)) { // Here, the resource will be loaded, the mapper file will be parsed, and the maperstatement object will be built, XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } ...
Note that the configuration file of mybatis is still parsed here, and our xml sql file has not been found yet. Some people may have questions. What are the package s and resource s here? I don't seem to see them in the configuration file of mybatis? In fact, the configuration file of mybatis can be written as follows:
<mappers> <mapper resource="Mapper xml Path to (relative to) classes (path to)"/> </mappers>
Or,
<mappers> <mapper class="The full class name of the interface" /> </mappers>
However, most of us use spring+mybatis. This configuration is rare. It is more likely to be like this:
mybatis: # Configure type alias type-aliases-package: com.xxx.xxx.system.model # Configure mapper scanning and find all mapper.xml mapping files mapper-locations: 'classpath*:/mybatis/*/**Mapper.xml' # Load global configuration file config-location: 'classpath:/mybatis/mybatis-config.xml'
The effect is actually the same.
Moving on, the XMLMapperBuilder#parse method,
public void parse() { if (!configuration.isResourceLoaded(resource)) { //XML and SQL files start with mapper configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace();//Try loading the configuration file through nameSpace. } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
configurationElement method,
private void configurationElement(XNode context) { try { ////Parse namespace String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); //Parses the parameterMap and finally adds it to the parameterMaps attribute of the Configuration object, which is universal globally parameterMapElement(context.evalNodes("/mapper/parameterMap")); //Resolve the resultMap and put it in the Configuration for global use resultMapElements(context.evalNodes("/mapper/resultMap")); ////Parsing sql sqlElement(context.evalNodes("/mapper/sql")); //Really start parsing the select, insert, update and delete tags 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); } }
Configuration class is a very core class of mybatis. Many global configurations will be parsed and placed here, such as parameterMaps, resultMap, etc. Continue to look at the buildStatementFromContext method, which finally calls org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode, as follows:
public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); .... // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } //Mybatis encapsulates each SQL tag into a SqlSource object 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"); //Through the builder assistant, add the assembled MappedStatement to the configuration and maintain the map of the statement. The key is the id of namespace+sql builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
As you can see, the MappedStatement object will eventually be generated and added to a map maintained in the configuration. The key of this map is the id of namespace+sql, and the value is the corresponding MappedStatement object. This MappedStatement object is very important. It is the key to connecting our two parts. Remember this class.
To sum up:
Each SQL tag in the XML file corresponds to a MappedStatement object, which has two important attributes.
- ID: the ID composed of fully qualified class name + method name.
- Sqlsource: the sqlsource object corresponding to the current SQL tag.
MappedStatement objects are cached in Configuration#mappedStatements and are globally valid. The Configuration object is the core class in Mybatis. Basically, all Configuration information is maintained here. After parsing all the XML, Configuration contains all the SQL information.
Dynamic agent
After understanding the parsing process, let's look at another question:
The Dao interface we defined does not implement classes, so how does it finally execute our SQL statements when calling it? Let me give the answer first, dynamic agent. Here's a specific analysis.
Here we first introduce a very important class MapperProxy to see its definition:
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -4724728412955527868L; private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC; private static final Constructor<Lookup> lookupConstructor; private static final Method privateLookupInMethod; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache; ...
This class inherits InvocationHandler and must be a dynamic proxy. If some partners are not familiar with dynamic agents, you can supplement this knowledge first, and the following contents will be better understood.
Consider a question: when was MapperProxy created? It is called in the implementation of the abstract method of SqlSession getMapper, and finally calls org.apache.ibatis.binding.MapperRegistry#getMapper. The code is as follows:
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 { //Create a Mapper instance through a dynamic proxy return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
When we declare a dao interface, we usually do this:
@MapperScan("com.fcbox.uniorder.system.dao")
This is the usage in spring boot, or you can use xml configuration. The function of this annotation is to register all classes under the path into the Spring Bean and set their Bean class to MapperFactoryBean. MapperFactoryBean implements the FactoryBean interface, commonly known as factory Bean. Then, when we inject the Dao interface, the returned object is the getObject() method object in the MapperFactoryBean factory Bean. (the getObject method returns the Bean instance created by FactoryBean)
org.mybatis.spring.mapper.MapperFactoryBean#getObject code is as follows:
public T getObject() throws Exception { return this.getSqlSession().getMapper(this.mapperInterface); }
This getMapper method will call all the way to the org.apache.ibatis.binding.MapperRegistry#getMapper method we mentioned above.
To sum up, that is, when we inject the Dao interface, we inject the proxy object MapperProxy. Naturally, according to the principle of dynamic proxy, when When we call the method of Dao interface, we will call the invoke method of MapperProxy object. Let's take a look at the invoke method:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //Judge whether the interface has an implementation class (generally, our dao interface does not have an implementation class) if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { //So I usually take this branch return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
Then, we will continue the invoke method of cachedInvoker to see what cachedInvoker is,
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { return MapUtil.computeIfAbsent(methodCache, method, m -> { if (m.isDefault()) { try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } else { /** * Will come here * PlainMethodInvoker Is a tool class that encapsulates a mapper call * MapperMethod Object contains references to two objects: * SqlCommand Contains the method name (fully qualified name) and the command type (insert, delete, and so on) */ return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); ... }
So the org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke method is called,
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { return mapperMethod.execute(sqlSession, args); }
The execute method of MapperMethod is called here. Continue to look,
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; } ...
MapperMethod source code finds that the related methods in sqlSession are finally called, and the sqlSession is delegated to Excutor for execution. For example, we take update as an example, as follows:
@Override public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
Eh? Why does this MappedStatement look so familiar? Isn't that what we talked about in the first part? Is there a feeling of another village.
To sum up, when we call the method of Dao interface, we will call the invoke method of MapperProxy object. Finally, we will find the MappedStatement object from a map of the housekeeper Configuration through the full pathname of the interface, and then execute the specific SQL through the Executor and return.

reference resources:
- https://juejin.cn/post/7004047712664420382