# 《Mybatis 手撸专栏》第11章:流程解耦,封装结果集处理器

作者:小傅哥
博客:https://bugstack.cn (opens new window)
原文:https://mp.weixin.qq.com/s/0v2ugaiGEwZcFiG04k3-hg (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

# 一、前言

码农,如何为自己的职业生涯续期?

上班就像打怪升级,拿着一把西瓜刀,从南天门砍到北天门。但时间长了,怪越来越凶了,西瓜刀也不得手了。咋办,在游戏里大家肯定是想办法换装备了、买武器了、学技能了,这样才能有机会打通更多的关卡。

其实我们作为程序员上班也是一样的,如果一直都以为这点技术够写写CRUD就够了,反正现在还能应付的了。但3年后呢、5年后呢,总有一天你的技术根本没法满足公司对你现阶段的要求,最简单的CRUD也早已交给了曾经年轻的另外的你。

有人说:“程序员不是技术牛就能一直行!” 但其实技术牛就是行,当你牛到一定的阶段,解决别人解决不了的问题,处理别人处理的不了的方案,蝎子粑粑独一份,谁又能拦得住你呢。在哪里工作都是你自己来定的,你只管技术牛,就能横着走。

# 二、目标

延续着上一章节,我们对参数的封装和调用,使用了策略模式进行解耦处理,本章节将对执行完查询的结果进行封装处理。而不是像我们前面章节那样粗鲁的判断封装,因为这样的方式既不能满足不同类型的优雅扩展,也不以为维护迭代。如图 11-1 所示

图 11-1 简单的结果集处理

  • 对于结果集的封装处理,其实核心在于我们拿到了 Mapper XML 中所配置的返回类型,解析后把从数据库查询到的结果,反射到类型实例化的对象上。
  • 那么这个过程中,我们需要满足不同返回类型的处理,比如Long、Double、String、Date等,都要一一与数据库的类型匹配,与此同时,返回的结果可能是一个普通的基本类型,也可能是我们封装后的对象类型。并这个结果查询也不一定只是一条记录,还可能是多条记录。那么为了更好的处理这些不同情况下的问题,就需要对流程进行分治和实现,以及在过程中进行抽象化的解耦,这样才能满足于我们把不同的返回信息诉求,封装到对象里去。分治、抽象和知识,来自于人月神话中的康威定律,它是系统设计的第一原则。

# 三、设计

在我们使用 JDBC 获取到查询结果 ResultSet#getObject 可以获取返回属性值,但其实 ResultSet 是可以按照不同的属性类型进行返回结果的,而不是都返回 Object 对象(如图11-2 所示)。那么其实我们在上一章节中处理属性信息时候,所开发的 TypeHandler 接口的实现类,就可以扩充返回结果的方法,例如:LongTypeHandler#getResult、StringTypeHandler#getResult 等,这样我们就可以使用策略模式非常明确的定位到返回的结果,而不需要进行if判断处理。

图 11-2 返回类型

再有了这个目标的前提下,就可以通过解析 XML 信息时封装返回类型到映射器语句类中,MappedStatement#resultMaps 直到执行完 SQL 语句,按照我们的返回结果参数类型,创建对象和使用 MetaObject 反射工具类填充属性信息。详细设计如图 11-3 所示

图 11-3 封装结果集处理器

  • 首先我们在解析 XML 语句解析构建器中,添加一个 MapperBuilderAssistant 映射器的助手类,方便我们对参数的统一包装处理,按照职责归属的方式进行细分解耦。通过这样的方式在 MapperBuilderAssistant#setStatementResultMap 中封装返回结果信息,一般来说我们使用 Mybatis 配置返回对象的时候 ResultType 就能解决大部分问题,而不需要都是配置一个 ResultMap 映射结果。但这里的设计其实是把 ResultType 也按照一个 ResultMap 的方式进行封装处理,这样统一一个标准的方式进行包装,做了到适配的效果,也更加方便后面对这样的参数进行统一使用。
  • 接下来就是执行 JDBC 操作查询到数据以后,对结果的封装。那么在 DefaultResultSetHandler 返回结果处理中,首先会按照我们已经解析的到的 ResultType 进行对象的实例化。实例化对象以后再根据解析出来对象中参数的名称获取对应的类型,在根据类型找到 TypeHandler 接口实现类,也就是我们前面提到的 LongTypeHandler、StringTypeHandler,因为通过这样的方式,可以避免 if···else 的判断,而是直接O(1)时间复杂度定位到对应的类型处理器,在不同的类型处理器中返回结果信息。最终拿到结果再通过前面章节已经开发过的 MetaObject 反射工具类进行属性信息的设置。metaObject.setValue(property, value) 最终填充实例化并设置了属性内容的结果对象到上下文中,直至处理完成返回最终的结果数据,以此处理完成。

