Hundreds of lines of code to write a Mybatis, the principle is thoroughly understood

Posted by xhelle on Wed, 05 Jan 2022 10:30:56 +0100

1, Foreword

The core principle of Mybatis is also the embodiment of its most convenient use. Why?

Because when we use Mybatis, we only need to define an interface that does not need to write implementation classes, and we can CRUD the database by annotation or configuring SQL statements.

So how do you do this? One very important point is that in Spring, you can hand over your proxy object to the Spring container. This proxy object can be regarded as a specific implementation class of DAO interface, and the proxy implementation class can complete an operation on the database, that is, this encapsulation process is called ORM framework.

After talking about the basic process, let's do some tests so that everyone can operate it! Learning knowledge must be first-hand, in order to get it! You can practice through the following source code repository

Source code:
https://github.com/fuzhengwei/CodeGuide/wiki

2, Insert the Bean into the Spring container in several steps

  • As for the technical scenario of Bean registration, MyBatis is the most common technical framework we use every day. When using MyBatis, you only define an interface without writing an implementation class, but this interface can be associated with the configured SQL statement, and the corresponding results can be returned when performing corresponding database operations. Then the proxy and registration of the Bean are used for the operation of this interface and the database.
  • We all know that a class call cannot directly call an interface that is not implemented, so we need to generate a corresponding implementation class for the interface through a proxy. Next, put the proxy class into the FactoryBean implementation of Spring, and finally register the FactoryBean implementation class with the Spring container. Now that your proxy class has been registered in the Spring container, you can inject it into the attribute by annotation.

According to this implementation method, let's operate to see how the registration process of a Bean is implemented in the code.

1. Define interface

public interface IUserDao { 

    String queryUserInfo();

}
  • First define an interface similar to DAO, which is still very common when using MyBatis. We will proxy and register this interface later.

2. Class proxy implementation

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = { IUserDao.class};    

InvocationHandler handler = (proxy, method, args) -> "You're represented " + method.getName();
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler); 

String res = userDao.queryUserInfo();
logger.info("Test results:{}", res);
  • The proxy method of Java itself is relatively simple to use, and the usage is also very fixed.
  • InvocationHandler is an interface class, and its corresponding implementation content is the specific implementation of the proxy object.
  • Finally, give Proxy to create Proxy object, Proxy newProxyInstance.

3. Implement Bean factory

public class ProxyBeanFactory implements FactoryBean { 

    @Override
    public Object getObject() throws Exception { 

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class[] classes = { IUserDao.class};
        InvocationHandler handler = (proxy, method, args) -> "You're represented " + method.getName();

        return Proxy.newProxyInstance(classLoader, classes, handler);
    }

    @Override
    public Class<?> getObjectType() { 
        return IUserDao.class;
    } 

}
  • FactoryBean plays a second leading role in spring. It has nearly 70 younger brothers (implementing its interface definition), so it has three methods;
    • T getObject() throws Exception; Return bean instance object
    • Class<?> getObjectType(); Returns the instance class type
    • boolean isSingleton(); Judge whether the singleton is. The singleton will be placed in the singleton cache pool in the Spring container
  • Here, we put the above object using Java proxy into the getObject() method, and now the object obtained from Spring is our proxy object.

