1
头图

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

Precipitate, share, grow, and let yourself and others gain something! 😄

I. Introduction

你只是在解释过程,而他是在阐述高度!

If it weren't for long-term precipitation, accumulation, and reserve, I would have no way to use more dimensions and more perspectives to elaborate on a problem in multiple aspects. Just like you and me; after crossing the cliffs and mountains, I realize that the access to the pillow and the teacher is smooth. After experiencing the thunder and falling, the rain is coming and the peaks are coming, and then I can smell the tranquility of the eight-faced flowing clouds and the clear night, and the tranquility of the spring city.

Therefore, when it comes to programming development, it simply means writing code and changing bugs. But if it is limited to just writing code, it is actually difficult to appreciate the many design ideas and complex problems, which are as hearty as a chef. And these hearty experiences all require your extended learning and in-depth exploration of technology, and absorb experience from many excellent source code frameworks. Repeatedly pondering and trying again and again, there will eventually be a point in time when you will have a feeling of enlightenment. The accumulation of these feelings can help you to express more in-depth technical ideas and analog design comparisons in the interview, debriefing, defense, sharing, reporting and other scenarios in the future, and look down on the business scene from a higher perspective. Towards and gives a long-term architectural plan.

2. Goals

Before the implementation of this chapter, most of the core structures of the Mybatis ORM framework have been gradually reflected, including; parsing, binding, mapping, transaction, execution, data source, etc. But with the gradual improvement of more functions, we need to refine the implementation in the module, not just complete the functional logic. This is a bit like splitting and decoupling CRUD using design principles to meet the ease of maintenance and scalability of the code. And here we first start to deal with the problem of XML parsing, refine the rough implementation before, to satisfy our integration and processing of some parameters during parsing.

图 9-1 ORM框架XML解析映射关系

  • The parsing of this part is what we did in the XMLConfigBuilder#mapperElement method earlier in this chapter. Although it seems to be able to achieve the function, it always makes people feel that it is not regular enough. Just like the logic of the CRUD we usually develop, any process can be handled, but any process will become more and more chaotic.
  • Just like when we call the method of performing database operations in the ORM framework DefaultSqlSession, when we need the PreparedStatementHandler#parameterize parameter, we do not accurately locate the parameter type, the conversion relationship between jdbcType and javaType, so the subsequent attribute filling will appear It's messy and not easy to extend. Of course, if you write it hard, you can write it, but this is not a good design!
  • So next, Brother Fu will take the reader to deal with this part of the analysis, use design principles to decouple the process and responsibilities, and combine our current demands to prioritize the processing of static SQL content. After the framework structure is gradually improved, some dynamic SQL and more parameter types will be processed, so that readers can gain some experience when they read the Mybatis source code and need to develop their own X-ORM framework.

3. Design

Referring to the design principles, for the reading of XML information, the process of each functional module should conform to a single responsibility, and each specific implementation must have the Law of Demeter, so that the realized functions can have good scalability. Usually, this kind of code will also look very clean . Based on such demands, we need to disassemble and serially call the different contents of the parsing process according to their respective responsibility classes. The overall design is shown in Figure 9-2

图 9-2 XML 配置构建器解析过程

  • Compared with the previous parsing code, instead of processing all parsing in one loop, XMLMapperBuilder and XMLStatementBuilder are introduced to process 映射构建器 and 语句构建器 respectively during the whole parsing process. , which are analyzed according to different responsibilities.
  • At the same time, in the statement builder, a script language driver is introduced, and the default implementation is the XML language driver XMLLanguageDriver, which is used to specifically operate the parsing of static and dynamic SQL statement nodes. There are many ways to implement this part of the parsing process, even if you use regular or String interception yourself. So in order to maintain the unity with Mybatis, we directly refer to the source code Ognl for processing. The corresponding class is DynamicContext
  • All the parsing here is realized by decoupling, in order to handle the setting of the setParameters parameter more conveniently in the executor. The setting of the following parameters will also involve the use of the meta-object reflection tool class we implemented earlier.

