Analysis of dynamic proxy implementation of mybatis mapper

Time:2021-1-20

Introduction of mybatis core components

  • Sqlsession factorybuilder (constructor): it can create a sqlsession factry through XML, annotation or manual configuration
  • Sqlsession factory: the factory used to create a sqlsession
  • Sqlsession: sqlsession is the core class of mybatis, which can be used to execute statements, commit or roll back transactions, and get mapper interface
  • SQL mapper: it is composed of an interface, an XML configuration file or annotation. It needs to give the corresponding SQL and mapping rules. It is responsible for sending SQL to execute and giving play to the results
  • Component use case:

    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);
            }
        }
    }
    //Results: the results were as follows
    User : User{id=1, age=21, name='pjmike'}

Implementation of mybatis dynamic proxy

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 use the getmapper method of sqlsession to get the usermapper object. In fact, we get the proxy class of the usermapper interface, and then the proxy class executes the method. Before exploring the implementation of dynamic proxy class, we need to make clear what preparations have been made in the creation process of sqlsessionfactory factory.

Analysis of mybatis global configuration file

private static SqlSessionFactory sqlSessionFactory;
static {
    try {
        sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream("mybatis-config.xml"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

We usenew SqlSessionFactoryBuilder().build()Method to create a sqlsessionfactory factory

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 mybatis global profile parsing, the relevant parsing code is mainly inXMLConfigBuilderOfparse()In the method

public Configuration parse() {
        if (this.parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
            this.parsed = true;
            //Parsing global profile
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            return this.configuration;
        }
    }

  private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.typeAliasesElement(root.evalNode("typeAliases"));
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            this.settingsElement(settings);
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
           this.typeHandlerElement(root.evalNode("typeHandlers"));
            //Parse mapper configuration file
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

FromparseConfigurationMethod can be seen in the source code,XmlConfigBuilderreadmybatis-config.xmlAnd then save the information to theconfigurationIn class

Mapper file analysis of mapper

//Parse mapper configuration file
this.mapperElement(root.evalNode("mappers"));

This method is based on the global configuration filemappersAnalysis of attribute

private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(true) {
                while(var2.hasNext()) {
                    XNode child = (XNode)var2.next();
                    String resource;
                    if ("package".equals(child.getName())) {
                        resource = child.getStringAttribute("name");
                        this.configuration.addMappers(resource);
                    } else {
                        resource = child.getStringAttribute("resource");
                        String url = child.getStringAttribute("url");
                        String mapperClass = child.getStringAttribute("class");
                        XMLMapperBuilder mapperParser;
                        InputStream inputStream;
                        if (resource != null && url == null && mapperClass == null) {
                            ErrorContext.instance().resource(resource);
                            inputStream = Resources.getResourceAsStream(resource);
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
                            mapperParser.parse();
                        } else if (resource == null && url != null && mapperClass == null) {
                            ErrorContext.instance().resource(url);
                            inputStream = Resources.getUrlAsStream(url);
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
                            mapperParser.parse();
                        } else {
                            if (resource != null || url != null || mapperClass == null) {
                                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                            }

                            Class<?> mapperInterface = Resources.classForName(mapperClass);
                            this.configuration.addMapper(mapperInterface);
                        }
                    }
                }

                return;
            }
        }
    }

Among themmapperParser.parse()The way isXmlMapperBuilderAnalysis of mapper file

public void parse() {
        if (!this.configuration.isResourceLoaded(this.resource)) {
            //This method is mainly used to parse the element information of mappedstatement object in mapper file and save it to mappedstatements attribute of configuration class
            this.configurationElement(this.parser.evalNode("/mapper"));    
            this.configuration.addLoadedResource(this.resource);
            //This method will generate dynamic proxy classes according to the value of the namespace attribute
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }

Core method:bindMapperForNamespace()Method, which generates a dynamic proxy class for the interface class according to the value of the namespace attribute in the mapper file

Generation of dynamic proxy class

private void bindMapperForNamespace() {
        //Gets the element value of the mapper element's namespace
        String namespace = this.builderAssistant.getCurrentNamespace();
        if (namespace != null) {
            Class boundType = null;
            try {
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException var4) {
                //If there is no such class, it can be ignored directly. This is because the value of the namespace property only needs to be unique, and it does not necessarily correspond to an xxmapper interface. If there is no xxmapper interface, we can directly use sqlsession to add, delete, and query
            }

            if (boundType != null && !this.configuration.hasMapper(boundType)) {
                this.configuration.addLoadedResource("namespace:" + namespace);
                //If the value of the namespace attribute has a corresponding Java class, call the addmapper method in configuration to add it to mapperregistry
                this.configuration.addMapper(boundType);
            }
        }

    }
public <T> void addMapper(Class<T> type) {
    //This class must be a class interface, because the JDK dynamic proxy is used, so the interface is needed, otherwise the dynamic proxy will not be generated for it
        if (type.isInterface()) {
            if (this.hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                //It is used to generate a mapperproxyfactry, which is used to generate a dynamic proxy class later
                this.knownMappers.put(type, new MapperProxyFactory(type));
                //The following code block is mainly used to parse the annotations used in the xxmapper interface that we defined. Here, we mainly deal with the case that XML mapping files are not used
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    this.knownMappers.remove(type);
                }

            }
        }

    }

MapperRegistryA mapping relationship is maintained internally, and each interface corresponds to a mapperproxyfactory (generating dynamic proxy factory class)

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

This is easy to call laterMapperRegistryOfgetMapper()The dynamic proxy factory class corresponding to an interface is directly obtained from the map, and then the factory class is used to generate the real dynamic proxy class for its interface

Getmapper() method of configuration

Let’s go throughGetmapper() of sqlsessionMethod call to get the dynamic proxy class

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

ConfigurationIngetMapper()Methods are actually used internallyMapperRegistryOfgetMapper()method

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                //As you can see, each call generates a new proxy object return
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
protected T newInstance(MapperProxy<T> mapperProxy) {
    //Here, we use JDK dynamic proxy, through the Proxy.newProxyInstance Generate dynamic proxy class
    //Parameters of newproxyinstance: class loader, interface class, invocationhandler interface implementation class
    //The dynamic proxy can redirect all interface calls to the invocationhandler and call its invoke method
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }

Here is the implementation class mapperproxy of invocationhandler interface

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

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

            if (this.isDefaultMethod(method)) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
        //Call the method defined by xxmapper interface to proxy
        // first, the currently called method is constructed into a MapperMethod object, and then its excute method is invoked to really start execution.
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }
       ...
}
public Object execute(SqlSession sqlSession, Object[] args) {
        Object param;
        Object result;
        switch(this.command.getType()) {
        case INSERT:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
            break;
        case UPDATE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
            break;
        case DELETE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
            break;
        case SELECT:
            if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (this.method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            } else if (this.method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            } else if (this.method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            } else {
                param = this.method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(this.command.getName(), param);
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + this.command.getName());
        }

        if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
            throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
        } else {
            return result;
        }
    }

InMapperMethodThere are also two inner classes, SqlCommand and methodsignature, which are used first in the execute methodswithc caseStatement based onSqlCommandOfgetType()Method, determine the type of SQL to execute, and then call the SqlSession add and delete method.

Getmapper () method flow chart

Analysis of dynamic proxy implementation of mybatis mapper

References & thanks


  • https://pjmike.github.io/2018…