4. Bean registration

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor { 

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { 

        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(ProxyBeanFactory.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

}

In Spring's Bean management, all beans will eventually be registered to the class
In DefaultListableBeanFactory, the main contents of the above code include:

  • Implement beandefinitionregistrypostprocessor Postprocessbeandefinitionregistry method to obtain the Bean registration object.
  • Define Bean, GenericBeanDefinition. Here we mainly set up our proxy class factory.
  • Create a Bean definition processing class, BeanDefinitionHolder, and the main parameters required here; Define the Bean and name setBeanClass(ProxyBeanFactory.class).
  • Finally, register our own bean in the spring container, registry registerBeanDefinition()

5. Test verification

Above, we have registered the Bean of the custom proxy into the Spring container. Next, let's test how the Bean of the proxy is called.

1. Define spring config xml

<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/>
  • Here, we configure RegisterBeanFactory into the xml configuration of spring to facilitate loading at startup.

2. Unit test

@Test
public void test_IUserDao() { 
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("Test results:{}", res);
}

test result

22:53:14.759 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
22:53:14.760 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
22:53:14.796 [main] INFO  org.itstack.interview.test.ApiTest - Test result: you are represented queryUserInfo

Process finished with exit code 0
  • From the test results, we can see that we can achieve our expected results by injecting the proxy Bean object into Spring.
  • In fact, this process is also used in many frameworks, especially in some middleware development, similar ORM frameworks need to be used.

3, Write a Mybatis

Expand the previous source code analysis project; Itstack demo Mybatis, add like package to imitate Mybatis project. Complete procedure Download
https://github.com/fuzhengwei/CodeGuide/wiki

itstack-demo-mybatis
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo
    │   │       ├── dao
    │   │       │	├── ISchool.java		
    │   │       │	└── IUserDao.java	
    │   │       ├── like
    │   │       │	├── Configuration.java
    │   │       │	├── DefaultSqlSession.java
    │   │       │	├── DefaultSqlSessionFactory.java
    │   │       │	├── Resources.java
    │   │       │	├── SqlSession.java
    │   │       │	├── SqlSessionFactory.java
    │   │       │	├── SqlSessionFactoryBuilder.java	
    │   │       │	└── SqlSessionFactoryBuilder.java	
    │   │       └── interfaces     
    │   │         	├── School.java	
    │   │        	└── User.java
    │   ├── resources	
    │   │   ├── mapper
    │   │   │   ├── School_Mapper.xml
    │   │   │   └── User_Mapper.xml
    │   │   ├── props	
    │   │   │   └── jdbc.properties
    │   │   ├── spring
    │   │   │   ├── mybatis-config-datasource.xml
    │   │   │   └── spring-config-datasource.xml
    │   │   ├── logback.xml
    │   │   ├── mybatis-config.xml
    │   │   └── spring-config.xml
    │   └── webapp
    │       └── WEB-INF
    └── test
         └── java
             └── org.itstack.demo.test
                 ├── ApiLikeTest.java
                 ├── MybatisApiTest.java
                 └── SpringApiTest.java

The whole Demo version as like as two peas is not the same as all Mybatis, but the core of the content is displayed to you. You will feel the same from the use, but the implementation class has been replaced.

  • Configuration
  • DefaultSqlSession
  • DefaultSqlSessionFactory
  • Resources
  • SqlSession
  • SqlSessionFactory
  • SqlSessionFactoryBuilder
  • XNode

1. Test the whole DemoJdbc framework first

ApiLikeTest.test_queryUserInfoById()

@Test
public void test_queryUserInfoById() { 
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try { 
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
		
        try { 
            User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
            System.out.println(JSON.toJSONString(user));
        } finally { 
            session.close();
            reader.close();
        }
    } catch (Exception e) { 
        e.printStackTrace();
    }
}

Everything goes well and the results are as follows (newcomers often encounter various problems);

{ "age":18,"createTime":1576944000000,"id":1,"name":"Water water","updateTime":1576944000000}

Process finished with exit code 0

Perhaps at first glance, this test class is completely different from mybatisapitest The code of Java as like as two peas is not the same. In fact, their introduced packages are different;

MybatisApiTest. Package introduced in Java

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

ApiLikeTest. Package introduced in Java

import org.itstack.demo.like.Resources;
import org.itstack.demo.like.SqlSession;
import org.itstack.demo.like.SqlSessionFactory;
import org.itstack.demo.like.SqlSessionFactoryBuilder;

OK! Next, we start to analyze this part of the core code.

2. Load XML configuration file

Here, we use the configuration file structure of mybatis for parsing, which is as close to the source code as possible without destroying the original structure. When mybatis is used alone, two configuration files are used; Data source configuration and Mapper mapping configuration are as follows:;


mybatis-config-datasource. XML & data source configuration

<?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">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>

User_ Mapper. XML & mapper mapping configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.itstack.demo.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{ id}
    </select>

    <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{ age}
    </select>

</mapper>

The loading process here is different from mybaits. We use dom4j. In the case, you will see that the resources obtained at the beginning are as follows;


ApiLikeTest. test_ Queryuserinfo byid() & partial interception

String resource = "spring/mybatis-config-datasource.xml";
	Reader reader;
	try { 
		reader = Resources.getResourceAsReader(resource);
	...

It can be seen from the above that this is the process of obtaining the read stream through the address of the configuration file, so as to lay the foundation for subsequent parsing. First, let's look at the Resources class, which is our resource class.

Resources. Java & resource class

/** * Blog| https://bugstack.cn *Create by little brother @ 2020 */
public class Resources { 

    public static Reader getResourceAsReader(String resource) throws IOException { 
        return new InputStreamReader(getResourceAsStream(resource));
    }

    private static InputStream getResourceAsStream(String resource) throws IOException { 
        ClassLoader[] classLoaders = getClassLoaders();
        for (ClassLoader classLoader : classLoaders) { 
            InputStream inputStream = classLoader.getResourceAsStream(resource);
            if (null != inputStream) { 
                return inputStream;
            }
        }
        throw new IOException("Could not find resource " + resource);
    }

    private static ClassLoader[] getClassLoaders() { 
        return new ClassLoader[]{ 
                ClassLoader.getSystemClassLoader(),
                Thread.currentThread().getContextClassLoader()};
    }
}

The entry of this code method is getResourceAsReader until it is done;

  1. Get the ClassLoader collection and search the configuration file to the maximum extent
  2. Through classloader Getresourceasstream reads the configuration resource and returns immediately after finding it. Otherwise, an exception is thrown

3. Parse XML configuration file

After the configuration file is loaded, start the parsing operation. Here we also follow the example of mybatis, but simplify it as follows;

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);


SqlSessionFactoryBuilder. Build () & build class for entry

public DefaultSqlSessionFactory build(Reader reader) { 
    SAXReader saxReader = new SAXReader();
    try { 
        Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) { 
        e.printStackTrace();
    }
    return null;
}
  • Create an xml parsed Document class by reading the stream
  • parseConfiguration parses the xml file and sets the result to the configuration class, including; Connection pool, data source, mapper relationship


SqlSessionFactoryBuilder. Parseconfiguration() & parse process

private Configuration parseConfiguration(Element root) { 
    Configuration configuration = new Configuration();
    configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
    configuration.setConnection(connection(configuration.dataSource));
    configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
    return configuration;
}
  • As you can see from the previous xml content, we need to parse the database connection pool information datasource and the database statement mapping relationship mappers


SqlSessionFactoryBuilder. Datasource () & resolve the datasource

private Map<String, String> dataSource(List<Element> list) { 
    Map<String, String> dataSource = new HashMap<>(4);
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) { 
        Element e = (Element) o;
        String name = e.attributeValue("name");
        String value = e.attributeValue("value");
        dataSource.put(name, value);
    }
    return dataSource;
}
  • This process is relatively simple. You only need to obtain the data source information


SqlSessionFactoryBuilder. Connection () & get database connection

private Connection connection(Map<String, String> dataSource) { 
    try { 
        Class.forName(dataSource.get("driver"));
        return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
    } catch (ClassNotFoundException | SQLException e) { 
        e.printStackTrace();
    }
    return null;
}
  • This is the original jdbc code, which obtains the database connection pool


SqlSessionFactoryBuilder. Mapperelement() & parse SQL statement

private Map<String, XNode> mapperElement(List<Element> list) { 
    Map<String, XNode> map = new HashMap<>();
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) { 
        Element e = (Element) o;
        String resource = e.attributeValue("resource");
        try { 
            Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            Element root = document.getRootElement();
            //Namespace
            String namespace = root.attributeValue("namespace");
            // SELECT
            List<Element> selectNodes = root.selectNodes("select");
            for (Element node : selectNodes) { 
                String id = node.attributeValue("id");
                String parameterType = node.attributeValue("parameterType");
                String resultType = node.attributeValue("resultType");
                String sql = node.getText();
                // ?  matching
                Map<Integer, String> parameter = new HashMap<>();
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) { 
                    String g1 = matcher.group(1);
                    String g2 = matcher.group(2);
                    parameter.put(i, g2);
                    sql = sql.replace(g1, "?");
                }
                XNode xNode = new XNode();
                xNode.setNamespace(namespace);
                xNode.setId(id);
                xNode.setParameterType(parameterType);
                xNode.setResultType(resultType);
                xNode.setSql(sql);
                xNode.setParameter(parameter);
                
                map.put(namespace + "." + id, xNode);
            }
        } catch (Exception ex) { 
            ex.printStackTrace();
        }
    }
    return map;
}
  • This process first includes parsing all sql statements. At present, only select related statements are parsed for testing
  • All sql statements are used to confirm uniqueness; The IDS in namespace + select are spliced as key s, and then stored in the map together with sql.
  • In the sql statement configuration of mybaits, placeholders are used to pass parameters. where id = #{id} so we need to set the placeholder as a question mark. In addition, we need to store the order information and name of the placeholder in the map structure to facilitate the subsequent setting of input parameters during query.

4. Create DefaultSqlSessionFactory

Finally, use the initialized Configuration class Configuration as a parameter to create DefaultSqlSessionFactory, as follows:;

public DefaultSqlSessionFactory build(Reader reader) { 
    SAXReader saxReader = new SAXReader();
    try { 
        Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) { 
        e.printStackTrace();
    }
    return null;
}


DefaultSqlSessionFactory. Implementation class of Java & sqlsessionfactory

public class DefaultSqlSessionFactory implements SqlSessionFactory { 
    
	private final Configuration configuration;
    
	public DefaultSqlSessionFactory(Configuration configuration) { 
        this.configuration = configuration;
    }
	
