码农,如何为自己的职业生涯续期?
上班就像打怪升级,拿着一把西瓜刀,从南天门砍到北天门。但时间长了,怪越来越凶了,西瓜刀也不得手了。咋办,在游戏里大家肯定是想办法换装备了、买武器了、学技能了,这样才能有机会打通更多的关卡。
其实我们作为程序员上班也是一样的,如果一直都以为这点技术够写写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)最终填充实例化并设置了属性内容的结果对象到上下文中,直至处理完成返回最终的结果数据,以此处理完成。
复制
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 │ │ ├── resultset │ │ │ └── ParameterHandler.java │ │ ├── resultset │ │ │ ├── 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 │ ├── 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.
流程解耦,封装结果集处理器核心类关系,如图 11-4 所示
图 11-4 封装结果集处理器核心类关系
在 XML 语句构建器中使用映射构建器助手,包装映射器语句入参、出参的封装处理。通过此处功能职责的切割,满足不同逻辑单元的扩展。MapperBuilderAssistant#setStatementResultMap 处理 ResultType/ResultMap 的封装信息。
入参信息的解析会存放到映射语句 MappedStatement 类中,这样随着 DefaultSqlSession#selectOne 具体方法的执行时,就可以通过 statement 从配置项中获取到对应的 MappedStatement 信息,所以这里的设计是符合一个充血模型结构的领域功能聚合。
最后就是实现了 ResultSetHandler 结果集处理器接口的 DefaultResultSetHandler 实现类,对查询结果的封装处理,这里主要分为按照解析出来的 resultType 类型进行实例化对象,之后根据对象的属性信息寻找对应的处理策略,避免if···else判断的方式获取对应的结果,当对象和属性都准备完毕后,就可以使用 MetaObject 元对象反射工具类进行属性填充,形成一个完整的结果对象,并写入到结果上下文中 DefaultResultContext 返回。
鉴于对 XML 语句构建器中解析语句后的信息封装会逐步增多,所以这里需要引入映射构建器助手对类中方法的职责进行划分,降低一个方法块内的逻辑复杂度。这样的方式也更加利于代码的维护和扩展。
熟悉使用 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 就是一个简单的返回结果信息映射类,并提供了建造者方法,方便外部使用。没有太多的逻辑行为,具体可以参照源码。
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 映射语句类中。
接下来我们就可以清理 XMLStatementBuilder 语句构建器中解析后,映射语句类的构建和存放处理流程。通过使用助手类,统一封装参数信息。
源码详见:cn.bugstack.mybatis.builder.xml.XMLStatementBuilder
与上一章节相比,对于这部分的解析后的结果处理的职责内容,划分到了新增加的助手类中,这种实现方式在 Mybatis 的源码中还是非常多的,大部分的内容处理,都会提供一个助手类进行操作。
从 DefaultSqlSession 调用 Executor 语句执行器,一直到 PreparedStatementHandler 预处理语句处理,最后就是 DefaultResultSetHandler 结果信息的封装。
前面章节对此处的封装处理,并没有解耦的操作,只是简单的 JDBC 使用通过查询结果,反射处理返回信息就结束了。如果是使用这样的一个简单的 if···else 面向过程方式进行开发,那么后续所需要满足 Mybatis 的全部封装对象功能,就会变得特别吃力,一个方法块也会越来越大。
所以这一部分的内容处理是需要被解耦,分为;对象的实例化、结果信息的封装、策略模式的处理、写入上下文返回等操作,只有通过这样的解耦流程,才能更加方便的扩展流程不同节点中的各类需求。
源码详见:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#handleResultSet
这是一套结果封装的核心处理流程,包括创建处理器、封装数据和保存结果,接下来就分别介绍下这块代码的具体实现。
源码详见: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 集合中。
在处理封装数据的过程中,包括根据 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(); 也是一样的效果
对象实例化完成后,就是根据 ResultSet 获取出对应的值填充到对象的属性中,但这里需要注意,这个结果的获取来自于 TypeHandler#getResult 接口新增的方法,由不同的类型处理器实现,通过这样的策略模式设计方式就可以巧妙的避免 if···else 的判断处理。
图 11-7 使用策略模式,获取返回结果
源码详见:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings
columnName 是属性名称,根据属性名称,按照反射工具类从对象中获取对应的 properyType 属性类型,之后再根据类型获取到 TypeHandler 类型处理器。有了具体的类型处理器,在获取每一个类型处理器下的结果内容就更加方便了。
获取属性值后,再使用 MetaObject 反射工具类设置属性值,一次循环设置完成以后,这样一个完整的结果信息 Bean 对象就可以返回了。返回后写入到 DefaultResultContext#nextResultObject 上下文中
创建一个数据库名称为 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.
复制
<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 进行测试验证。
复制
<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.
这部分暂时不需要调整,目前还只是一个入参的类型的参数,后续我们全部完善这部分内容以后,则再提供更多的其他参数进行验证。
复制
@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 以外的其他操作,还有一些缓存数据的使用等,后面章节将在这些内容中,摘取一些核心的设计和实现进行讲解,让读者吸收更多的设计技巧。