Mybatis source code analysis

Mybatis source code analysis

The first is the basic steps of Mybatis

public class StudentMapperTest {
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            //A Configuration object is actually created internally, which creates the DefaultSqlSessionFactory object
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testSelectList() {
        SqlSession sqlSession = null;
        try {
            sqlSession = sqlSessionFactory.openSession();
			//The method is actually called through the executor object
            List<Student> students = sqlSession.selectList("com.demo.mapper.UserMapper.getUserByName");
            //This is actually through this configuration. getMapper(type, this); To get the of the proxy object. The proxy object executes sql through sqlSession internally.
            //The internal of sqlSession is actually executed through the executor. The L1 cache is in the executor, so all methods executed through the same sqlSession share the L1 cache.
            //sqlSessionFactory. If opensession () creates another sqlSession, the internal executor is actually newly created, so it will not share the first level cache with the previous sqlSession.
            Mapper mapper = sqlSession.getMapper(mapper.class);
            for (int i = 0; i < students.size(); i++) {
                System.out.println(students.get(i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }
}

Here is the specific 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">
<!-- Through this configuration file mybatis Connection to database -->
<configuration>
    <!-- Import data source configuration database.properties file -->
    <properties resource="database.properties"></properties>

    <!--to configure mybatis Some behaviors in operation -->
    <settings>
        <!-- set up Mybatis of log Realize as LOG4J -->
        <setting name="logImpl" value="LOG4J"/>
    </settings>

    <typeAliases>
        <!-- 
        <typeAlias alias="User" type="com.zy.entity.User"/>
         -->
        <package name="cn.zy.entity"/>
    </typeAliases>

    <!-- to configure mybatis Operating environment -->
    <environments default="dev">
        <environment id="dev">
            <!-- use jdbc transaction management -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- use Mybatis Self contained data source POOLED -->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${user}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- take mapper Add file to configuration file -->
    <mappers>
        <mapper resource="cn/zy/dao/UserMapper.xml"/>
        <package name="com.hytc.mapper" />
        <package url="....." />
        <package class="Interface path"/>
    </mappers>
   
</configuration>

First, put the process of executing sql statements through sqlSession. Here is the reference Coder648 blog

//This method is implemented in the parent BaseExecutor of simpleexecution
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	//The SQL statement is obtained dynamically according to the passed in parameters, and the final return is represented by BoundSql object
    BoundSql boundSql = ms.getBoundSql(parameter);
    //Create cached Key for this query
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }
 
//Enter the overloaded method of query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      	// If there is no value found this time in the cache, query from the database
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

//Query from database
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // Query method
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // Put query results into cache
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

// doQuery abstract method for implementing parent class in simpleexecution
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // Passing in parameters creates a StatementHanlder object to execute the query
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // Create a statement object in jdbc
      stmt = prepareStatement(handler, ms.getStatementLog());
      // StatementHandler
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

// Method of creating Statement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //After many calls, the getConnection method in this code will finally call the openConnection method to obtain the connection from the connection pool.
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
//Method of obtaining connection from connection pool
protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    //Get connection from connection pool
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommit);
  }


//Enter the query processed by StatementHandler. The default in StatementHandler is PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //Execution of native jdbc
    ps.execute();
    //The processing result is returned.
    return resultSetHandler.handleResultSets(ps);
  }

Let's look at the source code according to the basic use

First step

Reader reader = Resources.getResourceAsReader("mybatis-config.xml");

Gets the byte stream object of the mybatis configuration file

Step two

sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

Get the sqlSessionFactory object, which is actually an interface. The object we get is actually the implementation class DefaultSqlSessionFactory of the interface.

The source code of SqlSessionFactoryBuilder is as follows

Parameters of build method
1. Properties is actually a hashTable that stores the location information of the external database connection pool. Parameters can be passed in or configured in the configuration file, because they are finally stored in a default properties. The function is to provide configuration data (url username password) to the database connection pool
2. The reader reads the file stream object of the mybatis configuration file, which is the main function
3. environment. This parameter is used to specify which data source to use when mybatis is configured with multiple data sources.

