Mybatis的BaseTypeHandler<T>对于泛型集合List<T>与Json数组反序列化的一种方案, 可支持嵌套泛型类型List<List<T>>等

背景

数据库持久化时有时需要将一些与其他表有级联关系但在查找时较为不便的实体作为数据库字段保存在主表中, 一般会将其转为Json入库, 作为Json保存则有对象和数组两种形式, 如果在反序列化时不指定类型, 则在赋值时会抛出java.lang.ClassCastException异常, 因此对于对象的反序列化还需要获取目标类型

针对对象类型, 继承BaseTypeHandler<T>后通过含Class<?>的构造方法可以获取原属性的类型, 可以参考Mybatis-plus包中的AbstractJsonTypeHandler<T>

对于Json数组的反序列化使用单个对象的方式是不行的, 通过构造方法获取的集合类没有泛型信息, 相当于传递List.class, 这样就无法获取泛型类型, 对于集合类型, 由于泛型擦除, 在赋值时并不会抛出异常(测试发现Jackson序列化时疑似也不会抛出异常), 但是在取元素时由于内部会转换类型, 同样会抛出java.lang.ClassCastException异常, 即堆污染; 另外尝试了几种方法:

最后发现通过声明的集合类型字段获取类对象后可以获取泛型类型, 那么就可以记录每个实体类中List<T>类型的属性的泛型类型, 通过重写BaseTypeHandler<T>方法中的参数java.sql.ResultSet获取元信息java.sql.ResultSetMetaData, 可以获取数据库表和字段名, 最后通过名称获取泛型类, 即可实现, 较为简便

实现

继承BaseTypeHandler<T>

同样是继承BaseTypeHandler<T>, 重写抽象方法

@MappedTypes(List.class)
public class DynamicJsonArrayTypeHandler extends BaseTypeHandler<List<?>> {
}

此处无需再声明泛型类型了

获取实体类集合类型字段指定的泛型类对象

此处通过Mybatis-plus的com.baomidou.mybatisplus.core.metadata.TableInfoHelper获取com.baomidou.mybatisplus.core.metadata.TableInfo表信息, 可以获取目标字段对象(java.lang.reflect.Field)获取泛型类对象, 如果没有依赖Mybatis-plus, 可以自己实现实体类扫描, 注册字段信息, 参考Mybatis或Spring的扫描方式

private static final Map<String, Map<String, Class<?>>> LIST_TYPE_FIELD_GENERIC_METADATA = Maps.newConcurrentMap();

private static Class<?> findGenericRawType(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException {
    return LIST_TYPE_FIELD_GENERIC_METADATA.computeIfAbsent(
                        resultSetMetaData.getTableName(columnIndex),
                        k -> Optional.ofNullable(TableInfoHelper.getTableInfo(k))
                                .orElseThrow(() -> Exceptions.UNKNOWN_TABLE_METADATA.supply("对应表名不存在: {}", k))
                                // 读取表字段信息
                                .getFieldList().stream()
                                // 筛选指定泛型类型的List及其子类
                                .filter(f -> List.class.isAssignableFrom(f.getField().getType()))
                                .filter(f -> f.getField().getGenericType() instanceof ParameterizedType)
                                // key: 原始字段名, value: List类型字段指定的泛型类
                                .collect(Collectors.toMap(
                                        TableFieldInfo::getColumn,
                                        f -> Optional.ofNullable(((ParameterizedType) f.getField().getGenericType()).getActualTypeArguments()[0])
                                                // 如果是嵌套泛型类型则读取其原始类型
                                                .map(
                                                        t -> t instanceof ParameterizedType
                                                                ? (Class<?>) ((ParameterizedType) t).getRawType()
                                                                : (Class<?>) t
                                                )
                                                .orElse(Object.class),
                                        // 同名属性取第一个
                                        (v1, v2) -> v1,
                                        Maps::newConcurrentMap
                                ))
                )
                .getOrDefault(resultSetMetaData.getColumnName(columnIndex), Object.class);
}

反序列化

通过Json库对Json字符串反序列化为对象

@Nullable
private static List<?> asList(ResultSetMetaData resultSetMetaData, int columnIndex, String value) throws SQLException {
    // 必须指定类型, 否则会造成堆污染, 抛出java.lang.ClassCastException
    return value == null ? null : JSONArray.parseArray(value, findGenericRawType(resultSetMetaData, columnIndex));
}

完整代码

@MappedTypes(List.class)
public class DynamicJsonArrayTypeHandler extends BaseTypeHandler<List<?>> {
    /**
     * mp扫描到的表对应实体类中List及其子类类型属性的泛型信息
     */
    private static final Map<String, Map<String, Class<?>>> LIST_TYPE_FIELD_GENERIC_METADATA = Maps.newConcurrentMap();

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<?> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter == null ? null : JSONArray.toJSONString(parameter));
    }

    @Override
    public List<?> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return asList(rs.getMetaData(), rs.findColumn(columnName), rs.getString(columnName));
    }

    @Override
    public List<?> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return asList(rs.getMetaData(), columnIndex, rs.getString(columnIndex));
    }

    @Override
    public List<?> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return asList(cs.getMetaData(), columnIndex, cs.getString(columnIndex));
    }

    @Nullable
    private static List<?> asList(ResultSetMetaData resultSetMetaData, int columnIndex, String value) throws SQLException {
        // 必须指定类型, 否则会造成堆污染, 抛出java.lang.ClassCastException
        return value == null ? null : JSONArray.parseArray(value, findGenericRawType(resultSetMetaData, columnIndex));
    }

    private static Class<?> findGenericRawType(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException {
        return LIST_TYPE_FIELD_GENERIC_METADATA.computeIfAbsent(
                        resultSetMetaData.getTableName(columnIndex),
                        k -> Optional.ofNullable(TableInfoHelper.getTableInfo(k))
                                .orElseThrow(() -> Exceptions.UNKNOWN_TABLE_METADATA.supply("对应表名不存在: {}", k))
                                // 读取表字段信息
                                .getFieldList().stream()
                                // 筛选指定泛型类型的List及其子类
                                .filter(f -> List.class.isAssignableFrom(f.getField().getType()))
                                .filter(f -> f.getField().getGenericType() instanceof ParameterizedType)
                                // key: 原始字段名, value: List类型字段指定的泛型类
                                .collect(Collectors.toMap(
                                        TableFieldInfo::getColumn,
                                        f -> Optional.ofNullable(((ParameterizedType) f.getField().getGenericType()).getActualTypeArguments()[0])
                                                // 如果是嵌套泛型类型则读取其原始类型
                                                .map(
                                                        t -> t instanceof ParameterizedType
                                                                ? (Class<?>) ((ParameterizedType) t).getRawType()
                                                                : (Class<?>) t
                                                )
                                                .orElse(Object.class),
                                        // 同名属性取第一个
                                        (v1, v2) -> v1,
                                        Maps::newConcurrentMap
                                ))
                )
                .getOrDefault(resultSetMetaData.getColumnName(columnIndex), Object.class);
    }
}

使用

Mybatis-plus在实体类的属性上声明并且在实体类@TableName中声明autoResultMap = true

@TableName(value = "target_table", autoResultMap = true)
public class TargetTable implements Serializable {
    @TableField(typeHandler = DynamicJsonArrayTypeHandler.class)
    private List<TargetType> jsonField;
}

Mybatis通过其他方式注册, 没有验证有效性

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值