作者:小傅哥 博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!😜❞
码农,如何为自己的职业生涯续期?
上班就像打怪升级,拿着一把西瓜刀,从南天门砍到北天门。但时间长了,怪越来越凶了,西瓜刀也不得手了。咋办,在游戏里大家肯定是想办法换装备了、买武器了、学技能了,这样才能有机会打通更多的关卡。
其实我们作为程序员上班也是一样的,如果一直都以为这点技术够写写CRUD就够了,反正现在还能应付的了。但3年后呢、5年后呢,总有一天你的技术根本没法满足公司对你现阶段的要求,最简单的CRUD也早已交给了曾经年轻的另外的你。
有人说:“程序员不是技术牛就能一直行!” 但其实技术牛就是行,当你牛到一定的阶段,解决别人解决不了的问题,处理别人处理的不了的方案,蝎子粑粑独一份,谁又能拦得住你呢。在哪里工作都是你自己来定的,你只管技术牛,就能横着走。
延续着上一章节,我们对参数的封装和调用,使用了策略模式进行解耦处理,本章节将对执行完查询的结果进行封装处理。而不是像我们前面章节那样粗鲁的判断封装,因为这样的方式既不能满足不同类型的优雅扩展,也不以为维护迭代。如图 11-1 所示
图 11-1 简单的结果集处理
在我们使用 JDBC 获取到查询结果 ResultSet#getObject 可以获取返回属性值,但其实 ResultSet 是可以按照不同的属性类型进行返回结果的,而不是都返回 Object 对象(如图11-2 所示)。那么其实我们在上一章节中处理属性信息时候,所开发的 TypeHandler 接口的实现类,就可以扩充返回结果的方法,例如:LongTypeHandler#getResult、StringTypeHandler#getResult 等,这样我们就可以使用策略模式非常明确的定位到返回的结果,而不需要进行if判断处理。
图 11-2 返回类型
再有了这个目标的前提下,就可以通过解析 XML 信息时封装返回类型到映射器语句类中,MappedStatement#resultMaps 直到执行完 SQL 语句,按照我们的返回结果参数类型,创建对象和使用 MetaObject 反射工具类填充属性信息。详细设计如图 11-3 所示
图 11-3 封装结果集处理器
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
工程源码:公众号「bugstack虫洞栈」,回复:手写Mybatis,获取完整源码
流程解耦,封装结果集处理器核心类关系,如图 11-4 所示
图 11-4 封装结果集处理器核心类关系
鉴于对 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;
//...
}
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);
}
}
接下来我们就可以清理 XMLStatementBuilder 语句构建器中解析后,映射语句类的构建和存放处理流程。通过使用助手类,统一封装参数信息。
源码详见:cn.bugstack.mybatis.builder.xml.XMLStatementBuilder
从 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());
}
}
在处理封装数据的过程中,包括根据 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);
}
对象实例化完成后,就是根据 ResultSet 获取出对应的值填充到对象的属性中,但这里需要注意,这个结果的获取来自于 TypeHandler#getResult 接口新增的方法,由不同的类型处理器实现,通过这样的策略模式设计方式就可以巧妙的避免 if···else 的判断处理。
图 11-7 使用策略模式,获取返回结果
源码详见:cn.bugstack.mybatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings
创建一个数据库名称为 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', '小傅哥');
<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>
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password<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>
@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));
}
测试结果
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
- END -
你好,我是小傅哥。一线互联网java
工程师、架构师,开发过交易&营销、写过运营&活动、设计过中间件也倒腾过中继器、IO板卡。不只是写Java语言,也搞过C#、PHP,是一个技术活跃的折腾者。
2021年上架一本小册《SpringBoot 中间件设计和开发》,16个互联网中间件场景、30个工程,是全网唯一一次手把手教你造轮子、写中间件,因为这样的技术离P7最近、离架构师最近、离高薪资最近!