public class SqlSessionFactoryBuilder {
    public SqlSessionFactoryBuilder() {
    }
    
	//It can be seen from this that build can be roughly divided into two categories, mainly character stream and character stream.
    public SqlSessionFactory build(Reader reader) {
        return this.build((Reader)reader, (String)null, (Properties)null);
    }

    public SqlSessionFactory build(Reader reader, String environment) {
        return this.build((Reader)reader, environment, (Properties)null);
    }

    public SqlSessionFactory build(Reader reader, Properties properties) {
        return this.build((Reader)reader, (String)null, properties);
    }

    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
        	//parser creates the Configuration and initializes it according to the Configuration file
            XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
            //var5 is the object returned by calling build (see the bottom method), that is, DefaultSqlSessionFactory
            var5 = this.build(parser.parse()Pass in a Configuration);
        } 
        ....Omit several
        return var5;
    }

    public SqlSessionFactory build(InputStream inputStream) {
        return this.build((InputStream)inputStream, (String)null, (Properties)null);
    }

    public SqlSessionFactory build(InputStream inputStream, String environment) {
        return this.build((InputStream)inputStream, environment, (Properties)null);
    }

    public SqlSessionFactory build(InputStream inputStream, Properties properties) {
        return this.build((InputStream)inputStream, (String)null, properties);
    }

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());
        } 
		....Omit several
        return var5;
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}

Next is the source code of XMLConfigBuilder, omitting several

public class XMLConfigBuilder extends BaseBuilder {
    private boolean parsed;
    private final XPathParser parser;
    private String environment;
    private final ReflectorFactory localReflectorFactory;

    public XMLConfigBuilder(Reader reader) {
        this((Reader)reader, (String)null, (Properties)null);
    }

    public XMLConfigBuilder(Reader reader, String environment) {
        this((Reader)reader, environment, (Properties)null);
    }

    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
    	//XPathParser is used to parse xml configuration files
        this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    public XMLConfigBuilder(InputStream inputStream) {
        this((InputStream)inputStream, (String)null, (Properties)null);
    }