    @Override
    public SqlSession openSession() { 
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }
}
  • This process is relatively simple. The constructor only provides configuration class input parameters
  • openSession() of SqlSessionFactory is implemented to create DefaultSqlSession, which can also execute sql operations

5. Open SqlSession

SqlSession session = sqlMapper.openSession();

The above step is to create DefaultSqlSession, which is relatively simple. As follows;

@Override
public SqlSession openSession() { 
    return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}

6. Execute SQL statement

User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);

In DefaultSqlSession, SqlSession is implemented to provide database statement query and close connection pool, as follows;

SqlSession. Java & definition

public interface SqlSession { 
    <T> T selectOne(String statement);
    <T> T selectOne(String statement, Object parameter);
    <T> List<T> selectList(String statement);
    <T> List<T> selectList(String statement, Object parameter);
    void close();
}

Next, let's look at the specific execution process, session selectOne


DefaultSqlSession. Selectone() & execute query

public <T> T selectOne(String statement, Object parameter) { 
    XNode xNode = mapperElement.get(statement);
    Map<Integer, String> parameterMap = xNode.getParameter();
    try { 
        PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
        buildParameter(preparedStatement, parameter, parameterMap);
        ResultSet resultSet = preparedStatement.executeQuery();
        List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        return objects.get(0);
    } catch (Exception e) { 
        e.printStackTrace();
    }
    return null;
}
  • selectOne objects get(0);, selectList returns all
  • Obtain the stored select tag information when parsing xml initially through the statement;

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select>

  • After obtaining the sql statement, give it to the PreparedStatement class of jdbc for execution
  • Here, we also need to set the input parameter. We extract the input parameter settings, as follows;
