文章目录
- 一、前言
- 二、DefaultParameterHandler
- 1. DefaultParameterHandler#setParameters
- 1.1 UnknownTypeHandler
- 1.2 自定义 TypeHandler
- 三、DefaultResultSetHandler
- 1. hasNestedResultMaps
- 2. handleRowValuesForNestedResultMap
- 2.1 resolveDiscriminatedResultMap
- 2.2 createRowKey
- 2.3 getRowValue
- 2.2.1 createResultObject
- 2.2.2 applyAutomaticMappings
- 2.2.3 applyPropertyMappings
- 2.2.4 applyNestedResultMappings
- 2.4 storeObject
- 3. handleRowValuesForSimpleResultMap
一、前言
Mybatis 官网 以及 本系列文章地址:
- Mybatis 源码 ① :开篇
- Mybatis 源码 ② :流程分析
- Mybatis 源码 ③ :SqlSession
- Mybatis 源码 ④ :TypeHandler
- Mybatis 源码 ∞ :杂七杂八
书接上文 Mybatis 源码 ③ :SqlSession
。我们这里来看下 DefaultParameterHandler 和 DefaultResultSetHandler 的处理过程。
二、DefaultParameterHandler
DefaultParameterHandler 类图如下,可以看到其实现了 ParameterHandler 接口,我们可以通过 Plugin 的方式对 ParameterHandler 进行增强。这里我们主要来看 DefaultParameterHandler 的具体作用。
1. DefaultParameterHandler#setParameters
在 SimpleExecutor 和 BaseExecutor doUpdate、doQuery、doQueryCursor 等方法中会调用 prepareStatement 方法,在其中会调用 StatementHandler#parameterize 来对参数做预处理,里面会调用 PreparedStatementHandler#parameterize,该方法如下:
@Overridepublic void parameterize(Statement statement) throws SQLException {// 这里会调用 DefaultParameterHandler#setParametersparameterHandler.setParameters((PreparedStatement) statement);}
因此我们可以知道,在Sql 执行前,会调用 DefaultParameterHandler#setParameters 方法来对参数做处理,这也就给了 TypeHandler 的参数转换提供了条件。
DefaultParameterHandler#setParameters 实现如下:
@Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 获取当前Sql执行时的参数List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();// 对一些额外参数处理if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 判断是否有合适的类型转换器,可以解析当前参数// 这里个人理解是为了判断参数是否是单独参数,value = parameterObject;} else {// 根据 参数名去获取参数传入的值。MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 如果当前参数指定了类型转换器, 则通过类型转换器进行转换。否则交由 UnknownTypeHandler TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 调用类型转换器进行处理, 默认情况下是 UnknownTypeHandler // jdbcType 是我们通过 jdbcType 属性指定的类型,没有指定则为空 typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}
上面可以看到逻辑比较简单:遍历所有参数,并且参数值交由 typeHandler.setParameter
来处理。需要注意的是这里的 typeHandler 如果没有指定默认是 UnknownTypeHandler。在UnknownTypeHandler 中则会根据参数实际类型来从注册的 TypeHandler 中选择合适的处理器来处理。下面我们具体来看。
1.1 UnknownTypeHandler
UnknownTypeHandler#setParameter 会调用 UnknownTypeHandler#setNonNullParameter, 我们以该方法为例,UnknownTypeHandler 的其他方法也类似。
@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)throws SQLException {// 根据参数类型来获取 类型处理器// jdbcType 是我们通过 jdbcType 属性指定的类型,没有指定则为空 TypeHandler handler = resolveTypeHandler(parameter, jdbcType);// 调用类型处理器处理handler.setParameter(ps, i, parameter, jdbcType);}private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {TypeHandler<?> handler;// 参数为空直接返回 ObjectTypeHandlerif (parameter == null) {handler = OBJECT_TYPE_HANDLER;} else {// 从注册的 TypeHandler 中根据类型选择合适的处理器handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);// check if handler is null (issue #270)// 如果没找到返回 ObjectTypeHandlerif (handler == null || handler instanceof UnknownTypeHandler) {handler = OBJECT_TYPE_HANDLER;}}return handler;}
这里可以看到, 在执行Sql前会通过 DefaultParameterHandler#setParameters 对参数做一次处理。
- 如果参数指定了 typeHandler 则使用参数指定的 TypeHandler
- 如果参数没有指定,则使用 UnknownTypeHandler 来处理。而 UnknownTypeHandler 会根据参数的实际类型和 jdbcType 来从已注册的 TypeHandler 选择合适的处理器对参数做处理。
1.2 自定义 TypeHandler
我们可以自定义 TypeHandler 来实现指定字段的特殊处理,如用户密码在数据库中不能明文展示,而在代码中我们明文处理,则就可以通过如下方式定义:
- 创建一个 PwdTypeHandler 类,继承 BaseTypeHandler
public class PwdTypeHandler extends BaseTypeHandler<String> {// 定义加解密方式private static final SymmetricCrypto AES = new SymmetricCrypto(SymmetricAlgorithm.AES, "1234567890123456".getBytes());// 赋值时加密@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, AES.encryptBase64(parameter));}// 取值时解密@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return AES.decryptStr(rs.getString(columnName));}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return AES.decryptStr(rs.getString(columnIndex));}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return AES.decryptStr(cs.getString(columnIndex));}
}
- XML 指定使用的 typeHandler,如下
<resultMap id="BaseResultMap" type="com.kingfish.entity.SysUser"><!--@Table sys_user--><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" typeHandler="com.kingfish.config.handler.PwdTypeHandler"/><!-- 忽略其他字段 --></resultMap>
- 在实际调用接口时新增或返回时都会使用 PwdTypeHandler 来对指定字段做处理人,如下:
- 调用接口明文新增时入库是加密后结果
- 数据库加密,查询返回是明文
三、DefaultResultSetHandler
DefaultResultSetHandler实现了ResultSetHandler 接口,ResultSetHandler 见名知意,即为结果集合处理器。所以下面我们来看看该方法的具体逻辑 :
@Overridepublic List<Object> handleResultSets(Statement stmt) throws SQLException {ErrorContext.instance().activity("handling results").object(mappedStatement.getId());final List<Object> multipleResults = new ArrayList<>();int resultSetCount = 0;// 获取第一个结果集 ResultSet 并包装成 ResultSetWrapper ResultSetWrapper rsw = getFirstResultSet(stmt);List<ResultMap> resultMaps = mappedStatement.getResultMaps();// ResultMap 的数量, 当使用存储过程时,可能会有多个,我们这里不考虑存储过程的多个场景。int resultMapCount = resultMaps.size();// ResultMap 数量校验 :rsw != null && resultMapCount < 1validateResultMapsCount(rsw, resultMapCount);、/**********************************************************************/// 1.对 ResultMap 的处理// 循环所有的 ResultMapwhile (rsw != null && resultMapCount > resultSetCount) {// 获取当前 ResultMapResultMap resultMap = resultMaps.get(resultSetCount);// 1.1 根据ResultMap中定义的映射规则处理ResultSet,并将映射得到的Java对象添加到 multipleResults集合中保存handleResultSet(rsw, resultMap, multipleResults, null);// 1.2 获取下一个 ResultSet rsw = getNextResultSet(stmt);// 1.3 清理nestedResultObjects集合,这个集合是用来存储中间数据的cleanUpAfterHandlingResultSet();resultSetCount++;}/**********************************************************************/// 2. 对 ResultSets 的处理// 对 resultSet 处理,<select>标签可以通过 resultSets 属性指定String[] resultSets = mappedStatement.getResultSets();if (resultSets != null) {// 处理reusltSet while (rsw != null && resultSetCount < resultSets.length) {// 获取ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);if (parentMapping != null) {String nestedResultMapId = parentMapping.getNestedResultMapId();ResultMap resultMap = configuration.getResultMap(nestedResultMapId);handleResultSet(rsw, resultMap, null, parentMapping);}// 获取下一个 ResultSetrsw = getNextResultSet(stmt);// 清理nestedResultObjects集合,这个集合是用来存储中间数据的cleanUpAfterHandlingResultSet();resultSetCount++;}}/**********************************************************************/// 返回结果集return collapseSingleResultList(multipleResults);}
这里可以看到, DefaultResultSetHandler#handleResultSet 方法的逻辑分为对 ResultMap 的处理和 对 ResultSets 的处理,在涉及存储过程的情况下会返回 ResultSets ,该部分不在本文的讨论范围内,在 Mybatis 官方文档 中对该属性做了具体的描述 : 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。具体使用如下图:
业务使用方面可以详参: https://blog.csdn.net/qq_40233503/article/details/94436578
本文主要看对 ResultMap 的处理内容,而其中最主要的则是 DefaultResultSetHandler#handleResultSet 方法,具体实现如下:
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {try {// 父级 mapper 不为空的情况 :在处理 ResultSet 时会出现,不在本文讨论范围if (parentMapping != null) {handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);} else {// 1. 未指定 ResultHandler 情况 : 如果 resultHandler 为空则创建一个 DefaultResultHandler 作为默认处理器// 这里的 resultHandler 是我们调用 Mapper Interface Method 方法时指定的。如果没指定则为空if (resultHandler == null) {// 如果没指定则使用默认的 DefaultResultHandler 来处理结果DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);multipleResults.add(defaultResultHandler.getResultList());} else {// 2. 指定了 ResultHandler 情况 : 将 resultHandler 传入作为结果处理器handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);}}} finally {// issue #228 (close resultsets)closeResultSet(rsw.getResultSet());}}
上面可以看到这里针对了 未指定 ResultHandler 情况 和 指定了 ResultHandler 情况做了判断:我们可以在 Mapper Interface Method 入参中传入 ResultHandler 来对返回结果集做处理。(也可传入 RowBounds 对返回结果集做逻辑分页,但是需要注意 RowBounds 仅是逻辑分页,数据已经查出,所以不建议使用),通过实现ResultHandler 接口来对该查询的结果进行定制化解析(需要注意方法不能有返回值,因为返回值已经交由 resultHandler 来处理了),当 Mybatis 将结果查询出后会交由 resultHandler#handleResult 方法来处理。在方法入参中传入 ResultHandler 实例,并且返回值为 void,如下指定了 selectByParam 方法查询的结果交由 ResultHandler 来处理:
void selectByParam(ResultHandler resultHandler);
而实际上无论 ResultHandler 指定与否,都会调用 DefaultResultSetHandler#handleRowValues 方法来解析行数据,所以我们来看看该方法的具体实现,如下:
// 处理行数据 : 该方法会获取并解析出来每一行的数据public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {// 1. 如果有嵌套的 ResultMap,即 ResultMap#hasNestedResultMaps = trueif (resultMap.hasNestedResultMaps()) {// 嵌套前的判断1 :嵌套情况下,如果 safeRowBoundsEnabled 为true,则不能使用 RowBounds (确切的说只能使用 默认的 RowBounds )// safeRowBoundsEnabled 可以通过 {mybatis.configuration.safe-row-bounds-enabled} 配置,代表 允许在嵌套语句中使用分页(RowBounds) , 默认 trueensureNoRowBounds();// 嵌套前的判断2 :嵌套情况下,如果 safeResultHandlerEnabled 为 true && 语句属性 resultOrdered 为 true 则抛出异常// safeResultHandlerEnabled 可以通过 {mybatis.configuration.safe-result-handler-enabled} 配置,代表 允许在嵌套语句中使用分页(ResultHandler)。默认 truecheckResultHandler();// 2. 处理嵌套 ResultMaphandleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {// 3. 无嵌套 ResultMap的 简单逻辑handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}}
这里可以看到,对于行数据的处理分为嵌套情况和非嵌套情况,如下 :
-
DefaultResultSetHandler#hasNestedResultMaps :通过 ResultMap#hasNestedResultMaps 属性判断当前是否是嵌套结果集,成立条件是
<resultMap>
标签中使用了子标签<association>
、<collection>
,并且标签没有指定 select 属性 或使用了<case>
标签。(如果指定了select属性,则会保存在 ResultMapping#nestedQueryId 指定 查询id)。 该属性在于 ResultMap.Builder#build 中会初始化,如下:
-
DefaultResultSetHandler#handleRowValuesForNestedResultMap :用来处理嵌套结果集的情况,即如果上面的判断成立了,则执行该逻辑。
-
DefaultResultSetHandler#handleRowValuesForSimpleResultMap :用来处理简单查询的情况,无嵌结果集的情况。
下面我们详细来看上面的详细逻辑
1. hasNestedResultMaps
DefaultResultSetHandler#hasNestedResultMaps 方法的作用是判断当前 ResultMap 是否是嵌套结果集,其判断依据是 ResultMap#hasNestedResultMaps = true
,如下:
public boolean hasNestedResultMaps() {return hasNestedResultMaps;}
而 ResultMap#hasNestedResultMaps 属性的初始化是在ResultMap.Builder#build 中完成,如下:
这里我们关注两个属性:ResultMap#hasNestedQueries (标记当前 ResultMap 是否有嵌套映射,判断依据是 ResultMapping#nestedQueryId != null
)和 ResultMap#hasNestedResultMaps (标记当前 ResultMap 是否有嵌套结果集,判断依据是 ResultMapping#nestedResultMapId != null || ResultMapping#resultSet != null
)
我们以 XML 解析为例,在 XMLMapperBuilder#buildResultMappingFromContext中,会通过如下逻辑来解析取 nestedSelect、nestedResultMap 属性 :
并且在 MapperBuilderAssistant#buildResultMapping 方法中根据 nestedSelect、nestedResultMap 来给 ResultMapping#nestedQueryId 和 ResultMapping#nestedResultMapId 赋值,如下:
综上,这里的嵌套判断成立的条件是 :<resultMap>
标签中使用了子标签 <association>
、<collection>
,并且标签没有指定 select 属性 或使用了 <case>
标签。(如果指定了select属性,则会保存在 ResultMapping#nestedQueryId 指定 查询id)。下面我们来简单介绍下这两种情况的区别。
对于嵌套映射,其存在两种实现方式:
- 内部嵌套 : 使用 association、collection 标签但是不指定 select 属性,或使用case 标签。这种是通过一条 Sql 语句查询后关联处理。 下面DefaultResultSetHandler#handleRowValuesForNestedResultMap 的方法就是处理该情况
- 外部嵌套 : 使用 association、collection 标签并指定 select 属性。这种是通过一条Sql语句执行后再根据select指定语句关联查询。下面DefaultResultSetHandler#applyPropertyMappings 中会对这种嵌套查询做处理
我们以如下两个表为例:
CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',`user_name` varchar(255) DEFAULT NULL COMMENT '用户名',`password` varchar(255) DEFAULT NULL COMMENT '密码',`role_id` bigint(20) DEFAULT NULL COMMENT '角色id',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',`role_name` varchar(255) DEFAULT NULL COMMENT '用户名',`status` varchar(255) DEFAULT NULL COMMENT '状态',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-
内部嵌套实现如下:
<resultMap id="UserBaseResultMap" type="com.kingfish.entity.SysUser"><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" /><!-- 忽略余下属性 --></resultMap><resultMap id="BaseResultMap" type="com.kingfish.entity.SysRole"><!--@Table sys_role--><result property="id" column="id" jdbcType="INTEGER"/><result property="roleName" column="role_name" jdbcType="VARCHAR"/><result property="status" column="status" jdbcType="VARCHAR"/><!-- 忽略余下属性 --></resultMap><!-- 内部嵌套映射 --><resultMap id="InnerNestMap" type="com.kingfish.entity.dto.SysRoleDto" extends="BaseResultMap"><!-- 指定 sysUsers 属性都是前缀为 user_ 的属性 --><collection property="sysUsers" columnPrefix="user_"resultMap="UserBaseResultMap"></collection></resultMap><!-- 通过联表查询出来多个属性,如果属性名跟 sysUsers 对应的com.kingfish.dao.SysUserDao.BaseResultMap配置的属性名一致则会映射上去 (属性名映射规则受到columnPrefix影响) --><select id="selectRoleUser" resultMap="InnerNestMap">select sr.*, su.id user_id, su.user_name user_user_name, su.password user_passwordfrom sys_role srleft join sys_user su on sr.id = su.role_id</select>
-
外部嵌套实现如下:
<resultMap id="UserBaseResultMap" type="com.kingfish.entity.SysUser"><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" /><!-- 忽略余下属性 --></resultMap><!-- 外部嵌套映射 --><resultMap id="OutNestMap" type="com.kingfish.entity.dto.SysRoleDto" extends="BaseResultMap"><!-- 指定使用selectUser 作为 sysUsers 属性的查询语句 --><collection property="sysUsers" ofType="com.kingfish.entity.dto.SysUserDto"select="selectUser" column="{roleId=id}" ></collection></resultMap><select id="selectUser" resultMap="UserBaseResultMap">selectid, user_name, passwordfrom sys_userwhere role_id = #{roleId}</select><select id="selectRole" resultMap="OutNestMap">select *from sys_role</select>
上述两种查询的返回结果都相同,如下:
关于该部分内容本文只做简单介绍,如有需要可详参:https://www.cnblogs.com/sanzao/p/11466496.html#_label1
2. handleRowValuesForNestedResultMap
上面我们介绍了嵌套条件成立的条件,当满足了上述条件后,说明了当前查询存在嵌套结果集,则调用 DefaultResultSetHandler#handleRowValuesForNestedResultMap 来处理,具体如下
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();// 跳过执行行数据,由 RowBounds.offset 属性决定skipRows(resultSet, rowBounds);Object rowValue = previousRowValue;// 确定当前剩余数据满足条件,即此次拉取的数据量 < RowBounds.limmit 时 且 连接未关闭 且后续还有结果集,则再次获取while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 1. 解析 discriminator 属性,获取 discriminator 指定的 ResultMap final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 2. 创建当前行记录的 缓存 keyfinal CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);// 尝试获取该行记录的缓存Object partialObject = nestedResultObjects.get(rowKey);// issue #577 && #542// resultOrdered = true 时if (mappedStatement.isResultOrdered()) {// 如果未缓存安全数据if (partialObject == null && rowValue != null) {// 清空缓存nestedResultObjects.clear();// 存储数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}// 3. 获取行数据 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);} else {// 3. 获取行数据rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);// 4. 存储数据 : partialObject == null 说明数据没有被缓存if (partialObject == null) {storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}}// 行数据不为空 && resultOrdered = true && 还需要查询更多行if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {// 存储数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);previousRowValue = null;} else if (rowValue != null) {previousRowValue = rowValue;}}
这里我们可以看到 :
- 利用 RowBounds 是可以实现分页的功能的,但却是一个逻辑分页,因为所有数据都是已经加载到内存后再根据 RowBounds 的分页限制选择是否丢弃或继续获取,因此并不建议使用。
- resolveDiscriminatedResultMap 方法实现了对
<discriminator >
标签的解析,并将<discriminator >
解析后的ResultMap 作为最终的 ResultMap 处理。下面我们会详细讲。 - getRowValue 方法会根据 resultMap 解析并获取当前的行数据。下面我们会详细讲。
- storeObject 方法会将处理后的行结果缓存起来。下面我们会详细讲。
2.1 resolveDiscriminatedResultMap
该方法的作用是为了解析 <discriminator>
标签, 内容比较简单,这里不在过多赘述。关于 <discriminator>
标签的用法,如有需要详参 Mybatis 源码 ∞ :杂七杂八
public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {Set<String> pastDiscriminators = new HashSet<>();Discriminator discriminator = resultMap.getDiscriminator();while (discriminator != null) {// 获取 discriminator 指定的 column 的值final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);// 根据 column 的值来判断执行哪个 case 分支 : 根据 value 获取到 discriminatedMapId ,如果获取到则说明有对应的 case 分支final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));// 如果存在该 ResultMap if (configuration.hasResultMap(discriminatedMapId)) {// 用 discriminator 指定 ResultMap 替换现有的 resultMap resultMap = configuration.getResultMap(discriminatedMapId);Discriminator lastDiscriminator = discriminator;discriminator = resultMap.getDiscriminator();if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {break;}} else {break;}}return resultMap;}
2.2 createRowKey
createRowKey 方法 作用是创建当前行的缓存Key。具体实现如下:
// 生成行数据的缓存Key,这里会将列名和列值都作为关键值创建Key// 在嵌套映射中会作为唯一标志标识一个结果对象private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {final CacheKey cacheKey = new CacheKey();// 使用映射结果集的id 作为 CacheKey 的一部分cacheKey.update(resultMap.getId());// 获取 <result> 标签结果集List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);// 为空则判断返回类型是是不是Mapif (resultMappings.isEmpty()) {if (Map.class.isAssignableFrom(resultMap.getType())) {// 由结果集中的所有列名以及当前记录行的所有列值一起构成CacheKeycreateRowKeyForMap(rsw, cacheKey);} else {// 由结果集中未映射的列名以及它们在当前记录行中的对应列值一起构成CacheKey对象createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);}} else {// 由ResultMapping集合中的列名以及它们在当前记录行中相应的列值一起构成CacheKeycreateRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);}// 如果除了映射结果集的id 之外没有任何属性参与生成CacheKey 则返回NULL_CACHE_KEYif (cacheKey.getUpdateCount() < 2) {return CacheKey.NULL_CACHE_KEY;}return cacheKey;}
这里我们不再具体分析具体的代码内容,直接总结具体的逻辑(下面内容来源 Mybatis源码阅读(三):结果集映射3.2 —— 嵌套映射):
- 尝试使用节点或者节点中定义的列名以及该列在当前记录行中对应的列值生成CacheKey
- 如果ResultMap中没有定义这两个节点,则有ResultMap中明确要映射的列名以及它们在当前记录行中对应的列值一起构成CacheKey对象
- 经过上面两个步骤后如果依然查不到相关的列名和列值,且ResultMap的type属性明确指明了结果对象为Map类型,则有结果集中所有列名以及改行记录行的所有列值一起构成CacheKey
- 如果映射的结果对象不是Map,则由结果集中未映射的列名以及它们在当前记录行中的对应列值一起构成CacheKey
额外需要注意的是 ,CacheKey 创建后,会尝试从 nestedResultObjects 中获取对象对数据。如下:
Object partialObject = nestedResultObjects.get(rowKey);
nestedResultObjects 的作用是缓存所有查询出的结果数据,但是这里会存在问题:在嵌套映射时如果存在两行完全一样的数据,则会被忽略。该问题我们在 Mybatis 源码 ∞ :杂七杂八 进行了详细说明
2.3 getRowValue
getRowValue 方法是处理每一行的值,需要注意的是这里的 handleRowValuesForNestedResultMap 中调用的 getRowValue 方法和 handleRowValuesForSimpleResultMap 中调用的 getRowValue 方法是重载方法。
下面我们来具体看 handleRowValuesForNestedResultMap 中调用的 getRowValue 如下:
// DefaultResultSetHandler#getRowValue(ResultSetWrapper, ResultMap, .CacheKey, String, Object)// 将数据库查出来的数据转换为 Mapper Interface Method 返回的类型private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {// 获取 ResultMap 的唯一IDfinal String resultMapId = resultMap.getId();// 外层数据赋值给 rowValueObject rowValue = partialObject;// 如果缓存有值,则认为是嵌套映射if (rowValue != null) {// 用外层数据生成元数据 metaObject final MetaObject metaObject = configuration.newMetaObject(rowValue);// 外层数据保存到 ancestorObjects 中putAncestor(rowValue, resultMapId);// 处理嵌套逻辑applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);// 从 ancestorObjects 中移除该数据ancestorObjects.remove(resultMapId);} else {final ResultLoaderMap lazyLoader = new ResultLoaderMap();// 1. 反射 Mapper Interface Method 返回的类型对象,这里尚未填充行数据rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// rowValue 不为空 && 没有针对 rowValue 类型的 TypeHandler if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 如果允许自动映射(可通过 <resultMap> 标签的 autoMapping 属性指定)if (shouldApplyAutomaticMappings(resultMap, true)) {// 2. 根据自动映射规则尝试映射,看是行数据是否能映射到对应的属性 (忽略大小写的映射)foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 3. 根据属性映射foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;putAncestor(rowValue, resultMapId);// 4. 对嵌套结果集进行映射foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;ancestorObjects.remove(resultMapId);foundValues = lazyLoader.size() > 0 || foundValues;// 如果 映射到了属性值 或者 配置了空数据返回实体类 (mybatis.configuration.return-instance-for-empty-row 属性指定)则 返回 rowValue, 否则返回空 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}// 缓存外层对象if (combinedKey != CacheKey.NULL_CACHE_KEY) {nestedResultObjects.put(combinedKey, rowValue);}}return rowValue;}
上面我们我们主要看下面几个方法:
-
createResultObject : 这里会创建Mapper Interface Method 返回的类型对象,但是并没有对各个属性赋值。不过需要注意 createResultObject 方法创建返回对象时分为下面集中情况:
- 如果Mybatis 中注册了针对 ResultMap.type 类型的 TypeHandler,则会调用 TypeHandler#getResult 来获取结果
- 如果当前 ResultMap 指定了构造函数参数,则使用指定入参构造结果
- 如果 ResultMap.type 是接口类型或者 ResultMap.type 有默认构造函数,则通过 ObjectFactory#create 创建构造函数
- 如果开启了自动映射则按构造函数签名创建
- 如果上述情况都没匹配,则抛出异常。
-
applyAutomaticMappings :如果开启了自动映射则会按照自动映射的规则(忽略属性大小写差异)进行属性映射
-
applyPropertyMappings :根据规则对剩余属性进行映射
-
applyNestedResultMappings : 处理嵌套映射的属性。
下面我们详细来看上面的几个方法的具体实现:
2.2.1 createResultObject
createResultObject 实现如下:
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {this.useConstructorMappings = false; // reset previous mapping resultfinal List<Class<?>> constructorArgTypes = new ArrayList<>();final List<Object> constructorArgs = new ArrayList<>();// 根据 ResultMap 的属性通过反射方式创建一个对象(如果通过 <constructor>指定了构造参数 则注入构造参数)Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);// 对象不为空且没有对应的类型处理器if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();// 遍历所有属性for (ResultMapping propertyMapping : propertyMappings) {// issue gcode #109 && issue #149// 如果是嵌套结果集 && 并且开启了懒加载,则这里创建一个代理对象,等实际调用时才会触发获取逻辑if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);break;}}}// 标注是否使用了构造映射this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping resultreturn resultObject;}
2.2.2 applyAutomaticMappings
如果开启了自动映射则会按照自动映射的规则(忽略属性大小写差异)进行属性映射
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {// 获取开启自动映射的结果集List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);boolean foundValues = false;// 不为空则开进行映射if (!autoMapping.isEmpty()) {for (UnMappedColumnAutoMapping mapping : autoMapping) {final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);if (value != null) {foundValues = true;}if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {// gcode issue #377, call setter on nulls (value is not 'found')metaObject.setValue(mapping.property, value);}}}return foundValues;}
2.2.3 applyPropertyMappings
这里是对剩下的属性进行映射,在上面我们提到过嵌套映射存在内部嵌套和外部嵌套两种情况。这里则会对外部嵌套的情况做处理。具体如下:
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 获取使用 columnPrefix拼接后的列名final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);boolean foundValues = false;// 获取 ResultMap 的 reuslt 属性final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();// 遍历所有属性for (ResultMapping propertyMapping : propertyMappings) {// 获取拼接 columnPrefix 后 属性列名String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);// 如果当前属性存在嵌套的 ResultMap 则忽略该列,交由下面进行嵌套解析if (propertyMapping.getNestedResultMapId() != null) {// the user added a column attribute to a nested result map, ignore itcolumn = null;}// 如果当前查询有复合结果(嵌套映射时,可能出现一对一、一对多的情况) || 当前列匹配(property 与 column经过转换后一致) || 当前属性指定了 ResultSetif (propertyMapping.isCompositeResult()|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))|| propertyMapping.getResultSet() != null) {// 解析并获取属性对应的列值Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);// issue #541 make property optionalfinal String property = propertyMapping.getProperty();if (property == null) {continue;} else if (value == DEFERRED) {foundValues = true;continue;}if (value != null) {foundValues = true;}// value 不为空 || (配置 {mybatis.configuration.call-setters-on-nulls} 为 true && set 方法不为私有) if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {// gcode issue #377, call setter on nulls (value is not 'found')// 设置属性值metaObject.setValue(property, value);}}}return foundValues;}
可以看到上面的关键方法在于下面 getPropertyMappingValue 方法,具体实现如下:
// 获取属性映射的值private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 如果当前是嵌套属性if (propertyMapping.getNestedQueryId() != null) {// 获取嵌套属性查询的结果return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);} else if (propertyMapping.getResultSet() != null) {// 添加挂起的子关系addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?// 返回一个固定对象return DEFERRED;} else {// 最基础的解析使用指定的 TypeHandler 解析数据并返回。如 Long 使用 LongTypeHandler等final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();// 拼接前缀:即 prefix + columnNamefinal String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);// 获取处理结果并返回return typeHandler.getResult(rs, column);}}
上面我们可以看到,这里分成三种情况
- 外部嵌套:交由 getNestedQueryMappingValue 方法来处理
- 指定 ResultSet : 挂起子关系,等后续一起处理(不在本文分析内容)
- 最基础的解析:交由 TypeHandler 来获取结果集并返回对象
下面我们来看看 getNestedQueryMappingValue 嵌套解析的过程:
// 获取嵌套查询的结果集映射private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 获取嵌套映射id 即 select属性指定的查询语句final String nestedQueryId = propertyMapping.getNestedQueryId();final String property = propertyMapping.getProperty();// 获取嵌套映射指定的语句final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);// 获取参数类型final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();// 获取嵌套映射的参数final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);Object value = null;if (nestedQueryParameterObject != null) {final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);final Class<?> targetType = propertyMapping.getJavaType();// 如果结果已经被缓存if (executor.isCached(nestedQuery, key)) {// 延迟加载executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);value = DEFERRED;} else {final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);// 如果是懒加载则加载到 lazyLoader中并返回推迟加载对象if (propertyMapping.isLazy()) {lazyLoader.addLoader(property, metaResultObject, resultLoader);value = DEFERRED;} else {// 加载结果value = resultLoader.loadResult();}}}return value;}
2.2.4 applyNestedResultMappings
applyNestedResultMappings 则是针对内部嵌套进行处理,如下:
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {boolean foundValues = false;for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {final String nestedResultMapId = resultMapping.getNestedResultMapId();if (nestedResultMapId != null && resultMapping.getResultSet() == null) {try {// 获取拼接 parentPrefix 后的列名 :我们可以通过 <collection> <association> 的 columnPrefix 属性指定前缀,这里会进行前缀拼接final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);// 1. 获取嵌套映射对应的结果集final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);// 如果列前缀为空:一般情况下如果使用嵌套映射则会声明前缀if (resultMapping.getColumnPrefix() == null) {// try to fill circular reference only when columnPrefix// is not specified for the nested result map (issue #215)// 尝试获取祖先对象Object ancestorObject = ancestorObjects.get(nestedResultMapId);if (ancestorObject != null) {// 如果是新对象,则进行链接 : 当第一次处理当前嵌套映射时认为是新对象,可以简单认为没有放入 nestedResultObjects 缓存if (newObject) {// 链接对象linkObjects(metaObject, resultMapping, ancestorObject); // issue #385}continue;}}// 创建行的keyfinal CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);// 与父级 key 进行组合:final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);// 从缓存中获取该行对象Object rowValue = nestedResultObjects.get(combinedKey);boolean knownValue = rowValue != null;// 如果对象是集合类型,则判断是否需要初始化,需要则创建爱你instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory// 据notNullColumn属性, 检测是否有非空属性,如果全为空则没必要解析if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {// 获取映射结果rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);// 如果映射结果不为空 && 不是缓存对象 则链接对象// 这里的判断会引发一个问题 : 在嵌套映射时如果两个对象完全一致会被缓存命中从而不会链接对象,导致数据丢失,下面会讲if (rowValue != null && !knownValue) {linkObjects(metaObject, resultMapping, rowValue);foundValues = true;}}} catch (SQLException e) {throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);}}}return foundValues;}// 链接对象private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {// 必要的话初始化集合对象final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);// 如果集合对象不为空,则添加到集合对象中if (collectionProperty != null) {final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);targetMetaObject.add(rowValue);} else {// 否则的话保存属性到元数据中metaObject.setValue(resultMapping.getProperty(), rowValue);}}
这里需要注意的是由于 Mybatis 的 RowKey 是属性名 + 属性值拼接,在嵌套时如果两行数据完全一致,则第一行数据会被缓存,当处理第二行数据时,会被缓存命中从而不满足 rowValue != null && !knownValue
的判断条件,导致数据丢失。
2.4 storeObject
storeObject 方法将数据保存起来, 具体实现如下:
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {// 如果父 ResultMap 存在 (嵌套模式),则链接到 父 ResultMap 中 if (parentMapping != null) {linkToParents(rs, parentMapping, rowValue);} else {// 回调 resultHandler 来处理结果callResultHandler(resultHandler, resultContext, rowValue);}}private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {// 获取到父ResultMapping 中该属性的缓存keyCacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(), parentMapping.getForeignColumn());// 获取缓存的对象List<PendingRelation> parents = pendingRelations.get(parentKey);if (parents != null) {for (PendingRelation parent : parents) {if (parent != null && rowValue != null) {// 将当前对象注入到父级linkObjects(parent.metaObject, parent.propertyMapping, rowValue);}}}}private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {resultContext.nextResultObject(rowValue);// 调用ResultHandler#handleResult来处理结果,默认情况是 DefaultResultHandler,将结果保存到 DefaultResultHandler#list 中((ResultHandler<Object>) resultHandler).handleResult(resultContext);}
3. handleRowValuesForSimpleResultMap
该方法用来解析非嵌套映射情况,具体实现如下:
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();// 跳过执行行数据,由 RowBounds.offset 属性决定skipRows(resultSet, rowBounds);// 确定当前剩余数据满足条件,即此次拉取的数据量 < RowBounds.limmit 时 且 连接未关闭 且后续还有结果集,则再次获取while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 1. 解析 discriminator 属性ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 2. 获取行数据 Object rowValue = getRowValue(rsw, discriminatedResultMap, null);// 3. 保存映射后的数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}// 跳过指定的行数private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {rs.absolute(rowBounds.getOffset());}} else {for (int i = 0; i < rowBounds.getOffset(); i++) {if (!rs.next()) {break;}}}} // 是否应该获取更多列private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();}
这里我们可以看到 :
-
利用 RowBounds 是可以实现分页的功能的,但却是一个逻辑分页,因为所有数据都是已经加载到内存后再根据 RowBounds 的分页限制选择是否丢弃或继续获取,因此并不建议使用。
-
resolveDiscriminatedResultMap 方法实现了对
<discriminator >
标签的解析,并将<discriminator >
解析后的ResultMap 作为最终的 ResultMap 处理,上面已经介绍,不再赘述。 -
getRowValue 方法会根据 resultMap 解析并获取当前的行数据, 这个跟上面不同是个重载方法,如下:
// 将数据库查出来的数据转换为 Mapper Interface Method 返回的类型private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();// 1. 反射 Mapper Interface Method 返回的类型对象,这里尚未填充行数据Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// rowValue 不为空 && 没有针对 rowValue 类型的 TypeHandler if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 如果允许自动映射(可通过 <resultMap> 标签的 autoMapping 属性指定)if (shouldApplyAutomaticMappings(resultMap, false)) {// 2. 根据自动映射规则尝试映射,看是行数据是否能映射到对应的属性 (忽略大小写的映射)foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 3. 根据属性映射foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;foundValues = lazyLoader.size() > 0 || foundValues;// 如果 映射到了属性值 或者 配置了空数据返回实体类 (mybatis.configuration.return-instance-for-empty-row 属性指定)则 返回 rowValue, 否则返回空 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}// 返回映射后的实体类return rowValue;}
-
storeObject 方法会将处理后的行结果缓存起来。上面已经介绍,这里不再赘述。
至此整个解析过程已经结束。
以上:内容部分参考
https://www.jianshu.com/p/cdb309e2a209
https://zhuanlan.zhihu.com/p/526147349
https://blog.csdn.net/qq_40233503/article/details/94436578
https://blog.csdn.net/weixin_42893085/article/details/105105958
https://blog.csdn.net/weixin_40240756/article/details/108889127
https://www.cnblogs.com/hongshaozi/p/14160328.html
https://www.jianshu.com/p/05f643f27246
https://www.cnblogs.com/sanzao/p/11466496.html
https://juejin.cn/post/6844904127823085581
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正