    public XMLConfigBuilder(InputStream inputStream, String environment) {
        this((InputStream)inputStream, environment, (Properties)null);
    }

    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    	//First, the Configuration object is created. And passed to its parent class, which is also the object returned by parse
        super(new Configuration());
        this.localReflectorFactory = new DefaultReflectorFactory();
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }

    public Configuration parse() {
    	//parsed here is a global variable, and multiple initialization of configuration is not allowed
        if (this.parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
            this.parsed = true;
   			//This method is actually an xml configuration file. Each tag pair is parsed into a node object, and the configuration is initialized
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            //Finally, the configuration object inherited from the parent class is returned
            return this.configuration;
        }
    }
	//Here are the specific methods. Most of the following methods adopt this form configuration. Setvfsimpl (vfsimpl) initializes the configuration.
    private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.loadCustomLogImpl(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"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

    
    private void loadCustomVfs(Properties props) {
        this.configuration.setVfsImpl(vfsImpl);
    }

    private void loadCustomLogImpl(Properties props) { 
        this.configuration.setLogImpl(logImpl);
    }

    private void typeAliasesElement(XNode parent) {
       this.configuration.getTypeAliasRegistry().registerAliases(alias);
    }

    private void pluginElement(XNode parent) throws Exception {
 		 this.configuration.addInterceptor(interceptorInstance);
    }

    private void objectFactoryElement(XNode context) throws Exception {						     		this.configuration.setObjectFactory(factory) 
    }

    private void objectWrapperFactoryElement(XNode context) throws Exception {
        this.configuration.setObjectWrapperFactory(factory);
    }

    private void reflectorFactoryElement(XNode context) throws Exception {
       this.configuration.setReflectorFactory(factory);
    }

    private void propertiesElement(XNode context) throws Exception {
    	if (context != null) {
            Properties defaults = context.getChildrenAsProperties();
            String resource = context.getStringAttribute("resource");
            String url = context.getStringAttribute("url");
            if (resource != null && url != null) {
                throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
            }

            if (resource != null) {
                defaults.putAll(Resources.getResourceAsProperties(resource));
            } else if (url != null) {
            	//This step is to parse the resource, return a property object and put it into defaults
                defaults.putAll(Resources.getUrlAsProperties(url));
            }
			//Here is to get the Properties passed in by build, because this. Is set earlier configuration. setVariables(props);
            Properties vars = this.configuration.getVariables();
            //If it is not empty, merge.
            if (vars != null) {
                defaults.putAll(vars);
            }
            //The purpose of setting is to provide data to replace ${user} with real value.
            this.parser.setVariables(defaults);
            //Assign the final merged Properties to the configuration. Note that Properties is actually the configuration of connecting to the database stored in a hashTable
            this.configuration.setVariables(defaults);
        }

    }

    private void settingsElement(Properties props) {
        this.configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
        this.configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
        //Enable L2 cache configuration
        this.configuration.setCacheEnabled(this.booleanValueOf(props.getProperty("cacheEnabled"), true));
        this.configuration.setProxyFactory((ProxyFactory)this.createInstance(props.getProperty("proxyFactory")));
        this.configuration.setLazyLoadingEnabled(this.booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
        this.configuration.setAggressiveLazyLoading(this.booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
        this.configuration.setMultipleResultSetsEnabled(this.booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
        this.configuration.setUseColumnLabel(this.booleanValueOf(props.getProperty("useColumnLabel"), true));
        this.configuration.setUseGeneratedKeys(this.booleanValueOf(props.getProperty("useGeneratedKeys"), false));
        this.configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
        this.configuration.setDefaultStatementTimeout(this.integerValueOf(props.getProperty("defaultStatementTimeout"), (Integer)null));
        this.configuration.setDefaultFetchSize(this.integerValueOf(props.getProperty("defaultFetchSize"), (Integer)null));
        this.configuration.setDefaultResultSetType(this.resolveResultSetType(props.getProperty("defaultResultSetType")));
        this.configuration.setMapUnderscoreToCamelCase(this.booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
        this.configuration.setSafeRowBoundsEnabled(this.booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
        this.configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
        this.configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
        this.configuration.setLazyLoadTriggerMethods(this.stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
        this.configuration.setSafeResultHandlerEnabled(this.booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
        this.configuration.setDefaultScriptingLanguage(this.resolveClass(props.getProperty("defaultScriptingLanguage")));
        this.configuration.setDefaultEnumTypeHandler(this.resolveClass(props.getProperty("defaultEnumTypeHandler")));
        this.configuration.setCallSettersOnNulls(this.booleanValueOf(props.getProperty("callSettersOnNulls"), false));
        this.configuration.setUseActualParamName(this.booleanValueOf(props.getProperty("useActualParamName"), true));
        this.configuration.setReturnInstanceForEmptyRow(this.booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
        this.configuration.setLogPrefix(props.getProperty("logPrefix"));
        this.configuration.setConfigurationFactory(this.resolveClass(props.getProperty("configurationFactory")));
    }

    private void environmentsElement(XNode context) throws Exception {
          if (context != null) {
          	//Here is to judge the String environment parameter passed from built. If it is not passed, the default data source will be used. Of course, the default is also specified in the configuration file
            if (this.environment == null) {
                this.environment = context.getStringAttribute("default");
            }

            Iterator var2 = context.getChildren().iterator();
			//Here is to traverse all data sources and find the data source with the same name as environment
            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String id = child.getStringAttribute("id");
                if (this.isSpecifiedEnvironment(id)) {
                    TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
                    //Create a database connection pool factory object.
                    DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    //Cascade call is actually an object encapsulated by return key id, dataSource and txFactory
                    Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
                  
                    
                   //environmentBuilder. The build () method takes the above three parameters as the parametric construction parameters of environment to create an environment object. Environment is actually a package of these three parameters
                    this.configuration.setEnvironment(environmentBuilder.build());
                }
            }
        }
    }
    //Create connection pool factory
    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
            String type = context.getStringAttribute("type");
            //This step obtains the information of connecting to the database and so on
            Properties props = context.getChildrenAsProperties();
            DataSourceFactory factory = (DataSourceFactory)this.resolveClass(type).newInstance();
            factory.setProperties(props);
            return factory;
        } else {
            throw new BuilderException("Environment declaration requires a DataSourceFactory.");
        }
    }

    private void databaseIdProviderElement(XNode context) {
            this.configuration.setDatabaseId(databaseId);
    }
    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;
                    //There are two types of mappers: package and other URL class resources
                    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;
                        //It can be seen from this that for each mapper url class resource, only one can be selected
                        if (resource != null && url == null && mapperClass == null) {
                            ErrorContext.instance().resource(resource);
                            inputStream = Resources.getResourceAsStream(resource);
                            //Here is the map Parsing XML is similar to parsing mybatis configuration files
                            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.");
                            }
							//No matter what method is used, the mapperInterface will eventually be created and implemented through addMapper, and the above will also call this configuration. addMapper () method, which is stored in a map set of mapperRegistry attribute of configuration. Map < class <? >, MapperProxyFactory<?>> In the collection. The value in this set is the mapper's proxy factory, which is used to create the mapper's proxy object. Here, the factory is just created. When calling getMapper, the proxy object is created
                            Class<?> mapperInterface = Resources.classForName(mapperClass);
                            this.configuration.addMapper(mapperInterface);
                        }
                    }
                }

                return;
            }
        }
    }
}

