Mybatis 源码 ④ :TypeHandler
文章目录
- 一、前言
- 二、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
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正
相关文章:

Mybatis 源码 ④ :TypeHandler
文章目录 一、前言二、DefaultParameterHandler1. DefaultParameterHandler#setParameters1.1 UnknownTypeHandler1.2 自定义 TypeHandler 三、DefaultResultSetHandler1. hasNestedResultMaps2. handleRowValuesForNestedResultMap2.1 resolveDiscriminatedResultMap2.2 creat…...

RabbitMQ和JMeter,一个完美的组合!优化你的中间件处理方式
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息中间件,它是基于Erlang语言编写的,并发能力强,性能好,是目前主流的消息队列中间件之一。 RabbitMQ的安装可参照官网( https://www.rabbitmq.c…...
WARNING: IPv4 forwarding is disabled. Networking will not work
当我在运行某条语句的时候 docker run -it -p 30001:22 --namecentos-ssh centos /bin/bash 提示 WARNING: IPv4 forwarding is disabled. Networking will not work. 解决: vim /usr/lib/sysctl.d/00-system.conf net.ipv4.ip_forward1 systemctl restart networ…...
SpringBoot复习:(40)@EnableConofigurationProperties注解的用法
一、配置文件: server.port9123 二、配置类: package cn.edu.tju.config;import com.mysql.fabric.Server; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigu…...

Live Market是如何做跨境客户服务的?哪些技术赋能?
在面对不同的海外市场和用户群体时,如何进行有效地出海营销是跨境商家面临的挑战。其中消费者服务管理和卖家保障尤其关键,如何做好客户服务管理?包括处理好客户投诉,提升消费者满意度是所有跨境商家和品牌独立站卖家非常重视的问题。 在数字化浪潮席卷之下&#…...

2023年7月京东洗衣机行业品牌销售排行榜(京东数据分析软件)
2023年上半年,洗衣机市场表现平淡,同环比来看出货量都有一定程度的下滑。7月份,洗衣机市场仍未改变这一下滑态势。 根据鲸参谋电商数据分析平台的相关数据显示,7月份,京东平台洗衣机的销量为109万,环比下降…...
【0214】postgres后端进程session退出,如何通过日志分析其会话信息
文章目录 1. postgres进程session退出2. 开启日志记录postgres进程会话状态3. postgres进程会话结束,记录日志的实现原理1. postgres进程session退出 默认情况下,新建一个postgres后端进程会话(session),或是postgres进程正常/异常退出时,日志中没有很明显的记录用于说明…...

Rust 重载运算符|复数结构的“加减乘除”四则运算
复数 基本概念 复数定义 由实数部分和虚数部分所组成的数,形如a+bi 。 其中a、b为实数,i 为“虚数单位”,i -1,即虚数单位的平方等于-1。 a、b分别叫做复数a+bi的实部和虚部。 当b0时,a&…...
Oracle删除表空间
1.检查表空间状态 SELECT tablespace_name, status FROM dba_tablespaces;备注:tablespace_name表示删除表空间的名称,status为表空间的状态。如果状态为ONLINE,表示表空间当前正在使用,不能被删除。 2.关闭表空间 ALTER TABLE…...

Mysql - 配置Mysql主从复制-keepalived高可用-读写分离集群
目录 高可用: 为什么需要高可用呢? 高可用的主要作用: keepalived是什么?它用在哪里? 什么是VRRP协议,它的作用是什么? 搭建一个基于keepalived的高可用Mysql主从复制读写分离集群 一、项…...
Qt QLineEdit输入时限制,采用正则表达式
QLineEdit 正则 序言使用方法正则表达式使用例子 序言 老是有人在群里问这个,所以我干脆写一篇方便予人查看,很简单的小功能。 使用方法 Qt5 #include <QRegExpValidator> //#include "qvalidator.h"ui->lineEdit->setValida…...

【CSS】文本效果
文本溢出、整字换行、换行规则以及书写模式 代码: <style> p.test1 {white-space: nowrap; width: 200px; border: 1px solid #000000;overflow: hidden;text-overflow: clip; }p.test2 {white-space: nowrap; width: 200px; border: 1px solid #000000;ove…...

Django快速上手,写一个简单的页面,快来看看吧~
还没有安装Django,以及不会创建Django项目的小伙伴,可以先看看博主这篇文章:http://t.csdn.cn/Ly7yM 目录 1、项目创建完成后,需要注意的点: 2、创建app 3、app的各个目录的作用 4、快速上手 4.1、注册app 4.2、U…...
【Express.js】数据库初始化
数据库初始化 在软件开发阶段和测试阶段,为了方便调试,我们通常会进行一系列的数据库初始化操作,比如重置数据表,插入记录等等,或者在部署阶段进行数据初始化的操作 根据前面章节介绍过的 knex.js 和 sequelize.js&…...

【数理知识】三维空间旋转矩阵的欧拉角表示法,四元数表示法,两者之间的转换,Matlab 代码实现
序号内容1【数理知识】自由度 degree of freedom 及自由度的计算方法2【数理知识】刚体 rigid body 及刚体的运动3【数理知识】刚体基本运动,平动,转动4【数理知识】向量数乘,内积,外积,matlab代码实现5【数理知识】最…...
【业务功能篇63】Springboot聊聊 过滤器和拦截器
过滤器的场景:过滤器通常用于对数据或资源进行筛选、修改或转换的场景。例如,在一个电子商务网站中,用户进行商品搜索时,你可以使用过滤器来过滤特定的商品类别、价格范围或其他条件,以便用户仅看到符合筛选条件的结果…...

提高学生学习效率的模拟考试系统
在如今竞争激烈的社会环境下,提高学生的学习效率显得尤为重要。为了帮助学生评估自己的学习水平并提供有针对性的学习建议,开发一款模拟考试系统是非常必要的。 一、学生信息录入 模拟考试系统首先需要学生信息录入功能。学生可以通过一个简单的表单填…...
解决QWebEngineView在linux下加载本地html失败的问题
通常我们使用QWebEngineView加载本地html文件时,是通过 void load(const QUrl &url) void setUrl(const QUrl &url) 两个函数,传入html的相对或绝对路径,进行加载。 而在linux(uos x86)下运行时,却发现加载失败…...

如何使用Redis实现内容推送功能
导读 在日常使用中,我们经常能看见内容推送功能。 常见的场景有,比如你在bilibili关注了某个up主,当up主发布视频后,就会推送到你的收件箱或者是动态中,让粉丝能够及时得知所关注的人发布了内容。 又比如朋友圈&…...

怎么对视频进行压缩?
怎么对视频进行压缩?视频压缩,我们都知道是将视频文件进行压缩变小的过程,是我们日常办公中较为常用的手段。现如今,在视频技术不断发展与创新的基础上,视频分辨率也在不断提高,进而导致文件占有量也非常大…...

wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...