MyBatis dynamic proxy implementation details

Posted by happyness on Sat, 18 Dec 2021 12:47:37 +0100

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:

  1. Database table
   CREATE TABLE  user(
     id int,
     name VARCHAR(255) not NULL ,
     age int ,
     PRIMARY KEY (id)
   )ENGINE =INNODB DEFAULT CHARSET=utf8;
  1. 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 + '\'' +
                   '}';
       }
   }
  1. 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>
  1. UserMapper interface
   public interface UserMapper {
       User selectById(int id);
   }
  1. 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>
  1. 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/