private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException { 

      int size = parameterMap.size();
      // Single parameter
      if (parameter instanceof Long) { 
          for (int i = 1; i <= size; i++) { 
              preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
          }
          return;
      }

      if (parameter instanceof Integer) { 
          for (int i = 1; i <= size; i++) { 
              preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
          }
          return;
      }

      if (parameter instanceof String) { 
          for (int i = 1; i <= size; i++) { 
              preparedStatement.setString(i, parameter.toString());
          }
          return;
      }

      Map<String, Object> fieldMap = new HashMap<>();
      // Object parameters
      Field[] declaredFields = parameter.getClass().getDeclaredFields();
      for (Field field : declaredFields) { 
          String name = field.getName();
          field.setAccessible(true);
          Object obj = field.get(parameter);
          field.setAccessible(false);
          fieldMap.put(name, obj);
      }

      for (int i = 1; i <= size; i++) { 
          String parameterDefine = parameterMap.get(i);
          Object obj = fieldMap.get(parameterDefine);

          if (obj instanceof Short) { 
              preparedStatement.setShort(i, Short.parseShort(obj.toString()));
              continue;
          }

          if (obj instanceof Integer) { 
              preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
              continue;
          }

          if (obj instanceof Long) { 
              preparedStatement.setLong(i, Long.parseLong(obj.toString()));
              continue;
          }

          if (obj instanceof String) { 
              preparedStatement.setString(i, obj.toString());
              continue;
          }

          if (obj instanceof Date) { 
              preparedStatement.setDate(i, (java.sql.Date) obj);
          }

      }

  }
  • A single parameter is relatively simple. You can set the value directly. Long, Integer, String... If it is a class object, you need to obtain the Field property to match and set it with the parameter Map
  • After setting parameters, execute the query Preparedstatement executeQuery()
  • Next, we need to convert the query result into our class (mainly the operation of reflection class), resultset2obj (resultset, class. Forname (xnode. Getresulttype());
private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { 
	List<T> list = new ArrayList<>();
	try { 
		ResultSetMetaData metaData = resultSet.getMetaData();
		int columnCount = metaData.getColumnCount();
		// Value of each traversal row
		while (resultSet.next()) { 
			T obj = (T) clazz.newInstance();
			for (int i = 1; i <= columnCount; i++) { 
				Object value = resultSet.getObject(i);
				String columnName = metaData.getColumnName(i);
				String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
				Method method;
				if (value instanceof Timestamp) { 
					method = clazz.getMethod(setMethod, Date.class);
				} else { 
					method = clazz.getMethod(setMethod, value.getClass());
				}
				method.invoke(obj, value);
			}
			list.add(obj);
		}
	} catch (Exception e) { 
		e.printStackTrace();
	}
	return list;
}
  • Our class objects are generated mainly through reflection. The type of this class is defined on the sql tag. The time type needs to be judged and processed. Timestamp is not the same type as java

7. Sql query supplementary description

sql queries have input parameters, whether they need input parameters, one query, and a query set. They only need to be properly packaged. For example, the following query sets, input parameters are object types;

ApiLikeTest.test_queryUserList()

@Test
public void test_queryUserList() { 
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try { 
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        
		try { 
            User req = new User();
            req.setAge(18);
            List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
            System.out.println(JSON.toJSONString(userList));
        } finally { 
            session.close();
            reader.close();
        }
    } catch (Exception e) { 
        e.printStackTrace();
    }
	
}

**Test results:

[{ "age":18,"createTime":1576944000000,"id":1,"name":"Water water","updateTime":1576944000000},{ "age":18,"createTime":1576944000000,"id":2,"name":"peas","updateTime":1576944000000}]

Process finished with exit code 0

4, Source code analysis (mybatis)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

The whole source code of Mybatis is still large. The following mainly arranges and analyzes some core contents to facilitate the subsequent analysis of the source code part of the integration of Mybatis and Spring. Brief description includes:; Container initialization, configuration file parsing, Mapper loading and dynamic proxy.

1. Start with a simple case

To learn the source code of Mybatis, the best way must be to enter from a simple point, rather than analyze it from the integration of Spring. SqlSessionFactory is the core instance object of the whole Mybatis, and the instance of SqlSessionFactory object is obtained through SqlSessionFactoryBuilder object. The SqlSessionFactoryBuilder object can load configuration information from an XML configuration file and then create a SqlSessionFactory. Examples are as follows:

MybatisApiTest.java

public class MybatisApiTest { 

    @Test
    public void test_queryUserInfoById() { 
        String resource = "spring/mybatis-config-datasource.xml";
        Reader reader;
        try { 
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

            SqlSession session = sqlMapper.openSession();
            try { 
                User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
                System.out.println(JSON.toJSONString(user));
            } finally { 
                session.close();
                reader.close();
            }
        } catch (IOException e) { 
            e.printStackTrace();
        }
    }

}

dao/IUserDao.java

public interface IUserDao { 

     User queryUserInfoById(Long id);

}

spring/mybatis-config-datasource.xml

<?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">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>

</configuration>

If all goes well, there will be the following results:

{ "age":18,"createTime":1571376957000,"id":1,"name":"tearful","updateTime":1571376957000}

From the code block above, you can see the core code; SqlSessionFactoryBuilder().build(reader) is responsible for loading, parsing and building the Mybatis configuration file until it can be executed and returned through SqlSession.

2. Container initialization

As can be seen from the above code, SqlSessionFactory is created through the SqlSessionFactoryBuilder factory class instead of using the constructor directly. The loading and initialization process of container configuration file is as follows:

  • Process core class
    • SqlSessionFactoryBuilder
    • XMLConfigBuilder
    • XPathParser
    • Configuration

SqlSessionFactoryBuilder.java

public class SqlSessionFactoryBuilder { 

  public SqlSessionFactory build(Reader reader) { 
    return build(reader, null, null);
  }

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

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

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) { 
    try { 
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) { 
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally { 
      ErrorContext.instance().reset();
      try { 
        reader.close();
      } catch (IOException e) { 
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(InputStream inputStream) { 
    return build(inputStream, null, null);
  }

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

  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.
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) { 
    return new DefaultSqlSessionFactory(config);
  }

}

As can be seen from the above source code, SqlSessionFactory provides three ways to build objects;

  • Byte stream: Java io. InputStream
  • Character stream: Java io. Reader
  • Configuration class: org apache. ibatis. session. Configuration

Then, the byte stream and character stream will create a configuration file parsing class: XMLConfigBuilder, and pass it through parser Parse () generates Configuration, and finally calls the configuration class construction method to generate SqlSessionFactory.

XMLConfigBuilder.java

public class XMLConfigBuilder extends BaseBuilder { 

  private boolean parsed;
  private final XPathParser parser;
  private String environment;
  private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

  ...
  public XMLConfigBuilder(Reader reader, String environment, Properties props) { 
    this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
  }
  ...
}  
  1. XMLConfigBuilder delegates the loading and parsing of XML files to XPathParser, and finally uses javax.xml that comes with JDK XML for XML parsing (XPath)
  2. XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)
    1. reader: use the character stream to create a new input source for reading XML files
    2. validation: whether to perform DTD verification
    3. variables: property configuration information
    4. entityResolver: Mybatis hardcoded new xmlmapterentityresolver() to provide the default XML parser

XMLMapperEntityResolver.java

public class XMLMapperEntityResolver implements EntityResolver { 

  private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
  private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
  private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";

  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

  /* * Converts a public DTD into a local one * * @param publicId The public id that is what comes after "PUBLIC" * @param systemId The system id that is what comes after the public id. * @return The InputSource for the DTD * * @throws org.xml.sax.SAXException If anything goes wrong */
  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException { 
    try { 
      if (systemId != null) { 
        String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) { 
          return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) { 
          return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
        }
      }
      return null;
    } catch (Exception e) { 
      throw new SAXException(e.toString());
    }
  }

  private InputSource getInputSource(String path, String publicId, String systemId) { 
    InputSource source = null;
    if (path != null) { 
      try { 
        InputStream in = Resources.getResourceAsStream(path);
        source = new InputSource(in);
        source.setPublicId(publicId);
        source.setSystemId(systemId);        
      } catch (IOException e) { 
        // ignore, null is ok
      }
    }
    return source;
  }

}
  1. Mybatis relies on the dtd file for parsing, in which ibatis-3-config DTDs are primarily used for compatibility purposes
  2. The call to getInputSource(String path, String publicId, String systemId) contains two parameters publicId (public identifier) and systemId (system identifier)

XPathParser.java

public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { 
  commonConstructor(validation, variables, entityResolver);
  this.document = createDocument(new InputSource(reader));
}

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { 
  this.validation = validation;
  this.entityResolver = entityResolver;
  this.variables = variables;
  XPathFactory factory = XPathFactory.newInstance();
  this.xpath = factory.newXPath();
}

private Document createDocument(InputSource inputSource) { 
  // important: this must only be called AFTER common constructor
  try { 
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setValidating(validation);
    factory.setNamespaceAware(false);
    factory.setIgnoringComments(true);
    factory.setIgnoringElementContentWhitespace(false);
    factory.setCoalescing(false);
    factory.setExpandEntityReferences(true);
    DocumentBuilder builder = factory.newDocumentBuilder();
    builder.setEntityResolver(entityResolver);
    builder.setErrorHandler(new ErrorHandler() { 
      @Override
      public void error(SAXParseException exception) throws SAXException { 
        throw exception;
      }
      @Override
      public void fatalError(SAXParseException exception) throws SAXException { 
        throw exception;
      }
      @Override
      public void warning(SAXParseException exception) throws SAXException { 
      }
    });
    return builder.parse(inputSource);
  } catch (Exception e) { 
    throw new BuilderException("Error creating document instance. Cause: " + e, e);
  }
  
}    
  1. From top to bottom, you can see that it is mainly to create a Mybatis Document parser. Finally, according to builder Parse (inputsource) returns Document
  2. After getting the xpathparser instance, next call the method: this (New xpathparser (reader, true, props, new xmlmapterentityresolver()), environment, props); XMLConfigBuilder. this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
  3. Among them, the constructor of the parent class is called public abstract class BaseBuilder {protected final Configuration configuration; protected final TypeAliasRegistry typeAliasRegistry; { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } }
  4. After XMLConfigBuilder is created, sqlSessionFactoryBuild calls parser Parse() creates configurationpublic class XMLConfigBuilder extensions basebuilder {public configuration parse() {if (parsed) {throw new builderexception ("each XMLConfigBuilder can only be used once");} parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } }

3. Configuration file analysis

This part is the core content of the whole XML file parsing and loading, including;

  1. Property resolution propertiesElement
  2. Load settings node settingsAsProperties
  3. Load custom VFS loadCustomVfs
  4. Resolve type alias typeAliasesElement
  5. Loading plugin pluginElement
  6. Load object factory objectFactoryElement
  7. Create an object wrapper factory objectWrapperFactoryElement
  8. Load reflection factory reflectorFactoryElement
  9. Element setting settingsElement
  10. Load environment configuration environmentsElement
  11. Database vendor ID load databaseIdProviderElement
  12. Load type processor typeHandlerElement
  13. (core) load mapper file mapperElement
parseConfiguration(parser.evalNode("/configuration"));

private void parseConfiguration(XNode root) { 
    try { 
      //issue #117 read properties first
      //Property resolution propertiesElement
      propertiesElement(root.evalNode("properties"));
      //Load settings node settingsAsProperties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //Load custom VFS loadCustomVfs
      loadCustomVfs(settings);
      //Resolve type alias typeAliasesElement
      typeAliasesElement(root.evalNode("typeAliases"));
      //Loading plugin pluginElement
      pluginElement(root.evalNode("plugins"));
      //Load object factory objectFactoryElement
      objectFactoryElement(root.evalNode("objectFactory"));
      //Create an object wrapper factory objectWrapperFactoryElement
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //Load reflection factory reflectorFactoryElement
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //Element settings
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //Load environment configuration environmentsElement
      environmentsElement(root.evalNode("environments"));
      //Database vendor ID load databaseIdProviderElement
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //Load type processor typeHandlerElement
      typeHandlerElement(root.evalNode("typeHandlers"));
      //Load mapper file mapperElement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) { 
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
} 

All root The bottom layer of evalnode() is to call XML DOM methods: Object evaluate(String expression, Object item, QName returnType), expression parameter expression, and return the final node content through xobject resultobject = Eval (expression, item), which can be used for reference
http://mybatis.org/dtd/mybatis-3-config.dtd , as follows;

<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
 
<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider type CDATA #REQUIRED >
 
<!ELEMENT properties (property*)>
<!ATTLIST properties resource CDATA #IMPLIED url CDATA #IMPLIED >
 
<!ELEMENT property EMPTY>
<!ATTLIST property name CDATA #REQUIRED value CDATA #REQUIRED >
 
<!ELEMENT settings (setting+)>
 
<!ELEMENT setting EMPTY>
<!ATTLIST setting name CDATA #REQUIRED value CDATA #REQUIRED >
 
<!ELEMENT typeAliases (typeAlias*,package*)>
 
<!ELEMENT typeAlias EMPTY>
<!ATTLIST typeAlias type CDATA #REQUIRED alias CDATA #IMPLIED >
 
<!ELEMENT typeHandlers (typeHandler*,package*)>
 
<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler javaType CDATA #IMPLIED jdbcType CDATA #IMPLIED handler CDATA #REQUIRED >
 
<!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory type CDATA #REQUIRED >
 
<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory type CDATA #REQUIRED >
 
<!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory type CDATA #REQUIRED >
 
<!ELEMENT plugins (plugin+)>
 
<!ELEMENT plugin (property*)>
<!ATTLIST plugin interceptor CDATA #REQUIRED >
 
<!ELEMENT environments (environment+)>
<!ATTLIST environments default CDATA #REQUIRED >
 
<!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment id CDATA #REQUIRED >
 
<!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager type CDATA #REQUIRED >
 
<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource type CDATA #REQUIRED >
 
<!ELEMENT mappers (mapper*,package*)>
 
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper resource CDATA #IMPLIED url CDATA #IMPLIED class CDATA #IMPLIED >
 
<!ELEMENT package EMPTY>
<!ATTLIST package name CDATA #REQUIRED >

mybatis-3-config. There are 11 configuration files in the DTD definition file, as follows:;

  1. properties?,
  2. settings?,
  3. typeAliases?,
  4. typeHandlers?,
  5. objectFactory?,
  6. objectWrapperFactory?,
  7. reflectorFactory?,
  8. plugins?,
  9. environments?,
  10. databaseIdProvider?,
  11. mappers?

Each of the above configurations is optional. The final configuration content is saved to
org.apache.ibatis.session.Configuration, as follows;

public class Configuration { 

  protected Environment environment;
  // Allow paging (rowboundaries) in nested statements. Set to false if allowed. The default is false
  protected boolean safeRowBoundsEnabled;
  // Allow paging (ResultHandler) in nested statements. Set to false if allowed.
  protected boolean safeResultHandlerEnabled = true;
  // Whether to enable the automatic hump naming rule (camel case) mapping, that is, a similar mapping from the classic database column name A_COLUMN to the classic Java attribute name aColumn. The default is false
  protected boolean mapUnderscoreToCamelCase;
  // When on, any method call will load all the properties of the object. Otherwise, each property is loaded on demand. The default value is false (true in ≤ 3.4.1)
  protected boolean aggressiveLazyLoading;
  // Whether a single statement is allowed to return multiple result sets (compatible drivers are required).
  protected boolean multipleResultSetsEnabled = true;
  // JDBC is allowed to support automatic generation of primary keys. Driver compatibility is required. This is the switch to obtain mysql auto incremented primary key / oracle sequence during insert. Note: Generally speaking, this is the desired result, and the default value should be true.
  protected boolean useGeneratedKeys;
  // Using column labels instead of column names is generally the desired result
  protected boolean useColumnLabel = true;
  // Whether to enable caching {it is enabled by default. Maybe this is also your interview question}
  protected boolean cacheEnabled = true;
  // Specifies whether to call the setter (put) method of the mapping object when the value in the result set is null, which is useful for Map.keySet() dependency or null value initialization.
  protected boolean callSettersOnNulls;
  // It is allowed to use the name in the method signature as the statement parameter name. In order to use this feature, your project must be compiled in Java 8 with the - parameters option. (starting from 3.4.1)
  protected boolean useActualParamName = true;
  //When all columns of the returned row are empty, MyBatis returns null by default. When this setting is enabled, MyBatis will return an empty instance. Note that it also applies to nested result sets (i.e. collectioin and association). (starting from 3.4.2) Note: it is more appropriate to split it into two parameters, one for the result set and the other for a single record. Generally speaking, we hope that the result set is not null and the single record is still null
  protected boolean returnInstanceForEmptyRow;
  // Specifies the prefix that MyBatis adds to the log name.
  protected String logPrefix;
  // Specify the specific implementation of the log used by MyBatis. If it is not specified, it will be found automatically. It is generally recommended to specify slf4j or log4j
  protected Class <? extends Log> logImpl;
   // Specify the implementation of VFS, which is a simple interface provided by mybatis for accessing resources in AS
  protected Class <? extends VFS> vfsImpl;
  // MyBatis uses the Local Cache mechanism to prevent circular references and accelerate repeated nested queries. The default value is SESSION. In this case, all queries executed in a SESSION will be cached. If the value is set to state, the local SESSION is only used for STATEMENT execution, and different calls to the same SqlSession will not share data.
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  // Specify a JDBC type for a NULL value when no specific JDBC type is provided for the parameter. Some drivers need to specify the JDBC type of the column. In most cases, they can directly use the general type, such as NULL, VARCHAR or OTHER.
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  // Specifies which method of the object triggers a deferred load.
  protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] {  "equals", "clone", "hashCode", "toString" }));
  // Set the timeout, which determines the number of seconds the driver waits for a response from the database. Default no timeout
  protected Integer defaultStatementTimeout;
  // Sets the default fetch quantity for the driven result set.
  protected Integer defaultFetchSize;
  // SIMPLE is an ordinary actuator; The REUSE executor reuses prepared statements; the BATCH executor reuses statements and performs BATCH updates.
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  // Specify how MyBatis should automatically map columns to fields or properties. NONE means to cancel automatic mapping; PARTIAL automatically maps only result sets that do not have nested result set mappings defined. FULL automatically maps any complex result set, nested or not.
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  // Specifies the behavior of discovering unknown columns (or unknown attribute types) for automatic mapping. This value should be set to WARNING
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  // properties property under settings
  protected Properties variables = new Properties();
  // The default reflector factory is used to operate properties and constructors
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  // Object factory. All resultMap classes need to be instantiated by relying on the object factory
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  // The object wrapper factory is mainly used to create non-native objects, such as proxy classes with some monitoring or special attributes
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
  // Global switch for delayed loading. When on, all associated objects are loaded late. In a specific association, the switch state of the item can be overridden by setting the fetchType property.
  protected boolean lazyLoadingEnabled = false;
  // Specifies the proxy tool used by mybatis to create objects with deferred loading capability. MyBatis 3.3 + uses JAVASSIST
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
  // MyBatis can execute different statements according to different database vendors. This multi vendor support is based on the databaseId attribute in the mapping statement.
  protected String databaseId;
  ...
}

