美文网首页MyBatis+SpringMVC+SpringBootJava成长之路
MyBatis 类型处理器 TypeHandler 泛型擦除问题

MyBatis 类型处理器 TypeHandler 泛型擦除问题

作者: hdfg159 | 来源:发表于2021-05-28 19:52 被阅读0次

    MyBatis 类型处理器 TypeHandler 泛型擦除问题

    问题

    • Q:使用 TypeHandler 处理 List Map 等带泛型字段序列化 JSON 保存进去 MySQL 数据库时候发现没法反序列化还原

    • A:Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。因为 List 泛型字段 编译后擦除相关类型导致出现这个问题

    编写万能通用 JSON TypeHandler

    万能通用转 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;
    
    /**
     * 万能对象类型转换 json
     * 注意:这个反序列化会保存类的相关信息,请不要修改反序列化类的类名和路径
     *
     * @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);
            // 不使用科学计数
            MAPPER.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
            // null 值不输出(节省内存)
            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 转 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);
            }
        }
    
        /**
         * 转换对象
         *
         * @param json
         *         json数据
         * @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 字符串会保存泛型类的相关信息,导致保存的 JSON 存储内容过大
      • 因为保存泛型类的相关信息,反序列时候确保泛型类的路径和名称都是一致的(变化了会导致反序列化后信息丢失)

    【推荐】 使用 数组 代替 List

    泛型数组 转 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;
    
    /**
     * 数组类型转换 json
     *
     * 主要是用于对象数据 基础类型包装对象不建议用
     *
     * @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);
            // 不使用科学计数
            MAPPER.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
            // null 值不输出(节省内存)
            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 转 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);
            }
        }
    
        /**
         * 转换对象
         *
         * @param json
         *         json数据
         * @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);
        }
    }
    
    
    • 此方案也可以完美解决泛型丢失的问题
    • 这种方案是相对比较好的,不会像上面那样保存一些泛型类的信息,保存数据就是普通json字符串,不会有反序列化泛型丢失的问题

    其他方案

    方案各种各样,下面列举一下其他方案

    • 自定义一个指定泛型的集合类替代 List<T>
      • 去包装一下 List<Generic> 并指定泛型类型
      • 这种方式实际就是普通JSON和对象转换
    public class PackList extends List<Generic> {
        // ...
    }
    
    • 写一个通用Json TypeHandler , 每次自定义一个新的 TypeHandler 处理

    数组序列化 JSON 使用案例

    使用方式

    下面方式任选其中一种

    • 常规可以在 ResultMap 指定相应的 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 标注对应类型

    Integer 数组 序列化 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 数组类型转换 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 数组 序列化 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 数组类型转换 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());
        }
    }
    
    

    相关文章

      网友评论

        本文标题:MyBatis 类型处理器 TypeHandler 泛型擦除问题

        本文链接:https://www.haomeiwen.com/subject/njfesltx.html