背景
数据库持久化时有时需要将一些与其他表有级联关系但在查找时较为不便的实体作为数据库字段保存在主表中, 一般会将其转为Json入库, 作为Json保存则有对象和数组两种形式, 如果在反序列化时不指定类型, 则在赋值时会抛出java.lang.ClassCastException
异常, 因此对于对象的反序列化还需要获取目标类型
针对对象类型, 继承
BaseTypeHandler<T>
后通过含Class<?>
的构造方法可以获取原属性的类型, 可以参考Mybatis-plus包中的AbstractJsonTypeHandler<T>
对于Json数组的反序列化使用单个对象的方式是不行的, 通过构造方法获取的集合类没有泛型信息, 相当于传递List.class
, 这样就无法获取泛型类型, 对于集合类型, 由于泛型擦除, 在赋值时并不会抛出异常(测试发现Jackson序列化时疑似也不会抛出异常), 但是在取元素时由于内部会转换类型, 同样会抛出java.lang.ClassCastException
异常, 即堆污染; 另外尝试了几种方法:
- 参考MybatisPlus 自定义 TypeHandler 映射JSON类型为List: 继承
BaseTypeHandler<T>
定义抽象类, 添加抽象方法, 让子类返回TypeReference<T>
, 这是可以实现的, 但是这种实现每种类型都需要继承抽象类, 较为不便 - 参考mybatis:自定义实现拦截器插件Interceptor, MyBatis插件(拦截器)其实不难,我们使用通俗易懂的话来深入理解插件原理: 尝试通过拦截器, 拦截赋值行为, 但是找不到切入点或不生效, 且实现较为麻烦
最后发现通过声明的集合类型字段获取类对象后可以获取泛型类型, 那么就可以记录每个实体类中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通过其他方式注册, 没有验证有效性