8
头图

Author: Xiao Fu Ge
Blog: https://bugstack.cn

Precipitate, share, and grow, so that you and others can gain something! 😄

I. Introduction

Mybatis is also the embodiment of its easiest to use, why is this said?

Because when we use Mybatis, we only need to define an interface that does not need to write an implementation class, and we can perform CRUD operations on the database through annotations or configuration of SQL statements.

So how is this done? One of the most important points is that in Spring, your proxy object can be handed over to the Spring container. This proxy object can be used as a specific implementation class of the DAO interface, and this proxy implementation The class can complete an operation on the database, that is, this encapsulation process is called the ORM framework.

Speaking of the basic process, let's do some tests, so that everyone can get started! learn knowledge, you must get started before you can get it! You can use the following source code repository to practice

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


Two, put the Bean into the Spring container, in a few steps

Bean注册

  • Regarding the technical scenarios of Bean registration, MyBatis is the most common technical framework we use every day. By just defining an interface when using MyBatis, there is no need to write an implementation class, but this interface can be associated with the configured SQL statement, and the corresponding result can be returned when the corresponding database operation is executed. Then the proxy and registration of the bean used in the operation of this interface and the database.
  • We all know that the call of a class cannot directly call an interface that is not implemented, so we need to generate the corresponding implementation class for the interface by proxy. Next, by putting the proxy class in the implementation of Spring's FactoryBean, and finally registering the FactoryBean implementation class in the Spring container. So now your proxy class has been registered to the Spring container, and then you can inject it into the attribute by way of annotations.

According to this implementation, let's take a look at how the registration process of a Bean is implemented in the code.

1. Define the interface

public interface IUserDao {

    String queryUserInfo();

}
  • First define an interface similar to DAO. Basically such an interface 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) -> "你被代理了 " + method.getName();
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler); 

String res = userDao.queryUserInfo();
logger.info("测试结果:{}", res);
  • Java's own proxy method is relatively simple to use, and its usage is also very fixed.
  • InvocationHandler is an interface class, and its corresponding realization content is the concrete realization of the proxy object.
  • Finally, the proxy is handed over to Proxy to create a 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) -> "你被代理了 " + method.getName();

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

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

}
  • FactoryBean plays a role of second master in spring, it will have more than 70 younger brothers (implementing its interface definition), then it has three methods;

    • T getObject() throws Exception; returns the bean instance object
    • Class<?> getObjectType(); returns the instance class type
    • boolean isSingleton(); Determine whether it is a singleton, the singleton will be placed in the single-instance buffer pool in the Spring container
  • Here we put the object using the Java proxy above into the getObject() method, then the object obtained from Spring now 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 Bean management, all Beans will eventually be registered in the class DefaultListableBeanFactory. The main contents of the above code include:

  • Implement the 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, the main parameters needed here; define Bean and name setBeanClass(ProxyBeanFactory.class) .
  • Finally, register our own bean to the spring container, registry.registerBeanDefinition()

5. Test Verification

In the above, we have registered the bean of the custom proxy in the Spring container. Next, let's test how the bean of this proxy is called.

1. Define spring-config.xml

<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/>
  • Here we configure RegisterBeanFactory to spring's xml configuration for easy loading at startup.

2. Unit Testing

