MyBatis 類型處理器 TypeHandler 泛型擦除問題
問題
Q:使用 TypeHandler 處理
List
Map
等帶泛型字段序列化 JSON 保存進(jìn)去 MySQL 數(shù)據(jù)庫時候發(fā)現(xiàn)沒法反序列化還原A:Java語言的泛型采用的是擦除法實現(xiàn)的偽泛型,泛型信息(類型變量、參數(shù)化類型)編譯之后通通被除掉了逼庞。因為 List 泛型字段 編譯后擦除相關(guān)類型導(dǎo)致出現(xiàn)這個問題
編寫萬能通用 JSON TypeHandler
萬能通用轉(zhuǎn) JSON TypeHandler 代碼
package io.github.hdfg159.common.handler.typehandler;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
/**
* 萬能對象類型轉(zhuǎn)換 json
* 注意:這個反序列化會保存類的相關(guān)信息众羡,請不要修改反序列化類的類名和路徑
*
* @author hdfg159
* @date 2021/5/26 17:00
*/
@Slf4j
@MappedJdbcTypes(value = {JdbcType.VARCHAR}, includeNullJdbcType = true)
public class GenericJacksonJsonTypeHandler<E> extends BaseTypeHandler<E> {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
// 未知字段忽略
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 不使用科學(xué)計數(shù)
MAPPER.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
// null 值不輸出(節(jié)省內(nèi)存)
MAPPER.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
// 配置輸出 反序列化類名的形式
MAPPER.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
}
private final Class<E> type;
public GenericJacksonJsonTypeHandler(Class<E> type) {
Objects.requireNonNull(type);
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (Objects.isNull(parameter)) {
ps.setString(i, null);
return;
}
ps.setString(i, toJson(parameter));
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toObject(rs.getString(columnName), type);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toObject(rs.getString(columnIndex), type);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toObject(cs.getString(columnIndex), type);
}
/**
* object 轉(zhuǎn) json
*
* @param obj
* 對象
*
* @return String json字符串
*/
private String toJson(E obj) {
if (Objects.isNull(obj)) {
return null;
}
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("mybatis column to json error,obj:" + obj, e);
}
}
/**
* 轉(zhuǎn)換對象
*
* @param json
* json數(shù)據(jù)
* @param clazz
* 類
*
* @return E
*/
private E toObject(String json, Class<E> clazz) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("mybatis column json to object error,json:{}", json, e);
return null;
}
}
}
- 此方案可以完美解決泛型丟失的問題(同時也是 Redis 序列化 JSON 常用方式)
- 缺點明顯
- 序列化 JSON 字符串會保存泛型類的相關(guān)信息,導(dǎo)致保存的 JSON 存儲內(nèi)容過大
- 因為保存泛型類的相關(guān)信息襟衰,反序列時候確保泛型類的路徑和名稱都是一致的(變化了會導(dǎo)致反序列化后信息丟失)
【推薦】 使用 數(shù)組 代替 List
泛型數(shù)組 轉(zhuǎn) JSON TypeHandler 代碼
package io.github.hdfg159.common.handler.typehandler;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import java.lang.reflect.Array;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Objects;
/**
* 數(shù)組類型轉(zhuǎn)換 json
*
* 主要是用于對象數(shù)據(jù) 基礎(chǔ)類型包裝對象不建議用
*
* @author hdfg159
* @date 2021/5/26 17:00
*/
@Slf4j
@MappedJdbcTypes(value = {JdbcType.VARCHAR}, includeNullJdbcType = true)
public class ArrayObjectJsonTypeHandler<E> extends BaseTypeHandler<E[]> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String STRING_JSON_ARRAY_EMPTY = "[]";
static {
// 未知字段忽略
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 不使用科學(xué)計數(shù)
MAPPER.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
// null 值不輸出(節(jié)省內(nèi)存)
MAPPER.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
}
private final Class<E[]> type;
public ArrayObjectJsonTypeHandler(Class<E[]> type) {
Objects.requireNonNull(type);
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E[] parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, toJson(parameter));
}
@Override
public E[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toObject(rs.getString(columnName), type);
}
@Override
public E[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toObject(rs.getString(columnIndex), type);
}
@Override
public E[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toObject(cs.getString(columnIndex), type);
}
/**
* object 轉(zhuǎn) json
*
* @param obj
* 對象
*
* @return String json字符串
*/
private String toJson(E[] obj) {
if (ArrayUtils.isEmpty(obj)) {
return STRING_JSON_ARRAY_EMPTY;
}
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("mybatis column to json error,obj:" + Arrays.toString(obj), e);
}
}
/**
* 轉(zhuǎn)換對象
*
* @param json
* json數(shù)據(jù)
* @param clazz
* 類
*
* @return E
*/
private E[] toObject(String json, Class<E[]> clazz) {
if (json == null) {
return null;
}
if (StringUtils.isBlank(json)) {
return newArray(clazz);
}
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("mybatis column json to object error,json:{}", json, e);
return newArray(clazz);
}
}
private E[] newArray(Class<E[]> clazz) {
return (E[]) Array.newInstance(clazz.getComponentType(), 0);
}
}
- 此方案也可以完美解決泛型丟失的問題
- 這種方案是相對比較好的,不會像上面那樣保存一些泛型類的信息,保存數(shù)據(jù)就是普通json字符串找爱,不會有反序列化泛型丟失的問題
其他方案
方案各種各樣,下面列舉一下其他方案
- 自定義一個指定泛型的集合類替代
List<T>
- 去包裝一下
List<Generic>
并指定泛型類型 - 這種方式實際就是普通JSON和對象轉(zhuǎn)換
- 去包裝一下
public class PackList extends List<Generic> {
// ...
}
- 寫一個通用Json TypeHandler , 每次自定義一個新的 TypeHandler 處理
- 每次自定義一個新的 TypeHandler指定相應(yīng)類型泡孩,覆寫相應(yīng)的解析方法车摄,不推薦
- 可以參考 MyBatis Plus 做法
數(shù)組序列化 JSON 使用案例
使用方式
下面方式任選其中一種
- 常規(guī)可以在 ResultMap 指定相應(yīng)的 TypeHandler
<resultMap id="BaseResultMap" type="io.github.hdfg159.system.domain.TypeHandlerTest">
<!-- 省略部分代碼 -->
<!-- ArrayObjectJsonTypeHandler 使用 -->
<result column="j2" property="j2"
typeHandler="io.github.hdfg159.common.handler.typehandler.ArrayObjectJsonTypeHandler"/>
<!-- GenericJacksonJsonTypeHandler 使用 -->
<result column="a2" property="a2"
typeHandler="io.github.hdfg159.common.handler.typehandler.GenericJacksonJsonTypeHandler"/>
<!-- 省略部分代碼 -->
</resultMap>
如果是使用
Mybatis Plus
,請查閱 參考文檔
- 直接繼承
ArrayObjectJsonTypeHandler
- 使用
@Component
聲明成 Spring Bean -
@MappedTypes
和@MappedJdbcTypes
標(biāo)注對應(yīng)類型
- 使用
Integer 數(shù)組 序列化 JSON 案例
package io.github.hdfg159.common.handler.typehandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;
/**
* Integer 數(shù)組類型轉(zhuǎn)換 json
*
* @author hdfg159
* @date 2021/5/26 17:00
*/
@Slf4j
@Component
@MappedTypes(value = {Integer[].class})
@MappedJdbcTypes(value = {JdbcType.VARCHAR}, includeNullJdbcType = true)
public class IntegerArrayJsonTypeHandler extends ArrayObjectJsonTypeHandler<Integer> {
public IntegerArrayJsonTypeHandler() {
super((Class<Integer[]>) new Integer[0].getClass());
}
}
Long 數(shù)組 序列化 JSON 案例
package io.github.hdfg159.common.handler.typehandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.stereotype.Component;
/**
* Long 數(shù)組類型轉(zhuǎn)換 json
*
* @author hdfg159
* @date 2021/5/26 17:00
*/
@Slf4j
@Component
@MappedTypes(value = {int[].class})
@MappedJdbcTypes(value = {JdbcType.VARCHAR}, includeNullJdbcType = true)
public class LongArrayJsonTypeHandler extends ArrayObjectJsonTypeHandler<Long> {
public LongArrayJsonTypeHandler() {
super((Class<Long[]>) new Long[0].getClass());
}
}