3
头图

Author: Xiao Fu Ge Blog: https://bugstack.cn 《手写Mybatis系列》

I. Introduction

为什么,要读框架源码?

Because the business engineering code in hand is too stretched! Usually as business research and development, most of the code developed is a series of process-based processing, lacking the decoupling of functional logic, and has the characteristics of frequent iteration but poor iterability. Therefore, such code can usually only learn business logic, but it is difficult to absorb the successful experience of large-scale system design and functional logic implementation, which are often lessons of failure.

The core of the design and implementation of all systems is how to decouple. If the decoupling is not clear, the direct result is that when the function continues to iterate, the implementation of the entire system will become more and more bloated, and the stability will become worse and worse. The practice of decoupling has a very good design and implementation in the source code of various frameworks, so reading this part of the source code is to absorb the successful experience. Gradually applying the idea of decoupling to actual business development will allow us to write a better code structure.

2. Goals

In the previous chapter, we implemented a data source with/without connection pooling, and we can use our pooling technology to complete database operations when calling and executing SQL.

As for the invocation, execution and result encapsulation of the pooled data source, we are still only initiating it in DefaultSqlSession, as shown in Figure 7-1. So this way of writing the code process to death is definitely not suitable for our extended use, and it is also not conducive to the call of each newly defined method in SqlSession to the pooled data source.

图 7-1 DefaultSqlSession 调用数据源

  • Decouple the invocation, execution and result encapsulation of the data source in the DefaultSqlSession#selectOne method, and provide a new function module to replace this part of hard-coded logic processing.
  • Only by providing a separate execution method entry, we can better expand and deal with the changes in requirements in this part, including various input parameters, result encapsulation, executor types, batch processing, etc., to meet different styles of user needs , that is, the specific information configured into Mapper.xml.

3. Design

From our progressive development process of the ORM framework, the execution actions that can be separated include parsing configuration, proxy objects, mapping methods, etc., until we package and use the data source in the previous chapters, but we put the operation of the data source. It is hard bound to the execution method of DefaultSqlSession.

Then in order to decouple the processing of this piece, you need to propose a separate executor service function, and then pass the executor function to the executor function when the DefaultSqlSession is created, and then the specific method call can call the executor to process , so as to decouple this part of the functional modules. As shown in Figure 7-2.

图 7-2 引入执行器解耦设计

  • First of all, we need to extract the interface of the executor, define the execution method, transaction acquisition and the definition of the corresponding commit, rollback, and close. At the same time, because the executor is a standard execution process, it can be implemented by an abstract class. Process packaging of content for template mode. In the packaging process, define abstract classes, which are implemented by concrete subclasses. This part of the code will be reflected in the following code SimpleExecutor Simple executor implementation.
  • Next is the processing of SQL. We all know that when using JDBC to execute SQL, it is divided into simple processing and preprocessing. Preprocessing includes prepared statements, parameterized transmission, query execution, and final result encapsulation and return. Therefore, we also need to divide the steps of this part of JDBC into structured class processes to implement, which is convenient for function expansion. The specific code is mainly reflected in the interface implementation of the statement processor StatementHandler .

4. Realization

1. Engineering structure

 mybatis-step-06
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           ├── datasource
    │           ├── executor
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultHandler.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

Project source code : 公众号「bugstack虫洞栈」,回复:手写Mybatis,获取完整源码

SQL method executor core class relationship, as shown in Figure 7-3

图 7-3 SQL方法执行器核心类关系

  • The Executor interface is defined as the executor entry, and the unified standard interface for transaction and operation and SQL execution is determined. The abstract class is defined and implemented by the executor interface, that is, the abstract class is used to handle the unified and shared transaction and the standard process of executing SQL, that is, the abstract interface for executing SQL defined here is implemented by the subclass.
  • In the concrete simple SQL executor implementation class, handle the concrete operation procedure of the doQuery method. This process will introduce the creation of the SQL statement processor, and the creation process is still provided by the configuration configuration item. You will find a lot of such generation processing, all from configuration items
  • When the development of the executor is completed, the next step is to pass it to DefaultSqlSessionFactory to open the openSession along with the constructor parameters, so that the executor can be called for processing when DefaultSqlSession#selectOne is executed. This completes the decoupling operation.

2. Definition and Implementation of Actuator

The executor is divided into three parts: interface, abstract class, and simple executor implementation class. Usually, in the source code of the framework, there will be abstract classes for the processing of some standard processes. It is responsible for providing common functional logic, as well as defining and processing the execution process of interface methods, and extracting abstract interfaces for implementation by subclasses. This design pattern is also defined as the template pattern.

2.1 Executor