# 四、实现

# 1. 工程结构

mybatis-step-10
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           ├── builder
    │           │   ├── xml
    │           │   │   ├── XMLConfigBuilder.java
    │           │   │   ├── XMLMapperBuilder.java
    │           │   │   └── XMLStatementBuilder.java
    │           │   ├── BaseBuilder.java
    │           │   ├── MapperBuilderAssistant.java
    │           │   ├── ParameterExpression.java
    │           │   ├── SqlSourceBuilder.java
    │           │   └── StaticSqlSource.java
    │           ├── datasource
    │           ├── executor
    │           │   ├── parameter
    │           │   │   └── ParameterHandler.java
    │           │   ├── result
    │           │   │   ├── DefaultResultContext.java
    │           │   │   └── DefaultResultHandler.java
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   │   └── ResultSetWrapper.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
    │           │   ├── ResultMap.java
    │           │   ├── ResultMapping.java
    │           │   ├── SqlCommandType.java
    │           │   └── SqlSource.java
    │           ├── parsing
    │           ├── reflection
    │           ├── scripting
    │           │   ├── defaults
    │           │   │   ├── DefaultParameterHandler.java
    │           │   │   └── RawSqlSource.java
    │           │   ├── xmltags
    │           │   │   ├── DynamicContext.java
    │           │   │   ├── MixedSqlNode.java
    │           │   │   ├── SqlNode.java
    │           │   │   ├── StaticTextSqlNode.java
    │           │   │   ├── XMLLanguageDriver.java
    │           │   │   └── XMLScriptBuilder.java
    │           │   ├── LanguageDriver.java
    │           │   └── LanguageDriverRegistry.java
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultContext.java
    │           │   ├── ResultHandler.java
    │           │   ├── RowBounds.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    │               ├── BaseTypeHandler.java
    │               ├── JdbcType.java
    │               ├── LongTypeHandler.java
    │               ├── StringTypeHandler.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

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

流程解耦,封装结果集处理器核心类关系,如图 11-4 所示

图 11-4 封装结果集处理器核心类关系

  • 在 XML 语句构建器中使用映射构建器助手,包装映射器语句入参、出参的封装处理。通过此处功能职责的切割,满足不同逻辑单元的扩展。MapperBuilderAssistant#setStatementResultMap 处理 ResultType/ResultMap 的封装信息。
  • 入参信息的解析会存放到映射语句 MappedStatement 类中,这样随着 DefaultSqlSession#selectOne 具体方法的执行时,就可以通过 statement 从配置项中获取到对应的 MappedStatement 信息,所以这里的设计是符合一个充血模型结构的领域功能聚合。
  • 最后就是实现了 ResultSetHandler 结果集处理器接口的 DefaultResultSetHandler 实现类,对查询结果的封装处理,这里主要分为按照解析出来的 resultType 类型进行实例化对象,之后根据对象的属性信息寻找对应的处理策略,避免if···else判断的方式获取对应的结果,当对象和属性都准备完毕后,就可以使用 MetaObject 元对象反射工具类进行属性填充,形成一个完整的结果对象,并写入到结果上下文中 DefaultResultContext 返回。

# 2. 出参参数处理

鉴于对 XML 语句构建器中解析语句后的信息封装会逐步增多,所以这里需要引入映射构建器助手对类中方法的职责进行划分,降低一个方法块内的逻辑复杂度。这样的方式也更加利于代码的维护和扩展。

# 2.1 结果映射封装

熟悉使用 Mybatis 的读者都清楚的知道,在一条语句配置中需要有包括一个返回类型的配置,这个返回类型可以是通过 resultType 配置,也可以使用 resultMap 进行处理,而无论使用哪种方式其实最终都会被封装成统一的 ResultMap 结果映射类。

那么一般我们配置 ResultMap 都是配置了字段的映射,所以实际的代码开发中 ResultMap 还会包含 ResultMapping 也就是每一个字段的映射信息,包括:colum、javaType、jdbcType 等。由于本章节暂时还不涉及到 ResultMap 的使用,所以这里我们先只是建好基本的地基结构就可以。