Here is the method of mapper operation

public <T> void addMapper(Class<T> type) {
        this.mapperRegistry.addMapper(type);
}
//This MapperRegistry is also a member variable of Configuration
public class MapperRegistry {
    private final Configuration config;
    //this.mapperRegistry.addMapper(type) is stored in the map known mappers
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }
	//This getMapper is a proxy object that returns a Mapper
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    	//Get from knownMappers, whose value is given by calling the addMapper method. This method is called during Configuration initialization.
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
            	//Check this method in detail. This method creates the mapper proxy object
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }

    public <T> boolean hasMapper(Class<T> type) {
        return this.knownMappers.containsKey(type);
    }

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (this.hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }

            boolean loadCompleted = false;

            try {
                this.knownMappers.put(type, new MapperProxyFactory(type));
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    this.knownMappers.remove(type);
                }

            }
        }

    }

    public Collection<Class<?>> getMappers() {
        return Collections.unmodifiableCollection(this.knownMappers.keySet());
    }
	//Add the class es in the scanning package to the knownMappers map collection
    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        //This function scans all class es in the package and adds the type new IsA(superType) to the mapperSet, which is the set set returned below
        resolverUtil.find(new IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        Iterator var5 = mapperSet.iterator();

        while(var5.hasNext()) {
            Class<?> mapperClass = (Class)var5.next();
            this.addMapper(mapperClass);
        }

    }

    public void addMappers(String packageName) {
        this.addMappers(packageName, Object.class);
    }
}

mapperProxyFactory

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return this.mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return this.methodCache;
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }
	//Why pass in sqlsession? In fact, the sql statements executed through the mapper proxy object are actually executed through sqlsession,
    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

MapperProxy

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 {
        	//Method for directly releasing Object definitions
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }
			//Default method of direct release. The default method is to define an implemented method in the interface, and the implementation class of the interface does not need to implement the method. Why should there be a default method. Because if there is no default method and a new abstract method is added to an interface in the JDK, all classes that implement the interface will have to be modified, which will have a great impact.
            if (method.isDefault()) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
		//Create a mappermethod object, which executes sql statements by calling sqlSession. At the same time, mappermethod has an attribute command, which encapsulates a mappedstatements from the configuration object get(mapperInterface.getName() + "." +  Get methodname).
		//Then get the id="com.demo.mapper.UserMapper.getUserByName" of mappedStatement and assign it to command. Used to execute sqlSession Method ("com.demo.mapper.UserMapper.getUserByName",params)
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        return (MapperMethod)this.methodCache.computeIfAbsent(method, (k) -> {
            return new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
        });
    }

    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
        Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE);
        if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }

        Class<?> declaringClass = method.getDeclaringClass();
        return ((Lookup)constructor.newInstance(declaringClass, 15)).unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
    }
}

