preface
Mybatis has always been used as a persistence framework. We also know that when we define the xxxmapper interface class and use it to do CRUD operations, mybatis uses the dynamic proxy technology to help us generate proxy classes. So what are the implementation details inside the dynamic agent? XXXMapper.java class and xxxmapper How are XML related? This article will analyze the specific implementation mechanism of mybatis dynamic agent in detail.
Core components and applications of MyBatis
Before exploring the dynamic agent mechanism in MyBatis in detail, let's supplement the basic knowledge and understand the core components of MyBatis.
- SqlSessionFactory Builder (constructor): it can create SqlSessionFactory from XML, annotations, or manually configure Java code.
- SqlSessionFactory: the factory used to create SqlSession (session)
- SqlSession: SqlSession is the core class of Mybatis. It can be used to execute statements, commit or rollback transactions, and obtain the interface of Mapper
- SQL Mapper: it is composed of a Java interface and an XML file (or annotation). It needs to give the corresponding SQL and mapping rules. It is responsible for sending SQL to execute and returning results
"Note: now we use Mybatis, which is generally integrated with the Spring framework. In this case, SqlSession will be created by the Spring framework, so we often don't need to use SqlSessionFactoryBuilder or SqlSessionFactory to create SqlSession
Here's how to use these components of MyBatis, or how to quickly use MyBatis:
- Database table
CREATE TABLE user( id int, name VARCHAR(255) not NULL , age int , PRIMARY KEY (id) )ENGINE =INNODB DEFAULT CHARSET=utf8;
- Declare a User class
@Data public class User { private int id; private int age; private String name; @Override public String toString() { return "User{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + '}'; } }
- Define a global configuration file mybatis config XML (refer to the official document for the explanation of specific attribute tags in the configuration file)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--Root element of global profile--> <configuration> <!--enviroments Represents the environment configuration, which can be configured as a development environment(development),testing environment(test),production environment (production)etc.--> <environments default="development"> <environment id="development"> <!--transactionManager: Transaction manager, properties type There are only two values: JDBC and MANAGED--> <transactionManager type="MANAGED" /> <!--dataSource: Data source configuration--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments> <!--mappers File path configuration--> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
- UserMapper interface
public interface UserMapper { User selectById(int id); }
- UserMapper file
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--namespace Property represents the command space, which is different xml Mapping file namespace Must be different--> <mapper namespace="com.pjmike.mybatis.UserMapper"> <select id="selectById" parameterType="int" resultType="com.pjmike.mybatis.User"> SELECT id,name,age FROM user where id= #{id} </select> </mapper>
- Test class
public class MybatisTest { private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream("mybatis-config.xml")); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(1); System.out.println("User : " + user); } } } // result: User : User{id=1, age=21, name='pjmike'}
The above example simply shows how to use MyBatis. At the same time, I will use this example to further explore the implementation of MyBatis dynamic principle.
Implementation of MyBatis dynamic agent
public static void main(String[] args) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// <1> User user = userMapper.selectById(1); System.out.println("User : " + user); } }
In the previous example, we used sqlsession The getmapper () method obtains the UserMapper object. In fact, here we obtain the proxy class of the UserMapper interface, and then the proxy class executes the method. So how is this proxy class generated? Before exploring how to generate dynamic proxy classes, let's take a look at the preparations for the creation process of SqlSessionFactory factory, such as how to read the mybatis config configuration file and how to read the mapper file?
mybatis global configuration file parsing
private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream("mybatis-config.xml")); } catch (IOException e) { e.printStackTrace(); } }
We use new sqlsessionfactorybuilder() Create the SqlSessionFactory factory in the build () method and enter the build method
public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); 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. } } }
For the parsing of the global configuration file of mybatis, the relevant parsing code is located in the parse() method of XMLConfigBuilder:
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //Resolve global configuration file parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { //issue #117 read properties first 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")); //Parsing mapper mapper files mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
From the source code of parseConfiguration method, it is easy to see that it parses the attributes of each element in the mybatis global configuration file. Of course, a configuration object is returned after the final parsing. Configuration is a very important class, which contains all the configuration information of mybatis. It is built by taking money from XMLConfigBuilder, and mybatis reads mybatis config through XMLConfigBuilder XML, and then save the information to configuration
Mapper file parsing
//Parsing mapper mapper files mapperElement(root.evalNode("mappers"));
This method is to resolve the mappers attribute in the global configuration file. Go to:
[

]( https://pjmike-1253796536.cos.ap-beijing.myqcloud.com/mybatis/mapper xml parsing png)mapper xml
mapperParser. The parse () method is that XMLMapperBuilder parses Mapper mapper files, which can be compared with XMLConfigBuilder
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); //Resolve the mapper element of the root node of the mapping file configuration.addLoadedResource(resource); bindMapperForNamespace(); //Focus on the method, which generates a dynamic proxy class according to the namespace attribute value } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
- configurationElement(XNode context) method
This method is mainly used to parse the element information in the mapper file, such as insert and select, into the MappedStatement object and save it into the mappedStatements attribute in the Configuration class, so that the real Sql statement information can be obtained when the subsequent dynamic agent class performs CRUD operations