As can be seen from the above, Mybatis has all the configurations; resultMap, Sql statement, plug-in and cache are maintained in Configuration. Here is another trick. There is also a StrictMap internal class in Configuration, which inherits from HashMap and improves the anti duplication during put and the exception handling of unable to get a value during get, as follows;

protected static class StrictMap<V> extends HashMap<String, V> { 

    private static final long serialVersionUID = -4950446264854982944L;
    private final String name;

    public StrictMap(String name, int initialCapacity, float loadFactor) { 
      super(initialCapacity, loadFactor);
      this.name = name;
    }

    public StrictMap(String name, int initialCapacity) { 
      super(initialCapacity);
      this.name = name;
    }

    public StrictMap(String name) { 
      super();
      this.name = name;
    }

    public StrictMap(String name, Map<String, ? extends V> m) { 
      super(m);
      this.name = name;
    }
}    

(core) load mapper file mapperElement

Mapper file processing is the core service of Mybatis framework. All SQL statements are written in mapper, which is also the focus of our analysis. Other modules can be explained later.

XMLConfigBuilder.parseConfiguration()->mapperElement(root.evalNode("mappers"));

private void mapperElement(XNode parent) throws Exception { 
   if (parent != null) { 
     for (XNode child : parent.getChildren()) { 
       // If you want to use package automatic scanning and explicitly specify the mapper to be loaded through mapper at the same time, you must ensure that the scope of package automatic scanning does not include the explicitly specified mapper. Otherwise, when you try to load the interface scanned through package, a duplicate judgment error occurs in the logic of loadXmlResource() of the corresponding xml file and is reported as org apache. ibatis. binding. Bindingexception exception. Even if the contents contained in the xml file and the statements contained in the mapper interface are not repeated, errors will occur, including the xml mapper automatically loaded when loading the mapper interface.
       if ("package".equals(child.getName())) { 
         String mapperPackage = child.getStringAttribute("name");
         configuration.addMappers(mapperPackage);
       } else { 
         String resource = child.getStringAttribute("resource");
         String url = child.getStringAttribute("url");
         String mapperClass = child.getStringAttribute("class");
         if (resource != null && url == null && mapperClass == null) { 
           ErrorContext.instance().resource(resource);
           InputStream inputStream = Resources.getResourceAsStream(resource);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
           mapperParser.parse();
         } else if (resource == null && url != null && mapperClass == null) { 
           ErrorContext.instance().resource(url);
           InputStream inputStream = Resources.getUrlAsStream(url);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
           mapperParser.parse();
         } else if (resource == null && url == null && mapperClass != null) { 
           Class<?> mapperInterface = Resources.classForName(mapperClass);
           configuration.addMapper(mapperInterface);
         } else { 
           throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
         }
       }
     }
   }
}
  • Mybatis provides two methods to configure mapper. The first is to use the automatic search mode of package, so that all interfaces under the specified package will be registered as mapper, which is also a common method in Spring, such as < mappers > < package name = "org. Itstack. Demo" / > < / mappers >
  • The other is to specify mapper explicitly, which can be subdivided by resource, URL or class, for example< mappers> <mapper resource="mapper/User_Mapper.xml"/> <mapper class=""/> <mapper url=""/> </mappers>