@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("测试结果:{}", 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 - 测试结果:你被代理了 queryUserInfo

Process finished with exit code 0
  • From the test results, we can see that we can already 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.

Three, write a Mybatis

Extend the previous source code analysis project; itstack-demo-mybatis, add the like package, imitate the 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

Regarding the entire Demo version, it is not to implement all Mybatis, but to show you the core content. From the use, you will feel exactly the same, but the implementation classes have all been replaced, and the core classes include;

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

1. First test the entire DemoJdbc framework

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":"水水","updateTime":1576944000000}

Process finished with exit code 0

At first glance, this test class may look exactly the same as the code tested by MybatisApiTest.java, and there is no difference. In fact, their imported packages are different;

Packages introduced in MybatisApiTest.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;
Packages introduced in ApiLikeTest.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;

it is good! Next we begin to analyze this part of the core code.

2. Load the XML configuration file

Here we use the configuration file structure of mybatis for analysis, without destroying the original structure, as close as possible to the source code. 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 the dom4j method. In the case, you will see the initial acquisition of resources, as follows;

ApiLikeTest.test_queryUserInfoById() & 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 configuration file address, so as to lay the foundation for the subsequent analysis. First of all, let's look at the Resources class, the whole is our resource class.

Resources.java & resource class
/**
 * 博 客 | https://bugstack.cn
 * Create by 小傅哥 @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 point of this code method is getResourceAsReader, until the following is done;

  1. Get the ClassLoader collection, search for configuration files as much as possible
  2. Read configuration resources through classLoader.getResourceAsStream and return immediately when found, otherwise an exception will be thrown

3. Parse the XML configuration file

After the configuration file is loaded, the parsing operation starts. Here we also imitate mybatis but simplify it as follows;

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder.build() & entry building class
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() & parsing 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 in the previous xml content, we need to parse out the database connection pool information datasource, as well as the database statement mapping relationship mappers
SqlSessionFactoryBuilder.dataSource() & parse out the data source
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 most original code of jdbc, 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();
            //命名空间
            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();
                // ? 匹配
                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. Currently, only select related is parsed for testing.
  • All sql statements are used in order to be unique; the id in namespace + select is spliced as a key, and then stored in the map together with sql.
  • In the sql statement configuration of mybaits, there are placeholders for passing parameters. where id = #{id} Therefore, 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 query parameters.

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;
}
Implementation class of DefaultSqlSessionFactory.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
  • Implement openSession() of SqlSessionFactory, used to create DefaultSqlSession, which can also perform 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);

By implementing SqlSession in DefaultSqlSession, it provides 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, 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 is objects.get(0);, selectList is all returned
  • Obtain the stored select tag information when the xml is initially parsed 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, hand it to the PreparedStatement class of jdbc for execution
  • Here also need to set the input parameters, we will 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();
          // 单个参数
          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<>();
          // 对象参数
          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 to set the value directly, Long, Integer, String...
  • If it is a class object, it needs to be set to match the parameter Map by obtaining the Field property
  • Execute the query after setting the parameters preparedStatement.executeQuery()
  • Next, we need to convert the query result to our class (mainly the operation of the 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();
            // 每次遍历行值
            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;
    }
  • Mainly generate our class object through reflection, the type of this class is defined on the sql tag
  • Time type needs to be judged and processed. Timestamp is not the same type as java

7. Sql query supplementary instructions

sql query has input parameters, some do not need to enter, there is a query, and there is a query collection. It only needs to be packaged reasonably. For example, in the query collection below, the 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 result: *

[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0

Fourth, source code analysis (mybatis)

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

The entire source code of Mybatis is still very large. The following mainly organizes and analyzes part of the core content to facilitate subsequent analysis of the source code part of the integration of Mybatis and Spring. Briefly include; container initialization, configuration file analysis, Mapper loading and dynamic proxy.

1. Start with a simple case

To learn the Mybatis source code, the best way is to enter from a simple point, rather than starting from the Spring integration analysis. SqlSessionFactory is the core instance object of the entire Mybatis, and the instance of the SqlSessionFactory object is obtained through the SqlSessionFactoryBuilder object. The SqlSessionFactoryBuilder object can load configuration information from an XML configuration file, and then create a SqlSessionFactory. The following example:

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, then there will be the following result:

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

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

2. Container initialization

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

微信公众号:bugstack虫洞栈 & 初始化流程

  • 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 you can see from the source code above, 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, byte stream and character stream will create configuration file parsing class: XMLConfigBuilder, and generate Configuration through parser.parse(), and finally call 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 entrusts the loading and parsing of XML files to XPathParser, and finally uses javax.xml that comes with JDK for XML parsing (XPath)
  2. XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)

    1. reader: Use character stream to create a new input source for reading XML files
    2. validation: Whether to perform DTD verification
    3. variables: attribute configuration information
    4. entityResolver: Mybatis hard-coded new XMLMapperEntityResolver() 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 dtd files for parsing, of which ibatis-3-config.dtd is mainly used for compatible purposes
  2. The call to getInputSource(String path, String publicId, String systemId) has 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 the main purpose is to create a Mybatis document parser, and finally return Document according to builder.parse(inputSource)
  2. After getting the XPathParser instance, the next method is called: this (new XPathParser(reader, true, props, new XMLMapperEntityResolver()), 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. Which calls the constructor of the parent class

    public abstract class BaseBuilder {
      protected final Configuration configuration;
      protected final TypeAliasRegistry typeAliasRegistry;
      protected final TypeHandlerRegistry typeHandlerRegistry;
    
      public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
      }
    }
  4. After XMLConfigBuilder is created, sqlSessionFactoryBuild calls parser.parse() to create Configuration

    public class XMLConfigBuilder extends 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 entire XML file parsing and loading, including;

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

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      //属性解析propertiesElement
      propertiesElement(root.evalNode("properties"));
      //加载settings节点settingsAsProperties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加载自定义VFS loadCustomVfs
      loadCustomVfs(settings);
      //解析类型别名typeAliasesElement
      typeAliasesElement(root.evalNode("typeAliases"));
      //加载插件pluginElement
      pluginElement(root.evalNode("plugins"));
      //加载对象工厂objectFactoryElement
      objectFactoryElement(root.evalNode("objectFactory"));
      //创建对象包装器工厂objectWrapperFactoryElement
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //加载反射工厂reflectorFactoryElement
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //元素设置
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //加载环境配置environmentsElement
      environmentsElement(root.evalNode("environments"));
      //数据库厂商标识加载databaseIdProviderElement
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //加载类型处理器typeHandlerElement
      typeHandlerElement(root.evalNode("typeHandlers"));
      //加载mapper文件mapperElement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
} 

All root.evalNode() bottom layer is to call XML DOM method: Object evaluate(String expression, Object item, QName returnType), expression parameter expression, return the final node content through XObject resultObject = eval( expression, item ), you can refer to 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
>

There are 11 configuration files in the mybatis-3-config.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 will be saved to org.apache.ibatis.session.Configuration, as follows;

public class Configuration {

  protected Environment environment;
  // 允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false。默认为false
  protected boolean safeRowBoundsEnabled;
  // 允许在嵌套语句中使用分页(ResultHandler)。如果允许使用则设置为false。
  protected boolean safeResultHandlerEnabled = true;
  // 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。默认false
  protected boolean mapUnderscoreToCamelCase;
  // 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载。默认值false (true in ≤3.4.1)
  protected boolean aggressiveLazyLoading;
  // 是否允许单一语句返回多结果集(需要兼容驱动)。
  protected boolean multipleResultSetsEnabled = true;
  // 允许 JDBC 支持自动生成主键,需要驱动兼容。这就是insert时获取mysql自增主键/oracle sequence的开关。注:一般来说,这是希望的结果,应该默认值为true比较合适。
  protected boolean useGeneratedKeys;
  // 使用列标签代替列名,一般来说,这是希望的结果
  protected boolean useColumnLabel = true;
  // 是否启用缓存 {默认是开启的,可能这也是你的面试题}
  protected boolean cacheEnabled = true;
  // 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。
  protected boolean callSettersOnNulls;
  // 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的工程必须采用Java 8编译,并且加上-parameters选项。(从3.4.1开始)
  protected boolean useActualParamName = true;
  //当返回行的所有列都是空时,MyBatis默认返回null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集 (i.e. collectioin and association)。(从3.4.2开始) 注:这里应该拆分为两个参数比较合适, 一个用于结果集,一个用于单记录。通常来说,我们会希望结果集不是null,单记录仍然是null
  protected boolean returnInstanceForEmptyRow;
  // 指定 MyBatis 增加到日志名称的前缀。
  protected String logPrefix;
  // 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。一般建议指定为slf4j或log4j
  protected Class <? extends Log> logImpl;
   // 指定VFS的实现, VFS是mybatis提供的用于访问AS内资源的一个简便接口
  protected Class <? extends VFS> vfsImpl;
  // MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  // 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  // 指定对象的哪个方法触发一次延迟加载。
  protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
  // 设置超时时间,它决定驱动等待数据库响应的秒数。默认不超时
  protected Integer defaultStatementTimeout;
  // 为驱动的结果集设置默认获取数量。
  protected Integer defaultFetchSize;
  // SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  // 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  // 指定发现自动映射目标未知列(或者未知属性类型)的行为。这个值应该设置为WARNING比较合适
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  // settings下的properties属性
  protected Properties variables = new Properties();
  // 默认的反射器工厂,用于操作属性、构造器方便
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  // 对象工厂, 所有的类resultMap类都需要依赖于对象工厂来实例化
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  // 对象包装器工厂,主要用来在创建非原生对象,比如增加了某些监控或者特殊属性的代理类
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
  // 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。
  protected boolean lazyLoadingEnabled = false;
  // 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。MyBatis 3.3+使用JAVASSIST
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
  // MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。
  protected String databaseId;
  ...
}

As you can see above, Mybatis maintains all configurations; resultMap, Sql statements, plug-ins, caches, etc., in the Configuration. There is also a little trick here. There is also a StrictMap internal class in Configuration, which inherits from HashMap to improve the exception handling of anti-weighting when putting and not getting a value when getting, 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 the Mybatis framework. All SQL statements are written in Mapper. This 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()) {
       // 如果要同时使用package自动扫描和通过mapper明确指定要加载的mapper,一定要确保package自动扫描的范围不包含明确指定的mapper,否则在通过package扫描的interface的时候,尝试加载对应xml文件的loadXmlResource()的逻辑中出现判重出错,报org.apache.ibatis.binding.BindingException异常,即使xml文件中包含的内容和mapper接口中包含的语句不重复也会出错,包括加载mapper接口时自动加载的xml mapper也一样会出错。
       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 types of methods to configure Mapper. The first type is to use package automatic search mode, so that all interfaces under the specified package will be registered as mapper, which is also a common way in Spring, for example:

    <mappers>
      <package name="org.itstack.demo"/>
    </mappers>
  • The other type 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

Search and load automatically through the package method, and generate the corresponding mapper proxy 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 {
        ...
      }
    }
  }
}

微信公众号:bugstack虫洞栈 & 动态代理过程

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) {
  // mybatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或者继承于某个类/接口)的类,默认使用Thread.currentThread().getContextClassLoader()返回的加载器,和spring的工具类殊途同归。
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();   
  // 无条件的加载所有的类,因为调用方传递了Object.class作为父类,这也给以后的指定mapper接口预留了余地
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName); 
 // 所有匹配的calss都被存储在ResolverUtil.matches字段中
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {   
    //调用addMapper方法进行具体的mapper类/接口解析
    addMapper(mapperClass);
  }
}

Generate proxy class: MapperProxyFactory

public <T> void addMapper(Class<T> type) {    
  // 对于mybatis mapper接口文件,必须是interface,不能是class
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is

小傅哥
4.7k 声望28.4k 粉丝

CodeGuide | 程序员编码指南 - 原创文章、案例源码、资料书籍、简历模版等下载。