See the source code : cn.bugstack.mybatis.executor.Executor

 public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    <E> List<E> query(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql);

    Transaction getTransaction();

    void commit(boolean required) throws SQLException;

    void rollback(boolean required) throws SQLException;

    void close(boolean forceRollback);

}
  • The interface defined in the executor includes transaction-related processing methods and operations for executing SQL queries, and other methods will continue to be supplemented with subsequent function iterations.

2.2 BaseExecutor abstract base class

See the source code : cn.bugstack.mybatis.executor.BaseExecutor

 public abstract class BaseExecutor implements Executor {

    protected Configuration configuration;
    protected Transaction transaction;
    protected Executor wrapper;

    private boolean closed;

    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.wrapper = this;
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql) {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        return doQuery(ms, parameter, resultHandler, boundSql);
    }

    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql);

    @Override
    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new RuntimeException("Cannot commit, transaction is already closed");
        }
        if (required) {
            transaction.commit();
        }
    }

}
  • All the interfaces of the actuator are encapsulated in the abstract base class, so that after the concrete subclass inherits the abstract class, there is no need to deal with these common methods. At the same time, in the query query method, some necessary process processing is encapsulated. If the detection is turned off, etc., there are some cached operations in the Mybatis source code, which are temporarily removed here, focusing on the core process. Reader partners can compare and learn with the source code in the process of learning.

2.3 SimpleExecutor simple executor implementation

See the source code for details : cn.bugstack.mybatis.executor.SimpleExecutor

 public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    protected <E> List<E> doQuery(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql) {
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, resultHandler, boundSql);
            Connection connection = transaction.getConnection();
            Statement stmt = handler.prepare(connection);
            handler.parameterize(stmt);
            return handler.query(stmt, resultHandler);
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

}
  • The simple executor SimpleExecutor inherits the abstract base class and implements the abstract method doQuery. In this method, it wraps the acquisition of the data source, the creation of the statement processor, the instantiation of the Statement and the setting of related parameters. Finally, perform SQL processing and result return operations.
  • The implementation of the StatementHandler statement processor will be introduced next.

3. Statement processor

The statement processor is the dependent part of the SQL executor. The SQL executor encapsulates the transaction, connection and detection environment, etc., while the statement processor prepares the statement, passes parameterization, executes the SQL, and encapsulates the result.

3.1 StatementHandler

See the source code : cn.bugstack.mybatis.executor.statement.StatementHandler

 public interface StatementHandler {

    /** 准备语句 */
    Statement prepare(Connection connection) throws SQLException;

    /** 参数化 */
    void parameterize(Statement statement) throws SQLException;

    /** 执行查询 */
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

}
  • The core of the statement processor includes operations such as preparing statements, parameterized parameter transfer, and query execution. The corresponding Mybatis source code here also includes update, batch processing, and parameter acquisition processors.

3.2 BaseStatementHandler abstract base class

See the source code : cn.bugstack.mybatis.executor.statement.BaseStatementHandler

 public abstract class BaseStatementHandler implements StatementHandler {

    protected final Configuration configuration;
    protected final Executor executor;
    protected final MappedStatement mappedStatement;

    protected final Object parameterObject;
    protected final ResultSetHandler resultSetHandler;

    protected BoundSql boundSql;

    public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, ResultHandler resultHandler, BoundSql boundSql) {
        this.configuration = mappedStatement.getConfiguration();
        this.executor = executor;
        this.mappedStatement = mappedStatement;
        this.boundSql = boundSql;
                
                // 参数和结果集
        this.parameterObject = parameterObject;
        this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, boundSql);
    }

    @Override
    public Statement prepare(Connection connection) throws SQLException {
        Statement statement = null;
        try {
            // 实例化 Statement
            statement = instantiateStatement(connection);
            // 参数设置,可以被抽取,提供配置
            statement.setQueryTimeout(350);
            statement.setFetchSize(10000);
            return statement;
        } catch (Exception e) {
            throw new RuntimeException("Error preparing statement.  Cause: " + e, e);
        }
    }

    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

}
  • In the statement processor base class, the parameter information and result information are encapsulated. However, for the time being, we will not do too much parameter processing, including JDBC field type conversion. This part of the content will be iteratively developed after the construction of our entire actuator structure.
  • Next is the processing of the BaseStatementHandler#prepare method, including defining the instantiation abstract method, which is handed over to each concrete implementation subclass for processing. Includes; SimpleStatementHandler simple statement handler and PreparedStatementHandler prepared statement handler.

    • A simple statement processor is just the most basic execution of SQL, with no parameter settings.
    • The prepared statement processor is the most common operation method we use in JDBC. PreparedStatement sets SQL and passes the parameter setting process.

3.3 PreparedStatementHandler prepared statement processor