4. Mapper loading and dynamic proxy

Automatically search and load through package to generate the corresponding mapper agent class, code block and process, as follows;

private void mapperElement(XNode parent) throws Exception { 
  if (parent != null) { 
    for (XNode child : parent.getChildren()) { 
      if ("package".equals(child.getName())) { 
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else { 
        ...
      }
    }
  }
}

Mapper is loaded into the process of generating proxy objects. The main core classes include;

  1. XMLConfigBuilder
  2. Configuration
  3. MapperRegistry
  4. MapperAnnotationBuilder
  5. MapperProxyFactory

MapperRegistry.java

Parse and load Mapper

public void addMappers(String packageName, Class<?> superType) { 
  // The mybatis framework provides a search for the specified package under the classpath and the classes in the child packages that meet the conditions (annotation or inheritance from a class / interface). Thread is used by default currentThread(). The loader returned by getcontextclassloader () is the same as the spring tool class.
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();   
  // Load all classes unconditionally because the caller passed object Class as the parent class, which also leaves room for specifying the mapper interface in the future
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName); 
 // All matching calss are stored in resolverutil In the matches field
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {    
    //Call the addMapper method to parse the mapper class / interface
    addMapper(mapperClass);
  }
}

Generate proxy class: MapperProxyFactory

public <T> void addMapper(Class<T> type) {     
  // The mybatis mapper interface file must be interface, not class
  if (type.isInterface()) { 
    if (hasMapper(type)) { 
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {       
      // Create a MapperProxyFactory proxy for the mapper interface
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally { 
      if (!loadCompleted) { 
        knownMappers.remove(type);
      }
    }
  }
}

The mapping relationship between interface classes and proxy projects is maintained in MapperRegistry, known mappers;

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

MapperProxyFactory.java

public class MapperProxyFactory<T> { 
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  public MapperProxyFactory(Class<T> mapperInterface) { 
    this.mapperInterface = mapperInterface;
  }
  public Class<T> getMapperInterface() { 
    return mapperInterface;
  }
  public Map<Method, MapperMethod> getMethodCache() { 
    return methodCache;
  }
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) { 
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {  mapperInterface }, mapperProxy);
  }
  public T newInstance(SqlSession sqlSession) { 
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

The above is Mapper's proxy class project. mapperInterface in the constructor is the corresponding interface class. When instantiated, a specific MapperProxy proxy will be obtained, which mainly contains SqlSession.

5, Source code analysis (mybatis spring)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>

As an easy-to-use ORM framework, it must be Lori's face (simple), Royal sister's heart (powerful), paved bed (shielding direct contact with JDBC) and warm room (good speed performance)! In view of these advantages, Mybatis is used in almost most domestic Internet development frameworks, especially in some scenarios that require high performance. If sql needs to be optimized, it must be handwritten in xml. So, are you ready! Start analyzing its source code;

1. Start with a simple case

Like analyzing the source code of mybatis, first make a simple case; Define dao, write configuration file and junit unit test;

SpringApiTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-config.xml")
public class SpringApiTest { 

    private Logger logger = LoggerFactory.getLogger(SpringApiTest.class);

    @Resource
    private ISchoolDao schoolDao;
    @Resource
    private IUserDao userDao;

    @Test
    public void test_queryRuleTreeByTreeId(){ 
        School ruleTree = schoolDao.querySchoolInfoById(1L);
        logger.info(JSON.toJSONString(ruleTree));

        User user = userDao.queryUserInfoById(1L);
        logger.info(JSON.toJSONString(user));
    }

}

spring-config-datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1.Database connection pool: DriverManagerDataSource You can also use DBCP2-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${db.jdbc.driverClassName}"/>
        <property name="url" value="${db.jdbc.url}"/>
        <property name="username" value="${db.jdbc.username}"/>
        <property name="password" value="${db.jdbc.password}"/>
    </bean>

    <!-- 2.to configure SqlSessionFactory object -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- Inject database connection pool -->
        <property name="dataSource" ref="dataSource"/>
        <!-- to configure MyBaties Global profile:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- scanning entity Package use alias -->
        <property name="typeAliasesPackage" value="org.itstack.demo.po"/>
        <!-- scanning sql configuration file:mapper Needed xml file -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 3.Configure scan Dao Interface package, dynamic implementation Dao Interface, injection into spring In container -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- injection sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!-- Give the information that needs to be scanned Dao Interface package, separated by multiple commas -->
        <property name="basePackage" value="org.itstack.demo.dao"/>
    </bean>
              
</beans>

If all goes well, there will be the following results:

{ "address":"No. 5, Yiheyuan Road, Haidian District, Beijing","createTime":1571376957000,"id":1,"name":"Peking University","updateTime":1571376957000}
{ "age":18,"createTime":1571376957000,"id":1,"name":"tearful","updateTime":1571376957000}

From the above unit test code, we can see that two annotations without method body magically execute the configuration statements in our xml and output the results. In fact, it mainly benefits from the following two categories;

  • org.mybatis.spring.SqlSessionFactoryBean
  • org.mybatis.spring.mapper.MapperScannerConfigurer

2. Scan assembly registration (mappercannerconfigurer)

Mappercannerconfigurer generates dynamic proxy class registration for the whole Dao interface layer, and starts to play a core role. This class implements the following interfaces to process the scanned Mapper:

  • BeanDefinitionRegistryPostProcessor
  • InitializingBean
  • ApplicationContextAware
  • BeanNameAware

The overall class diagram is as follows;

The implementation process is as follows:;

In fact, the above class diagram + flow chart has clearly described the mappercannerconfigurer initialization process, but it is still too difficult for me to see for the first time. OK, continue!


MapperScannerConfigurer. Java & partial interception

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { 
  if (this.processPropertyPlaceHolders) { 
    processPropertyPlaceHolders();
  }
  ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  scanner.setAddToConfig(this.addToConfig);
  scanner.setAnnotationClass(this.annotationClass);
  scanner.setMarkerInterface(this.markerInterface);
  scanner.setSqlSessionFactory(this.sqlSessionFactory);
  scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
  scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
  scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
  scanner.setResourceLoader(this.applicationContext);
  scanner.setBeanNameGenerator(this.nameGenerator);
  scanner.registerFilters();
  scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
  • Implemented beandefinitionregistrypostprocessor Postprocessbeandefinitionregistry is used to register beans into the Spring container
  • Line 306: new ClassPathMapperScanner(registry); Hard coded classpath scanner for parsing Mapper files of Mybatis
  • Line 317: scanner Scan to scan Mapper. This includes a call to the inheritance class implementation relationship, which is the test question at the beginning of this article.


ClassPathMapperScanner. Java & partial interception

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) { 
  Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
  if (beanDefinitions.isEmpty()) { 
    logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
  } else { 
    processBeanDefinitions(beanDefinitions);
  }
  return beanDefinitions;
}
  • Call super. Of the parent class first doScan(basePackages); Register Bean information


ClassPathBeanDefinitionScanner. Java & partial interception

protected Set<BeanDefinitionHolder> doScan(String... basePackages) { 
	Assert.notEmpty(basePackages, "At least one base package must be specified");
	Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
	for (String basePackage : basePackages) { 
		Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
		for (BeanDefinition candidate : candidates) { 
			ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
			candidate.setScope(scopeMetadata.getScopeName());
			String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
			if (candidate instanceof AbstractBeanDefinition) { 
				postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
			}
			if (candidate instanceof AnnotatedBeanDefinition) { 
				AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate)
			}
			if (checkCandidate(beanName, candidate)) { 
				BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
				definitionHolder =
						AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.regi
				beanDefinitions.add(definitionHolder);
				registerBeanDefinition(definitionHolder, this.registry);
			}
		}
	}
	return beanDefinitions;
}
  • The doScan method of the parent class is preferentially called for Mapper scanning, Bean definition and registration to the DefaultListableBeanFactory Defaultlistablebeanfactory is the ancestor of IOC container in Spring. All classes that need to be instantiated need to be registered and initialized
  • Line 272: find candidate components (base package). Scan the package path. There is another way for annotation classes, which is similar
  • Line 288: registerBeanDefinition(definitionHolder, this.registry); The process of registering Bean information will eventually call: org springframework. beans. factory. support. DefaultListableBeanFactory


ClassPathMapperScanner. Java & partial interception

**processBeanDefinitions(beanDefinitions);**

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { 
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) { 
    definition = (GenericBeanDefinition) holder.getBeanDefinition();
    if (logger.isDebugEnabled()) { 
      logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
        + "' and '" + definition.getBeanClassName() + "' mapperInterface");
    }
    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
    definition.setBeanClass(this.mapperFactoryBean.getClass());
    definition.getPropertyValues().add("addToConfig", this.addToConfig);
    boolean explicitFactoryUsed = false;
    if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { 
      definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionFactory != null) { 
      definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
      explicitFactoryUsed = true;
    }
    if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { 
      if (explicitFactoryUsed) { 
        logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionTemplate != null) { 
      if (explicitFactoryUsed) { 
        logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
      explicitFactoryUsed = true;
    }
    if (!explicitFactoryUsed) { 
      if (logger.isDebugEnabled()) { 
        logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
      }
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}
  • Line 163: super doScan(basePackages);, After calling the parent class method, execute the internal method: processBeanDefinitions(beanDefinitions)
  • Line 186: definition getConstructorArgumentValues(). addGenericArgumentValue(definition.getBeanClassName()); Set the BeanName parameter, that is, our ISchoolDao and IUserDao
  • Line 187: definition setBeanClass(this.MapperFactoryBean.getClass());, When setting BeanClass, the interface itself does not have a class, so set the MapperFactoryBean class here. Finally, all dao layer interface classes are this MapperFactoryBean

MapperFactoryBean. Java & partial interception

This class has both inheritance and interface implementation. It's best to understand the overall class diagram, as follows;

This class is very important. Finally, all sql information execution will obtain getObject() through this class, that is, the proxy class of SqlSession to obtain mapper: mapperproxyfactory - > mapperproxy

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { 

  private Class<T> mapperInterface;

  private boolean addToConfig = true;

  public MapperFactoryBean() { 
    //intentionally empty 
  }
  
  public MapperFactoryBean(Class<T> mapperInterface) { 
    this.mapperInterface = mapperInterface;
  }

  /** * When the SpringBean container initializes, it will call checkDaoConfig(), which is an abstract method in the inherited class * {@ inheritDoc} */
  @Override
  protected void checkDaoConfig() { 
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { 
      try { 
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) { 
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally { 
        ErrorContext.instance().reset();
      }
    }
  }

  /** * {@inheritDoc} */
  @Override
  public T getObject() throws Exception { 
    return getSqlSession().getMapper(this.mapperInterface);
  }

  ...
}
  • Line 72: checkDaoConfig(), which is an abstract method in the inherited class, will be called when the SpringBean container is initialized
  • Line 95:
  • getSqlSession(). get Mapper( this.mapperInterface);, Get mapper (proxy class) through the interface. The calling process is as follows:;
    • DefaultSqlSession.getMapper(Class type), get Mapper
    • Configuration. Getmapper (class type, sqlsession, sqlsession), which is obtained from the configuration
    • MapperRegistry. Getmapper (class type, sqlsession, sqlsession), which is obtained from the registry and generated by instantiation
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { 
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) { 
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try { 
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) { 
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}
  • mapperProxyFactory.newInstance(sqlSession);, Generate MapperProxy through reflection project
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) { 
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {  mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) { 
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

MapperProxy. Java & partial interception

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 (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);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) { 
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) { 
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  @UsesJava7
  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable { 
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) { 
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  ...
}
  • Line 58: final MapperMethod mappermethod = cachedmapppermethod (method);, Get MapperMethod from cache
  • Line 59: mappermethod execute(sqlSession, args);, Execute the SQL statement and return the results (about the query, the obtained results will go to the bone (dry) layer); INSERT,UPDATE,DELETE,SELECT
public Object execute(SqlSession sqlSession, Object[] args) { 
  Object result;
  switch (command.getType()) { 
    case INSERT: { 
    Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: { 
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: { 
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    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);
        result = sqlSession.selectOne(command.getName(), param);
      }
      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;
}

The above analysis of mappercannerconfigurer layer is completed. The information injected from scan definition to Bean preparation for Spring container, proxy, reflection and SQL execution basically include all core contents. Next, analyze SqlSessionFactoryBean

3. SqlSession container factory initialization (SqlSessionFactoryBean)

During the initialization of SqlSessionFactoryBean, some of its own contents need to be processed, so the following interfaces also need to be implemented;

  • FactoryBean
  • InitializingBean -> void afterPropertiesSet() throws Exception
  • ApplicationListener

In fact, the above process has clearly described the whole core process, but there will be obstacles for novices on the road, so! OK, go on!


SqlSessionFactoryBean. Java & partial interception

public void afterPropertiesSet() throws Exception { 
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");
  this.sqlSessionFactory = buildSqlSessionFactory();
}
  • afterPropertiesSet(), the InitializingBean interface provides a way for beans to initialize methods. It only includes the afterPropertiesSet method. All classes that inherit this interface will execute this method when initializing beans.
  • Line 380: buildSqlSessionFactory(); Internal method construction, core functions continue to look down.


SqlSessionFactoryBean. Java & partial interception

protected SqlSessionFactory buildSqlSessionFactory() throws IOException { 
  Configuration configuration;
  XMLConfigBuilder xmlConfigBuilder = null;
  
  ...

  if (!isEmpty(this.mapperLocations)) { 
    for (Resource mapperLocation : this.mapperLocations) { 
      if (mapperLocation == null) { 
        continue;
      }
      try { 
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            configuration, mapperLocation.toString(), configuration.getSqlFragments());
        xmlMapperBuilder.parse();
      } catch (Exception e) { 
        throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
      } finally { 
        ErrorContext.instance().reset();
      }
      if (LOGGER.isDebugEnabled()) { 
        LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
      }
    }
  } else { 
    if (LOGGER.isDebugEnabled()) { 
      LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
    }
  }
  return this.sqlSessionFactoryBuilder.build(configuration);
}

  • Line 513: for (resource mapper location: this. Mapper locations) loop parses mapper content
  • Line 519: XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(...) parse XMLMapperBuilder
  • Line 521: xmlmapperbuilder Parse() performs parsing, as follows:;

XMLMapperBuilder. Java & partial interception

public class XMLMapperBuilder extends BaseBuilder { 
   private final XPathParser parser;
   private final MapperBuilderAssistant builderAssistant;
   private final Map<String, XNode> sqlFragments;
   private final String resource;

   private void bindMapperForNamespace() { 
     String namespace = builderAssistant.getCurrentNamespace();
     if (namespace != null) { 
       Class<?> boundType = null;
       try { 
         boundType = Resources.classForName(namespace);
       } catch (ClassNotFoundException e) { 
         //ignore, bound type is not required
       }
       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);
           configuration.addMapper(boundType);
         }
       }
     }
   }
}
  • Line 413 here is very important, configuration addMapper(boundType);, Really add Mapper to the configuration center

MapperRegistry. Java & partial interception

public class MapperRegistry { 

  public <T> void addMapper(Class<T> type) { 
    if (type.isInterface()) { 
      if (hasMapper(type)) { 
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try { 
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally { 
        if (!loadCompleted) { 
          knownMappers.remove(type);
        }
      }
    }
  }
  
}
  • Line 67: create the proxy project knownmappers put(type, new MapperProxyFactory(type));

Up to now, mappercannerconfigurer and SqlSessionFactoryBean have integrated the work done by the two classes;

  • The first one is used to scan the Dao interface, set the proxy class and register it in the IOC for subsequent generation of Bean entity class, MapperFactoryBean, and obtain Mapper from Configuration through mapperInterface
  • The other is used to generate SqlSession factory initialization, parse the XML Configuration in Mapper, conduct dynamic proxy mapperproxyfactory - > mapperproxy, and inject it into Mapper of Configuration
  • Finally, with the help of annotation classes, method injection can be carried out to obtain dynamic proxy objects during operation, so as to execute the corresponding CRUD operation @ Resource private ISchoolDao schoolDao; schoolDao.querySchoolInfoById(1L);

6, To sum up

  • The analysis process is long and lengthy. You may not be able to understand the whole process in one day, but you can still get a lot of gains when you are patient and study a little. In the future, such abnormalities can be easily solved, which is also helpful for interview and recruitment!
  • The reason for analyzing Mybatis is to add custom annotations to Dao at first, and it is found that the section cannot be intercepted. Thinking that this is a class that is dynamically proxied, it is often raked down until mapperproxy invoke! Of course, Mybatis provides custom plug-in development.
  • The above source code analysis only analyzes some core contents. If you want to know all of them, you can refer to resources; MyBatis 3 source code in-depth analysis, and debug the code. IDEA is still very convenient to see the source code, including class diagram, call sequence, etc.
  • In mybatis and mybatis spring, the most important thing is to assemble Mapper configuration file parsing and interface classes into proxy classes for mapping, so as to facilitate the CRUD operation of the database. After analyzing the source code, you can get more programming experience (routines).

Topics: Database Mybatis