源码详见cn.bugstack.mybatis.mapping.ResultMap

public class ResultMap {

    private String id;
    private Class<?> type;
    private List<ResultMapping> resultMappings;
    private Set<String> mappedColumns;

		//...
}
1
2
3
4
5
6
7
8
9
  • ResultMap 就是一个简单的返回结果信息映射类,并提供了建造者方法,方便外部使用。没有太多的逻辑行为,具体可以参照源码。

# 2.2 构建器助手

MapperBuilderAssistant 构建器助手专门为创建 MappedStatement 映射语句类而服务的,在这个类中封装了入参和出参的映射、以及把这些配置信息写入到 Configuration 配置项中。

源码详见cn.bugstack.mybatis.builder.MapperBuilderAssistant

public class MapperBuilderAssistant extends BaseBuilder {

    /**
     * 添加映射器语句
     */
    public MappedStatement addMappedStatement(
            String id,
            SqlSource sqlSource,
            SqlCommandType sqlCommandType,
            Class<?> parameterType,
            String resultMap,
            Class<?> resultType,
            LanguageDriver lang
    ) {
        // 给id加上namespace前缀:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById
        id = applyCurrentNamespace(id, false);
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);

        // 结果映射,给 MappedStatement#resultMaps
        setStatementResultMap(resultMap, resultType, statementBuilder);

        MappedStatement statement = statementBuilder.build();
        // 映射语句信息,建造完存放到配置项中
        configuration.addMappedStatement(statement);

        return statement;
    }

    private void setStatementResultMap(
            String resultMap,
            Class<?> resultType,
            MappedStatement.Builder statementBuilder) {
        List<ResultMap> resultMaps = new ArrayList<>();
        /*
         * 通常使用 resultType 即可满足大部分场景
         * <select id="queryUserInfoById" resultType="cn.bugstack.mybatis.test.po.User">
         * 使用 resultType 的情况下,Mybatis 会自动创建一个 ResultMap,基于属性名称映射列到 JavaBean 的属性上。
         */
        ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
                configuration,
                statementBuilder.id() + "-Inline",
                resultType,
                new ArrayList<>());
        resultMaps.add(inlineResultMapBuilder.build());
        statementBuilder.resultMaps(resultMaps);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  • 在映射构建器助手中,提供了添加映射器语句的方法,在这个方法中更加标准的封装了入参和出参信息。如果这些内容全部都堆砌到 XMLStatementBuilder 语句构建器的解析中,就会显得非常臃肿不易于维护了
  • 在 MapperBuilderAssistant#setStatementResultMap 方法中,其实它只是一个非常简单的结果映射建造的过程,无论是否为 ResultMap 都会进行这样的封装处理。并最终把创建的信息写入到 MappedStatement 映射语句类中。

# 2.3 调用助手类

接下来我们就可以清理 XMLStatementBuilder 语句构建器中解析后,映射语句类的构建和存放处理流程。通过使用助手类,统一封装参数信息。

源码详见cn.bugstack.mybatis.builder.xml.XMLStatementBuilder

  • 与上一章节相比,对于这部分的解析后的结果处理的职责内容,划分到了新增加的助手类中,这种实现方式在 Mybatis 的源码中还是非常多的,大部分的内容处理,都会提供一个助手类进行操作。

# 3. 查询结果封装

从 DefaultSqlSession 调用 Executor 语句执行器,一直到 PreparedStatementHandler 预处理语句处理,最后就是 DefaultResultSetHandler 结果信息的封装。

前面章节对此处的封装处理,并没有解耦的操作,只是简单的 JDBC 使用通过查询结果,反射处理返回信息就结束了。如果是使用这样的一个简单的 if···else 面向过程方式进行开发,那么后续所需要满足 Mybatis 的全部封装对象功能,就会变得特别吃力,一个方法块也会越来越大。

所以这一部分的内容处理是需要被解耦,分为;对象的实例化、结果信息的封装、策略模式的处理、写入上下文返回等操作,只有通过这样的解耦流程,才能更加方便的扩展流程不同节点中的各类需求。

源码详见cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#handleResultSet

  • 这是一套结果封装的核心处理流程,包括创建处理器、封装数据和保存结果,接下来就分别介绍下这块代码的具体实现。

# 3.1 结果集收集器

源码详见cn.bugstack.mybatis.executor.result.DefaultResultHandler

public class DefaultResultHandler implements ResultHandler {