4. Realization

1. Engineering structure

 mybatis-step-08
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           ├── builder
    │           │   ├── xml
    │           │   │   ├── XMLConfigBuilder.java
    │           │   │   ├── XMLMapperBuilder.java
    │           │   │   └── XMLStatementBuilder.java
    │           │   ├── BaseBuilder.java
    │           │   ├── ParameterExpression.java
    │           │   ├── SqlSourceBuilder.java
    │           │   └── StaticSqlSource.java
    │           ├── datasource
    │           ├── executor
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           │   ├── BoundSql.java
    │           │   ├── Environment.java
    │           │   ├── MappedStatement.java
    │           │   ├── ParameterMapping.java
    │           │   ├── SqlCommandType.java
    │           │   └── SqlSource.java
    │           ├── parsing
    │           │   ├── GenericTokenParser.java
    │           │   └── TokenHandler.java
    │           ├── reflection
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultHandler.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    │               ├── JdbcType.java
    │               ├── TypeAliasRegistry.java
    │               ├── TypeHandler.java
    │               └── TypeHandlerRegistry.java
    └── 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 : https://github.com/fuzhengwei/small-mybatis

XML statement parsing builder, core logic class relationship, as shown in Figure 9-3

图 9-3 XML 语句解析构建器,核心逻辑类关系

  • Decoupling the parsing of XML in the original XMLConfigBuilder, extending the mapping builder and statement builder, processing SQL extraction and parameter packaging, the entire core flow graph is called in series with XMLConfigBuilder#mapperElement as the entry.
  • Parse in the XMLStatementBuilder#parseStatementNode method <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">...</select> configure the statement, extract the parameter type and the result type, and the statement processing flow here is slightly longer, because the script language driver needs to be used to perform parsing processing and create a SqlSource statement information. SqlSource includes BoundSql, and at the same time extends ParameterMapping as a parameter wrapping pass class, rather than just wrapping as a Map structure. Because in this way, the parsed javaType/jdbcType information can be encapsulated

2. Decoupling mapping analysis

Provide a separate XML map builder XMLMapperBuilder class to parse and process the SQL in the Mapper. After this class is provided, the operations of this class can be put into the XML configuration builder, XMLConfigBuilder#mapperElement, for use. Specifically, let's look at the following code.

See the source code : cn.bugstack.mybatis.builder.xml.XMLMapperBuilder

 public class XMLMapperBuilder extends BaseBuilder {

    /**
     * 解析
     */
    public void parse() throws Exception {
        // 如果当前资源没有加载过再加载,防止重复加载
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(element);
            // 标记一下,已经加载过了
            configuration.addLoadedResource(resource);
            // 绑定映射器到namespace
            configuration.addMapper(Resources.classForName(currentNamespace));
        }
    }

    // 配置mapper元素
    // <mapper namespace="org.mybatis.example.BlogMapper">
    //   <select id="selectBlog" parameterType="int" resultType="Blog">
    //    select * from Blog where id = #{id}
    //   </select>
    // </mapper>
    private void configurationElement(Element element) {
        // 1.配置namespace
        currentNamespace = element.attributeValue("namespace");
        if (currentNamespace.equals("")) {
            throw new RuntimeException("Mapper's namespace cannot be empty");
        }

        // 2.配置select|insert|update|delete
        buildStatementFromContext(element.elements("select"));
    }

    // 配置select|insert|update|delete
    private void buildStatementFromContext(List<Element> list) {
        for (Element element : list) {
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);
            statementParser.parseStatementNode();
        }
    }

}

In the parsing of XMLMapperBuilder#parse, it is mainly reflected in resource parsing judgment, Mapper parsing and binding mapper to;

  • configuration.isResourceLoaded Resource judgment avoids repeated parsing and makes a record.
  • The configuration.addMapper binding mapper mainly binds the namespace cn.bugstack.mybatis.test.dao.IUserDao to the Mapper. That is, it is registered in the mapper registration machine.
  • The buildStatementFromContext called by the configurationElement method focuses on processing the XML statement builder, which is explained separately below.