See the source code for details : cn.bugstack.mybatis.executor.statement.PreparedStatementHandler

 public class PreparedStatementHandler extends BaseStatementHandler{

    @Override
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        String sql = boundSql.getSql();
        return connection.prepareStatement(sql);
    }

    @Override
    public void parameterize(Statement statement) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.setLong(1, Long.parseLong(((Object[]) parameterObject)[0].toString()));
    }

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
    }

}
  • Included in the prepared statement processor is instantiateStatement to preprocess SQL, parameterize to set parameters, and the operation of query query execution.
  • It should be noted here that the parameterize setting parameters are still hard-coded processing, and this part will be improved later.
  • The query method executes the query and encapsulates the result. The encapsulation of the result is relatively simple at present. It just extracts the content of the object in the previous chapter and encapsulates it. This part has not changed for the time being. All will be processed later.

4. Actuator creation and use

After the development of the executor is completed, it needs to be connected to the DefaultSqlSession for use. In this connection process, the executor needs to be constructed and passed as a parameter when the DefaultSqlSession is created. Then this piece involves the processing of DefaultSqlSessionFactory#openSession.

4.1 Turn on the actuator

See the source code for details : cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory

 public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            TransactionFactory transactionFactory = environment.getTransactionFactory();
            tx = transactionFactory.newTransaction(configuration.getEnvironment().getDataSource(), TransactionIsolationLevel.READ_COMMITTED, false);
            // 创建执行器
            final Executor executor = configuration.newExecutor(tx);
            // 创建DefaultSqlSession
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            try {
                assert tx != null;
                tx.close();
            } catch (SQLException ignore) {
            }
            throw new RuntimeException("Error opening session.  Cause: " + e);
        }
    }

}
  • In openSession, the transaction is passed to the creation of the executor. For details on the creation of the executor, please refer to the configuration.newExecutor code. There is not much complicated logic in this part. Readers can refer to the source code for learning.
  • After the executor is created, the parameters are passed to DefaultSqlSession, which connects the whole process.

4.2 Using the Actuator

See the source code : cn.bugstack.mybatis.session.defaults.DefaultSqlSession

 public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getBoundSql());
        return list.get(0);
    }

}
  • Well, after all the implementations of the above executors are completed, the next step is the decoupling call. After the MappedStatement mapping statement class is obtained in DefaultSqlSession#selectOne, it is passed to the executor for processing. Now, after the decoupling of the design idea, this class becomes more tidy, that is, it is easy to maintain and extend.

5. Test

1. Prepare in advance

1.1 Create library table

Create a database named mybatis and create a table user and add test data in the library, as follows:

 CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '用户ID',
        userHead VARCHAR(16) COMMENT '用户头像',
        createTime TIMESTAMP NULL COMMENT '创建时间',
        updateTime TIMESTAMP NULL COMMENT '更新时间',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');

1.2 Configure the data source

 <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/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
  • Configure data source information through mybatis-config-datasource.xml , including: driver, url, username, password
  • Here dataSource can be configured as DRUID, UNPOOLED and POOLED for testing and verification.

1.3 Configure Mapper

 <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>
  • This part does not need to be adjusted for the time being. At present, it is only a parameter of the type of input parameter. After we complete this part of the content, we will provide more other parameters for verification.

2. Unit testing

 @Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
   
    // 2. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 测试验证
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试结果:{}", JSON.toJSONString(user));
}
  • There is no change in the unit test, but we still pass a 1L long type parameter to process the method call. The processing process of the executor is verified through unit testing, and readers can perform breakpoint tests during the learning process to learn the processing content of each process.

Test Results

 22:16:25.770 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
22:16:26.076 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 540642172.
22:16:26.198 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}

Process finished with exit code 0

  • From the test results, we can already replace the call in DefaultSqlSession#selectOne with the executor to complete the processing of the whole process. Decoupling this part of the logic operation can also facilitate our subsequent expansion.

6. Summary

  • The implementation of the entire chapter is dealing with decoupling, from DefaultSqlSession#selectOne to decoupling the data source processing to the executor for operations. The executor also includes the dismantling of JDBC processing, linking, preparing statements, encapsulating parameters, and processing results. All these processes can be very convenient in subsequent functional iterations after decoupling the classes and methods. Done with extension.
  • This chapter also reserves extension points for our subsequent processing of extended parameters and encapsulation of result sets, as well as the selection of different statement processors, which need to be improved and supplemented in the future. At present, what we have connected in series is the core skeleton structure, which will be iteratively improved with the subsequent incremental development.
  • For the learning of source code, readers have to go through the process of reading, writing, thinking, and applying several steps in order to better absorb the ideas in it. meaning.

Seven, series recommendation


小傅哥
4.7k 声望28.4k 粉丝

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