    private final List<Object> list;
    /**
     * 通过 ObjectFactory 反射工具类,产生特定的 List
     */
    @SuppressWarnings("unchecked")
    public DefaultResultHandler(ObjectFactory objectFactory) {
        this.list = objectFactory.create(List.class);
    }

    @Override
    public void handleResult(ResultContext context) {
        list.add(context.getResultObject());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 这里封装了一个非常简单的结果集对象,默认情况下都会写入到这个对象的 list 集合中。

# 3.2 对象创建

在处理封装数据的过程中,包括根据 resultType 使用反射工具类 ObjectFactory#create 方法创建出 Bean 对象。这个过程会根据不同的类型进行创建,不过暂时我们这里只是普通对象,所以不会填充太多的代码,避免扰乱读者的重点核心内容的学习

调用链路:handleResultSet->handleRowValuesForSimpleResultMap->getRowValue->createResultObject

源码详见cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#createResultObject

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType);
    if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
        // 普通的Bean对象类型
        return objectFactory.create(resultType);
    }
    throw new RuntimeException("Do not know how to create an instance of " + resultType);
}
1
2
3
4
5
6
7
8
9
  • 对于这样的普通对象,只需要使用反射工具类就可以实例化对象了,不过这个时候属性信息还没有填充。其实和我们使用的 clazz.newInstance(); 也是一样的效果

# 3.3 属性填充

对象实例化完成后,就是根据 ResultSet 获取出对应的值填充到对象的属性中,但这里需要注意,这个结果的获取来自于 TypeHandler#getResult 接口新增的方法,由不同的类型处理器实现,通过这样的策略模式设计方式就可以巧妙的避免 if···else 的判断处理。

图 11-7 使用策略模式,获取返回结果

源码详见cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings

  • columnName 是属性名称,根据属性名称,按照反射工具类从对象中获取对应的 properyType 属性类型,之后再根据类型获取到 TypeHandler 类型处理器。有了具体的类型处理器,在获取每一个类型处理器下的结果内容就更加方便了。
  • 获取属性值后,再使用 MetaObject 反射工具类设置属性值,一次循环设置完成以后,这样一个完整的结果信息 Bean 对象就可以返回了。返回后写入到 DefaultResultContext#nextResultObject 上下文中

# 五、测试

# 1. 事先准备

# 1.1 创建库表

创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:

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
3
4
5
6
7
8
9
10
11
12
13
14

# 1.2 配置数据源

<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>
1
2
3
4
5
6
7
8
9
10
11
  • 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
  • 在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。

# 1.3 配置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>
1
2
3
4
5
  • 这部分暂时不需要调整,目前还只是一个入参的类型的参数,后续我们全部完善这部分内容以后,则再提供更多的其他参数进行验证。

# 2. 单元测试

@Before
public void init() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    sqlSession = sqlSessionFactory.openSession();
}

@Test
public void test_queryUserInfoById() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证:基本参数
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试结果:{}", JSON.toJSONString(user));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 这里我们只测试一个查询结果即可,返回的类型是一个自定义的对象类型。

测试结果

12:39:17.321 [main] INFO  c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:id propertyType:class java.lang.Long
12:39:17.321 [main] INFO  c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
12:39:17.382 [main] INFO  c.b.m.s.defaults.DefaultSqlSession - 执行查询 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById parameter:1
12:39:17.684 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
12:39:17.728 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}

Process finished with exit code 0
1
2
3
4
5
6
7
  • 通过 DefaultResultSetHandler 结果处理器的功能解耦和实现,已经可以正常查询和返回对应的对象信息了,后续其他内容的扩展也可以基于这个基座进行处理。

# 六、总结

  • 这一章节的整个功能实现,都在围绕流程的解耦进行处理,将对象的参数解析和结果封装都进行拆解,通过这样的方式来分配各个模块的单一职责,不让一个类的方法承担过多的交叉功能。
  • 那么我们在结合这样的思想和设计,反复阅读和动手实践中,来学习这样的代码设计和开发过程,都能为我们以后实际开发业务代码时候带来参考建议,避免总是把所有的流程都写到一个类或者方法中。
  • 到本章节全核心流程基本就串联清楚了,再有的就是一些功能的拓展,比如支持更多的参数类型,以及添加除了 Select 以外的其他操作,还有一些缓存数据的使用等,后面章节将在这些内容中,摘取一些核心的设计和实现进行讲解,让读者吸收更多的设计技巧。