Configure the builder, call the mapping builder, see the source code : cn.bugstack.mybatis.builder.xml.XMLMapperBuilder

 public class XMLConfigBuilder extends BaseBuilder {

    /*
     * <mappers>
     *     <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
     *     <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
     *     <mapper resource="org/mybatis/builder/PostMapper.xml"/>
     * </mappers>
     */
    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            InputStream inputStream = Resources.getResourceAsStream(resource);

            // 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
            mapperParser.parse();
        }
    }

}
  • In XMLConfigBuilder#mapperElement, the original process-oriented processing is decoupled, and the XMLMapperBuilder#parse method is called for parsing processing.

3. Statement Builder

The XMLStatementBuilder statement builder mainly parses the statements in XML select|insert|update|delete . Currently, we take select parsing as an example, and then expand other parsing processes.

See the source code for details : cn.bugstack.mybatis.builder.xml.XMLStatementBuilder

 public class XMLStatementBuilder extends BaseBuilder {

    //解析语句(select|insert|update|delete)
    //<select
    //  id="selectPerson"
    //  parameterType="int"
    //  parameterMap="deprecated"
    //  resultType="hashmap"
    //  resultMap="personResultMap"
    //  flushCache="false"
    //  useCache="true"
    //  timeout="10000"
    //  fetchSize="256"
    //  statementType="PREPARED"
    //  resultSetType="FORWARD_ONLY">
    //  SELECT * FROM PERSON WHERE ID = #{id}
    //</select>
    public void parseStatementNode() {
        String id = element.attributeValue("id");
        // 参数类型
        String parameterType = element.attributeValue("parameterType");
        Class<?> parameterTypeClass = resolveAlias(parameterType);
        // 结果类型
        String resultType = element.attributeValue("resultType");
        Class<?> resultTypeClass = resolveAlias(resultType);
        // 获取命令类型(select|insert|update|delete)
        String nodeName = element.getName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

        // 获取默认语言驱动器
        Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
        LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);

        SqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);

        MappedStatement mappedStatement = new MappedStatement.Builder(configuration, currentNamespace + "." + id, sqlCommandType, sqlSource, resultTypeClass).build();

        // 添加解析 SQL
        configuration.addMappedStatement(mappedStatement);
    }

}
  • The analysis of this part of the content is to disassemble the part about Mapper statement analysis from XMLConfigBuilder. Through such a decoupling design, the whole process will be clearer.
  • The XMLStatementBuilder#parseStatementNode method is the process of parsing the SQL statement node, including the statement ID, parameter type, result type, command ( select|insert|update|delete ), and using the language driver to process and encapsulate the SQL information, when the parsing is completed, write The Map<String, MappedStatement> mapping statement stored in the Configuration configuration file.

4. Scripting language driver

In the parsing of the XMLStatementBuilder#parseStatementNode statement builder, you can see such a block, the operation of getting the default language driver and parsing SQL. In fact, this part is the function implemented by the XML script language driver, which processes static SQL and dynamic SQL in XMLScriptBuilder, but we have only implemented a part of it at present, and will expand after the subsequent framework is perfected to avoid introducing too much at one time. code.

4.1 Defining the interface

See the source code for details : cn.bugstack.mybatis.scripting.LanguageDriver

 public interface LanguageDriver {

    SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);

}
  • Defines a scripting language-driven interface and provides a method for creating SQL information. The input parameters include configuration, elements, and parameters. In fact, there are three implementation classes; XMLLanguageDriver , RawLanguageDriver , VelocityLanguageDriver , here we just implement the default first one.

4.2 XML language driver implementation