The above is a process of creating a Configuration object by reading the mybatis Configuration file and initializing the Configuration object. It mainly occurs in the XMLConfigBuilder method. The focus is to create an environment object by reading the Configuration information. This object encapsulates the database connection pool object, transaction factory object and id specified by us. Store the mapper proxy factory object in the knownMappers collection of MapperRegistry

After creating the Configuration object, create a new DefaultSqlSessionFactory(config) object through this object, and then obtain the SqlSession object. Note that the SqlSession encapsulates the Configuration object

    private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
        DefaultSqlSession var8;
        try {
            boolean autoCommit;
            try {
                autoCommit = connection.getAutoCommit();
            } catch (SQLException var13) {
                autoCommit = true;
            }

            Environment environment = this.configuration.getEnvironment();
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            Transaction tx = transactionFactory.newTransaction(connection);
            //configuration. The source code of newexecutor is as follows, together with the following figure
            Executor executor = this.configuration.newExecutor(tx, execType);
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var14, var14);
        } finally {
            ErrorContext.instance().reset();
        }

        return var8;
    }

    private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
        return (TransactionFactory)(environment != null && environment.getTransactionFactory() != null ? environment.getTransactionFactory() : new ManagedTransactionFactory());
    }

Here is the inheritance system of Executor

The lowest level interface is Executor, which has two implementation classes: BaseExecutor and cacheingexecution. Cacheingexecution is used for L2 cache, while BaseExecutor is used for L1 cache and basic operations.

1. Simpleexecution is the simplest executor. It can be executed directly according to the corresponding sql without any additional operations;
2. Batch executor executor, as its name suggests, optimizes performance through batch operations. Generally, you should pay attention to the batch update operation. Since there is an internal cache implementation, remember to call flushStatements to clear the cache after use.
3. Reuseexecution is a reusable executor. The reused object is Statement, that is, the executor will cache the Statement of the same sql, eliminating the re creation of Statement and optimizing performance. The internal implementation maintains the Statement object through a HashMap. Since the current Map is only valid in this sqlsession, remember to call flushStatements to clear the Map after use.

public class Configuration {
	//The member variable is initialized by reading the configuration file. Is the L2 cache enabled
	protected boolean cacheEnabled;
	//Every sql statement in mapper will be encapsulated into a MappedStatement object. key = fully qualified class name + method name, value = corresponding MappedStatement object
	protected final Map<String, MappedStatement> mappedStatements;
	//The mapper proxy factory object is stored internally, and the proxy object is created and returned through the getMapper method
	protected final MapperRegistry mapperRegistry;
	//This object mainly encapsulates the database connection pool object, transaction object and id
	protected Environment environment;
	//This is mainly the information connecting to the database, such as user = root, password = 1234, etc. it is actually a hashTable
	protected Properties variables;

	public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
	        executorType = executorType == null ? this.defaultExecutorType : executorType;
	        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
	        Object executor;
	        if (ExecutorType.BATCH == executorType) {
	            executor = new BatchExecutor(this, transaction);
	        } else if (ExecutorType.REUSE == executorType) {
	            executor = new ReuseExecutor(this, transaction);
	        } else {
	            executor = new SimpleExecutor(this, transaction);
	        }
	
	        if (this.cacheEnabled) {
	            executor = new CachingExecutor((Executor)executor);
	        }
	
	        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
	        return executor;
	    }
}

Let's take a look at the source code of BaseExecutor

public abstract class BaseExecutor implements Executor {
	//This is the first level cache, which encapsulates a HashMap private map < object, Object > cache = new hashmap(); BaseExecutor is an attribute of sqlsession, so the scope of L1 cache is sqlsession
    protected PerpetualCache localCache;
	//The query is called through the sqlsession object. When the method is called through the sqlsession, the BaseExecutor method is called internally to execute the sql statement
	public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                //First judge whether there is in the cache
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                	//Note that this key value is determined by cachekey = this createCacheKey(ms, parameter, rowBounds, boundSql); These parameters affect the
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                	//Here is the real query in the database.
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }

                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }

            return list;
        }
    }
  
}