configurationElement
The buildStatementFromContext method is used to parse element information such as insert and select, and encapsulate them into MappedStatement objects. The specific implementation details will not be described in detail here.
- bindMapperForNamespace() method
This method is the core method. It will generate a dynamic proxy class for the interface according to the namespace attribute value in the mapper file. This comes to our topic - how to generate a dynamic proxy class.
Generation of dynamic proxy classes
The source code of bindMapperForNamespace method is as follows:
private void bindMapperForNamespace() { //Gets the value of the namespace attribute of the mapper element String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // Gets the Class object corresponding to the namespace attribute value boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //If there is no such class, it will be ignored directly because the namespace attribute value only needs to be unique and does not necessarily correspond to an XXXMapper interface //When there is no XXXMapper interface, we can directly use SqlSession to add, delete, modify and query } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); //If the namespace attribute value has a corresponding Java class, call the addMapper method of Configuration to add it to MapperRegistry configuration.addMapper(boundType); } } } }
The addMapper method of Configuration is mentioned here. In fact, the Configuration class maintains all the xxmapper interface information to generate the dynamic proxy class through the MapperRegistry object. It can be seen that the Configuration class is indeed a very important class
public class Configuration { ... protected MapperRegistry mapperRegistry = new MapperRegistry(this); ... public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } ... }
There are two important methods: getMapper() and addMapper()
- getMapper(): a dynamic class used to create an interface
- Addmapper(): when mybatis parses the configuration file, it registers the interface to generate the dynamic proxy class
1. Configuration#addMappper()
Configuration delegates the addMapper method to the addMapper of MapperRegistry. The source code is as follows:
public <T> void addMapper(Class<T> type) { // This class must be an interface. Because it uses JDK dynamic proxy, it needs to be an interface. Otherwise, dynamic proxy will not be generated for it if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // Generate a MapperProxyFactory for later generation of dynamic proxy classes knownMappers.put(type, new MapperProxyFactory<>(type)); //The following code snippet is used to parse the annotations used in the xxmapper interface defined by us, which is mainly used to deal with the situation that xml mapping files are not used MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
MapperRegistry internally maintains a mapping relationship, and each interface corresponds to a MapperProxyFactory (generating a dynamic proxy factory class)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
In this way, when calling getMapper() of MapperRegistry later, it is convenient to directly obtain the dynamic proxy factory class corresponding to an interface from the Map, and then use the factory class to generate a real dynamic proxy class for its interface.
2. Configuration#getMapper()
The getMapper() method of Configuration calls the getMapper() method of MapperRegistry. The source code is as follows:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //Get the factory object MapperProxyFactory that creates the dynamic proxy according to the Class object final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { //Here you can see that a new proxy object is created for each call return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
As can be seen from the above, the core code for creating a dynamic proxy class is in mapperproxyfactory In the newinstance method, the source code is as follows:
protected T newInstance(MapperProxy<T> mapperProxy) { //JDK dynamic proxy is used here through proxy Newproxyinstance generates a dynamic proxy class // Parameters of newProxyInstance: class loader, interface class, InvocationHandler interface implementation class // The dynamic agent can redirect the calls of all interfaces to the calling processor InvocationHandler and call its invoke method return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
"PS: the detailed introduction of JDK dynamic agent will not be detailed here. If you are interested, please refer to my previous article: the principle and application of dynamic agent
The implementation class of InvocationHandler interface here is MapperProxy, and its source code is as follows:
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //If the method defined in the Object class is called, it can be called directly through reflection 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); } //Call the method customized by the xxmapper interface to proxy //First, construct the currently called Method into a MapperMethod object, and then use its execute Method to really start execution. final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } ... }
The final execution logic lies in the execute method of MapperMethod class. The source code is as follows:
public class MapperMethod { 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, mapperInterface, method); } public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { //Processing logic of insert statement case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } //Processing logic of update statement case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } //Processing logic of delete statement case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } //Processing logic of select statement 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); //Call the selectOne method of sqlSession 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()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } ... }
There are also two internal classes in MapperMethod, SqlCommand and MethodSignature. In the execute method, first use the switch case statement to judge the type of sql to be executed according to the getType() method of SqlCommand, such as insert, UPDATE, DELETE, SELECT and FLUSH, and then call the addition, deletion, modification and query methods of SqlSession respectively.
Wait, after all that, when will this getMapper() method be called? In fact, at the beginning, we call the getMapper() method of SqlSession:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class); public class DefaultSqlSession implements SqlSession { private final Configuration configuration; private final Executor executor; @Override public <T> T getMapper(Class<T> type) { return configuration.getMapper(type, this); } ... }
Therefore, the approximate calling logical chain of getMapper method is: SqlSession#getMapper() - > configuration #getMapper() - > mapperregistry #getMapper() - > mapperproxyfactory #newinstance() - > proxy #newproxyinstance()
Another point we need to pay attention to: we obtain the interface proxy through the getMapper method of SqlSession to perform CRUD operation. Its bottom layer still depends on the usage method of SqlSession.
Summary
According to the above inquiry process, a logic diagram is simply drawn (not necessarily accurate):

This article mainly introduces the dynamic principle of MyBatis. Looking back, we need to know that we use the dynamic proxy class of UserMapper to perform CRUD operation. In essence, we still perform addition, deletion, modification and query operations through the key class SqlSession. However, we have not carefully described how SqlSession performs CRUD operation. Interested students can refer to relevant materials.
References & acknowledgements
- http://www.tianshouzhi.com/api/tutorials/mybatis/360
- http://www.mybatis.org/mybatis-3/