See the source code for details : cn.bugstack.mybatis.scripting.xmltags.XMLLanguageDriver

 public class XMLLanguageDriver implements LanguageDriver {

    @Override
    public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

}
  • The implementation of the XML language driver is relatively simple, and it just encapsulates the call processing of XMLScriptBuilder.

4.3 XML Script Builder Parsing

See the source code : cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder

 public class XMLScriptBuilder extends BaseBuilder {

    public SqlSource parseScriptNode() {
        List<SqlNode> contents = parseDynamicTags(element);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        return new RawSqlSource(configuration, rootSqlNode, parameterType);
    }

    List<SqlNode> parseDynamicTags(Element element) {
        List<SqlNode> contents = new ArrayList<>();
        // element.getText 拿到 SQL
        String data = element.getText();
        contents.add(new StaticTextSqlNode(data));
        return contents;
    }

}
  • XMLScriptBuilder#parseScriptNode The processing of parsing SQL nodes is actually not too complicated, mainly the packaging processing of RawSqlSource. Other small details can be learned by reading the source code

4.4 SQL source code builder

See the source code for details : cn.bugstack.mybatis.builder.SqlSourceBuilder

 public class SqlSourceBuilder extends BaseBuilder {

    private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        // 返回静态 SQL
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
       
        @Override
        public String handleToken(String content) {
            parameterMappings.add(buildParameterMapping(content));
            return "?";
        }

        // 构建参数映射
        private ParameterMapping buildParameterMapping(String content) {
            // 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
            Map<String, String> propertiesMap = new ParameterExpression(content);
            String property = propertiesMap.get("property");
            Class<?> propertyType = parameterType;
            ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
            return builder.build();
        }

    }
    
}
  • As mentioned above, the parameters of BoundSql.parameterMappings come from the ParameterMappingTokenHandler#buildParameterMapping method for construction processing.
  • The specific javaType and jdbcType will be reflected in the ParameterExpression parameter expression to complete the parsing operation. This parsing process is directly Mybatis' own source code. The whole process has a single function and can be directly compared and studied.

5. DefaultSqlSession call adjustment

Because of the whole design and implementation above, the parsing process has been adjusted, and the SQL creation has been refined. Then in the MappedStatement mapping statement, BoundSql is replaced by SqlSource, so there will be corresponding adjustments in DefaultSqlSession.

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

 public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;
    private 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.getSqlSource().getBoundSql(parameter));
        return list.get(0);
    }

}
  • The use adjustment here is not big, mainly reflected in the operation of obtaining SQL; ms.getSqlSource().getBoundSql(parameter) After obtaining this, the following process will not change much. After our entire parsing framework is gradually improved, we will begin to set the attribute information addition of each field.

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));
}
  • The test here does not need to be adjusted, because the development content of this chapter is mainly to decouple the parsing of XML, as long as it can keep the same as the previous chapter and output the result normally.

Test Results

07:26:15.049 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 1138410383.
07:26:15.192 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
Disconnected from the target VM, address: '127.0.0.1:54797', transport: 'socket'

Process finished with exit code 0
  • From the test results and debugging screenshots, we can see that after our XML parsing and dismantling, we can successfully support our use.

6. Summary

  • In this chapter, we are like splitting and decoupling the original CRUD code through design principles, using different classes to assume different responsibilities, and implementing the entire function. This includes; combined use of map builders, statement builders, source builders, and corresponding references; scripting language drivers and script builders parsing, processing SQL statements in our XML.
  • Through such refactoring of code, we can also have some insight into the large process-oriented process code in ordinary business development. When you can subdivide and disassemble the responsibilities and functions into different classes, your code will be more clear and easy to maintain.
  • In the future, we will continue to complete the functional logic development of other modules according to the current extension structure base. Because of the construction of these basic contents, it will be easier to continue to supplement functions. Of course, these codes still need to be mastered after you are familiar with them. During the learning process, you can try breakpoint debugging to see what work is being done in each step.

小傅哥
4.7k 声望28.4k 粉丝

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