Let's take a look at the source code of DefaultSqlSession

public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;
    private final boolean autoCommit;
	
	//From this, we can see that executing sql statements through Sqlsession is actually through executor Query.
	public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

    }  
}

As for L2 cache, note that there is not only one L2 cache. For each MappedStatement, there is a cache attribute, which is the L2 buffer corresponding to the sql statement. Each MappedStatement is created when initializing Configuration. Then take the cache as the key value and obtain value=TransactionalCache. There is a temporary secondary cache inside the TransactionalCache. Pass the cache in. There is an internal method flushPendingEntries to assign data between the temporary cache and the cache.

public class CachingExecutor implements Executor {
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();
	
	//The Executor passed in here is one of the three executors created above
    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }

    public Transaction getTransaction() {
        return this.delegate.getTransaction();
    }

    public void close(boolean forceRollback) {
        try {
            if (forceRollback) {
                this.tcm.rollback();
            } else {
                this.tcm.commit();
            }
        } finally {
            this.delegate.close(forceRollback);
        }

    }

    public boolean isClosed() {
        return this.delegate.isClosed();
    }

    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    	//Note that if the flushCache attribute is configured in the mybatis configuration, the temporary buffer will be emptied during update. Otherwise, only the real L2 buffer will be cleaned up
        this.flushCacheIfRequired(ms);
        return this.delegate.update(ms, parameterObject);
    }

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        this.flushCacheIfRequired(ms);
        return this.delegate.queryCursor(ms, parameter, rowBounds);
    }

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    	//The key point is the place where the L2 cache is actually stored. Each MappedStatement shares one. Therefore, L2 cache can share data in different sqlsessions
        Cache cache = ms.getCache();
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                //First get the value from the L2 cache. If there is no value with the cache as key in the map, create a TransactionalCache object with the cache as a parameter, and get the data from the cache through the key.
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
                	//Here is the way to read the database from the first level cache
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    //Here is to put the read data into the temporary L2 cache. The data in the temporary L2 cache will be put into the cache in the MappedStatement only after the commit is executed
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        
    }
    
	public void commit(boolean required) throws SQLException {
       	this.delegate.commit(required);
       	//In fact, the commit method in TransactionalCache is called
       	//The commit here will store the real two level buffer in the temporary two level buffer and empty the temporary two level buffer. If the clear is called before calling commit, it will clear the two level buffer first and then store the temporary buffer into the two level buffer.
        this.tcm.commit();
    }
}

Methods in TransactionalCache class

	public void clear() {
        this.clearOnCommit = true;
        this.entriesToAddOnCommit.clear();
    }

    public void commit() {
        if (this.clearOnCommit) {
            this.delegate.clear();
        }

        this.flushPendingEntries();
        this.reset();
    }

    public void rollback() {
        this.unlockMissedEntries();
        this.reset();
    }

    private void reset() {
        this.clearOnCommit = false;
        this.entriesToAddOnCommit.clear();
        this.entriesMissedInCache.clear();
    }

    private void flushPendingEntries() {
        Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();

        while(var1.hasNext()) {
            Entry<Object, Object> entry = (Entry)var1.next();
            this.delegate.putObject(entry.getKey(), entry.getValue());
        }

        var1 = this.entriesMissedInCache.iterator();

        while(var1.hasNext()) {
            Object entry = var1.next();
            if (!this.entriesToAddOnCommit.containsKey(entry)) {
                this.delegate.putObject(entry, (Object)null);
            }
        }

    }

This is the main process. I don't think the idea is very clear. If there are mistakes, please point them out and I will correct them. The purpose of writing this article is mainly to take notes and hope to help others.

Keywords: Mybatis

Added by php_gromnie on Mon, 31 Jan 2022 12:32:51 +0200