🌺The Begin🌺点点关注,收藏不迷路🌺

一、背景:一个隐藏了多年的Bug

上周五下午,测试同学跑过来跟我说:“老王,有个接口返回的数据不对,JSON字段解析出来是空的。”

我赶紧看了下代码,是一个存了JSON数组的字段:

@Data
public class UserConfig {
    private Long id;
    private String userId;
    // 存的是JSON数组,比如 ["admin", "editor", "viewer"]
    private List<String> roles;  
    private List<Long> permissionIds;
}

对应的Mapper配置:

<resultMap id="userConfigMap" type="UserConfig">
    <id property="id" column="id"/>
    <result property="userId" column="user_id"/>
    <!-- 这里用了自定义的JSON处理器 -->
    <result property="roles" column="roles" typeHandler="JsonListTypeHandler"/>
    <result property="permissionIds" column="permission_ids" typeHandler="JsonListTypeHandler"/>
</resultMap>

看起来没问题啊,JsonListTypeHandler我写了很久了,一直用得好好的。但调试后发现一个诡异的现象:roles能正常解析,permissionIds却是空的

折腾了一下午,终于找到了原因:JsonListTypeHandler在处理List<String>List<Long>时,泛型信息丢失了!

二、问题分析:泛型擦除带来的困扰

2.1 泛型擦除是什么?

Java的泛型是编译期实现的,运行时会被擦除。也就是说:

// 编译时
List<String> roles = new ArrayList<>();
List<Long> permissionIds = new ArrayList<>();

// 运行时,变成了
List roles = new ArrayList();
List permissionIds = new ArrayList();

这就是为什么在TypeHandler里,我们拿不到List中的元素类型。

2.2 问题复现

先来看一下最初有问题的代码:

@Slf4j
public class JsonListTypeHandler extends BaseTypeHandler<List> {
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    List parameter, JdbcType jdbcType) throws SQLException {
        try {
            ps.setString(i, OBJECT_MAPPER.writeValueAsString(parameter));
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON序列化失败", e);
        }
    }
    
    @Override
    public List getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        if (json == null || json.isEmpty()) {
            return null;
        }
        try {
            // 这里出了问题!没有类型信息,只能转成List<Object>
            return OBJECT_MAPPER.readValue(json, List.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON解析失败", e);
        }
    }
    
    // ... 其他方法省略
}

问题就出在readValue(json, List.class)这里。由于不知道List里的元素类型,Jackson只能把它解析成List<Object>,里面的数字变成了Integer,字符串变成了String。

当MyBatis尝试把这个List<Object>赋值给List<Long>时,类型不匹配,结果就是null!

反序列化

赋值给

类型不匹配

JSON字符串

List<Object>

List<Long>

null

三、解决方案:三种思路对比

3.1 方案一:在XML中指定类型(可行但繁琐)

<result property="permissionIds" column="permission_ids" 
        javaType="list" ofType="java.lang.Long"
        typeHandler="com.example.handler.JsonListTypeHandler"/>

优点:不用改代码
缺点:每个用到的地方都要配,太繁琐了

3.2 方案二:通用的泛型TypeHandler(优雅)

利用构造器传入Class信息:

public class GenericListTypeHandler<T> extends BaseTypeHandler<List<T>> {
    
    private final Class<T> elementClass;
    
    public GenericListTypeHandler(Class<T> elementClass) {
        this.elementClass = elementClass;
    }
    
    // ... 实现方法
}

优点:一个类搞定所有类型
缺点:MyBatis默认调用无参构造器,需要额外配置

3.3 方案三:每个类型一个Handler(简单直接)

public class StringListTypeHandler extends BaseTypeHandler<List<String>> {}
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {}
public class IntegerListTypeHandler extends BaseTypeHandler<List<Integer>> {}

优点:简单,配置方便
缺点:代码重复,类型多了要写很多类

经过权衡,我选择了方案二 + 方案三的组合:写一个通用的基类,再为常用类型提供具体实现类

四、最终实现:优雅的解决方案

4.1 整体设计

使用方式

具体实现

基类

提供通用逻辑

继承

继承

继承

继承

指定

指定

AbstractListTypeHandler

序列化/反序列化

StringListTypeHandler

LongListTypeHandler

IntegerListTypeHandler

自定义ListTypeHandler

XML配置

注解配置

4.2 基类实现

先写一个抽象基类,把公共逻辑抽出来:

package com.example.mybatis.handler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * 抽象列表类型处理器
 * 子类只需实现 specificType() 方法提供类型信息
 */
@Slf4j
public abstract class AbstractListTypeHandler<T> extends BaseTypeHandler<List<T>> {
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
    static {
        // 配置ObjectMapper
        OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 可以添加更多配置,比如日期格式等
    }
    
    /**
     * 子类提供具体的类型信息
     */
    protected abstract TypeReference<List<T>> specificType();
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    List<T> parameter, JdbcType jdbcType) throws SQLException {
        try {
            String json = OBJECT_MAPPER.writeValueAsString(parameter);
            ps.setString(i, json);
            log.debug("JSON序列化: {}", json);
        } catch (JsonProcessingException e) {
            log.error("JSON序列化失败, 参数: {}", parameter, e);
            throw new RuntimeException("JSON序列化失败", e);
        }
    }
    
    @Override
    public List<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parseJson(json);
    }
    
    @Override
    public List<T> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJson(json);
    }
    
    @Override
    public List<T> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJson(json);
    }
    
    /**
     * 解析JSON字符串
     */
    private List<T> parseJson(String json) {
        if (json == null || json.trim().isEmpty()) {
            return null;
        }
        
        try {
            List<T> result = OBJECT_MAPPER.readValue(json, specificType());
            log.debug("JSON解析成功: {} -> {}", json, result);
            return result;
        } catch (IOException e) {
            log.error("JSON解析失败, json: {}", json, e);
            throw new RuntimeException("JSON解析失败", e);
        }
    }
    
    /**
     * 获取ObjectMapper实例(供子类使用)
     */
    protected ObjectMapper getObjectMapper() {
        return OBJECT_MAPPER;
    }
}

4.3 具体实现类

为常用类型提供具体的实现类:

package com.example.mybatis.handler;

import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * String列表处理器
 */
@Component
public class StringListTypeHandler extends AbstractListTypeHandler<String> {
    
    @Override
    protected TypeReference<List<String>> specificType() {
        return new TypeReference<List<String>>() {};
    }
}
package com.example.mybatis.handler;

import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Long列表处理器
 */
@Component
public class LongListTypeHandler extends AbstractListTypeHandler<Long> {
    
    @Override
    protected TypeReference<List<Long>> specificType() {
        return new TypeReference<List<Long>>() {};
    }
}
package com.example.mybatis.handler;

import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Integer列表处理器
 */
@Component
public class IntegerListTypeHandler extends AbstractListTypeHandler<Integer> {
    
    @Override
    protected TypeReference<List<Integer>> specificType() {
        return new TypeReference<List<Integer>>() {};
    }
}
package com.example.mybatis.handler;

import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 自定义对象列表处理器
 * 比如处理 List<User> 这样的复杂对象
 */
@Component
public class UserListTypeHandler extends AbstractListTypeHandler<User> {
    
    @Override
    protected TypeReference<List<User>> specificType() {
        return new TypeReference<List<User>>() {};
    }
}

4.4 另一种实现:使用JavaType

除了TypeReference,还可以用Jackson的JavaType:

package com.example.mybatis.handler;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * 基于JavaType的列表处理器
 */
@Slf4j
public abstract class AbstractJavaTypeListHandler<T> extends BaseTypeHandler<List<T>> {
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final JavaType javaType;
    
    static {
        OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    }
    
    public AbstractJavaTypeListHandler(Class<T> elementClass) {
        // 构造 List<T> 类型的 JavaType
        this.javaType = OBJECT_MAPPER.getTypeFactory()
                .constructCollectionType(List.class, elementClass);
        log.info("初始化JavaType: {}", javaType);
    }
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    List<T> parameter, JdbcType jdbcType) throws SQLException {
        try {
            String json = OBJECT_MAPPER.writeValueAsString(parameter);
            ps.setString(i, json);
        } catch (IOException e) {
            throw new RuntimeException("JSON序列化失败", e);
        }
    }
    
    @Override
    public List<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parseJson(json);
    }
    
    @Override
    public List<T> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJson(json);
    }
    
    @Override
    public List<T> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJson(json);
    }
    
    private List<T> parseJson(String json) {
        if (json == null || json.isEmpty()) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(json, javaType);
        } catch (IOException e) {
            throw new RuntimeException("JSON解析失败", e);
        }
    }
}

// 使用时需要传入元素类型
public class LongListHandler extends AbstractJavaTypeListHandler<Long> {
    public LongListHandler() {
        super(Long.class);
    }
}

五、使用方式

5.1 XML配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserConfigMapper">
    
    <resultMap id="BaseResultMap" type="com.example.entity.UserConfig">
        <id property="id" column="id"/>
        <result property="userId" column="user_id"/>
        <!-- 使用String列表处理器 -->
        <result property="roles" 
                column="roles" 
                typeHandler="com.example.mybatis.handler.StringListTypeHandler"/>
        <!-- 使用Long列表处理器 -->
        <result property="permissionIds" 
                column="permission_ids" 
                typeHandler="com.example.mybatis.handler.LongListTypeHandler"/>
        <!-- 使用自定义对象列表处理器 -->
        <result property="users" 
                column="users" 
                typeHandler="com.example.mybatis.handler.UserListTypeHandler"/>
    </resultMap>
    
    <select id="selectById" resultMap="BaseResultMap">
        select * from user_config where id = #{id}
    </select>
    
    <insert id="insert" parameterType="com.example.entity.UserConfig">
        insert into user_config (
            user_id, 
            roles, 
            permission_ids, 
            users
        ) values (
            #{userId},
            #{roles, typeHandler=com.example.mybatis.handler.StringListTypeHandler},
            #{permissionIds, typeHandler=com.example.mybatis.handler.LongListTypeHandler},
            #{users, typeHandler=com.example.mybatis.handler.UserListTypeHandler}
        )
    </insert>
    
    <update id="update" parameterType="com.example.entity.UserConfig">
        update user_config
        set roles = #{roles, typeHandler=com.example.mybatis.handler.StringListTypeHandler},
            permission_ids = #{permissionIds, typeHandler=com.example.mybatis.handler.LongListTypeHandler},
            users = #{users, typeHandler=com.example.mybatis.handler.UserListTypeHandler}
        where id = #{id}
    </update>
</mapper>

5.2 注解配置(MyBatis-Plus)

如果使用MyBatis-Plus,可以用注解:

package com.example.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.mybatis.handler.LongListTypeHandler;
import com.example.mybatis.handler.StringListTypeHandler;
import com.example.mybatis.handler.UserListTypeHandler;
import lombok.Data;

import java.util.List;

@Data
@TableName("user_config")
public class UserConfig {
    
    private Long id;
    private String userId;
    
    @TableField(typeHandler = StringListTypeHandler.class)
    private List<String> roles;
    
    @TableField(typeHandler = LongListTypeHandler.class)
    private List<Long> permissionIds;
    
    @TableField(typeHandler = UserListTypeHandler.class)
    private List<User> users;
}

5.3 纯MyBatis注解方式

package com.example.mapper;

import com.example.entity.UserConfig;
import com.example.mybatis.handler.LongListTypeHandler;
import com.example.mybatis.handler.StringListTypeHandler;
import com.example.mybatis.handler.UserListTypeHandler;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserConfigMapper {
    
    @Results(id = "userConfigMap", value = {
        @Result(property = "id", column = "id"),
        @Result(property = "userId", column = "user_id"),
        @Result(property = "roles", column = "roles", 
                typeHandler = StringListTypeHandler.class),
        @Result(property = "permissionIds", column = "permission_ids", 
                typeHandler = LongListTypeHandler.class),
        @Result(property = "users", column = "users", 
                typeHandler = UserListTypeHandler.class)
    })
    @Select("select * from user_config where id = #{id}")
    UserConfig selectById(Long id);
    
    @Insert("insert into user_config(user_id, roles, permission_ids, users) " +
            "values(#{userId}, " +
            "#{roles, typeHandler=com.example.mybatis.handler.StringListTypeHandler}, " +
            "#{permissionIds, typeHandler=com.example.mybatis.handler.LongListTypeHandler}, " +
            "#{users, typeHandler=com.example.mybatis.handler.UserListTypeHandler})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(UserConfig userConfig);
}

六、单元测试

写个单元测试验证一下:

package com.example.mybatis.handler;

import com.example.entity.User;
import com.example.entity.UserConfig;
import com.example.mapper.UserConfigMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class ListTypeHandlerTest {
    
    @Autowired
    private UserConfigMapper userConfigMapper;
    
    @Test
    public void testInsertAndSelect() {
        // 准备数据
        UserConfig config = new UserConfig();
        config.setUserId("10001");
        config.setRoles(Arrays.asList("admin", "editor", "viewer"));
        config.setPermissionIds(Arrays.asList(1001L, 1002L, 1003L));
        
        User user1 = new User();
        user1.setId(1L);
        user1.setName("张三");
        
        User user2 = new User();
        user2.setId(2L);
        user2.setName("李四");
        
        config.setUsers(Arrays.asList(user1, user2));
        
        // 插入
        int result = userConfigMapper.insert(config);
        assertEquals(1, result);
        assertNotNull(config.getId());
        
        // 查询
        UserConfig dbConfig = userConfigMapper.selectById(config.getId());
        assertNotNull(dbConfig);
        
        // 验证String列表
        List<String> roles = dbConfig.getRoles();
        assertNotNull(roles);
        assertEquals(3, roles.size());
        assertEquals("admin", roles.get(0));
        assertEquals("editor", roles.get(1));
        assertEquals("viewer", roles.get(2));
        
        // 验证Long列表
        List<Long> permissionIds = dbConfig.getPermissionIds();
        assertNotNull(permissionIds);
        assertEquals(3, permissionIds.size());
        assertEquals(1001L, permissionIds.get(0));
        assertEquals(1002L, permissionIds.get(1));
        assertEquals(1003L, permissionIds.get(2));
        
        // 验证User列表
        List<User> users = dbConfig.getUsers();
        assertNotNull(users);
        assertEquals(2, users.size());
        assertEquals("张三", users.get(0).getName());
        assertEquals("李四", users.get(1).getName());
        
        System.out.println("测试通过!");
        System.out.println("roles: " + roles);
        System.out.println("permissionIds: " + permissionIds);
        System.out.println("users: " + users);
    }
}

七、性能优化

7.1 缓存ObjectMapper

ObjectMapper是线程安全的,可以重用:

public class JsonUtil {
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
    static {
        OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OBJECT_MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 注册Java 8时间模块
        OBJECT_MAPPER.registerModule(new JavaTimeModule());
    }
    
    public static ObjectMapper getInstance() {
        return OBJECT_MAPPER;
    }
}

7.2 批量操作优化

对于批量插入,可以复用TypeHandler:

@Insert({
    "<script>",
    "insert into user_config(user_id, roles, permission_ids) values",
    "<foreach collection='list' item='item' separator=','>",
    "(#{item.userId}, ",
    "#{item.roles, typeHandler=com.example.mybatis.handler.StringListTypeHandler}, ",
    "#{item.permissionIds, typeHandler=com.example.mybatis.handler.LongListTypeHandler})",
    "</foreach>",
    "</script>"
})
int batchInsert(List<UserConfig> list);

八、踩坑记录

坑1:空字符串处理

问题:数据库中存了空字符串"",解析时报错。

解决:在parseJson中增加空字符串判断:

if (json == null || json.trim().isEmpty()) {
    return null;
}

坑2:JSON数组为空

问题:数据库存的是"[]",期望返回空List而不是null。

解决:根据业务需求决定:

// 方式1:返回空List
if ("[]".equals(json)) {
    return new ArrayList<>();
}

// 方式2:返回null
if (json == null || json.isEmpty() || "[]".equals(json)) {
    return null;
}

坑3:日期格式问题

问题:对象中有日期字段,序列化后格式不对。

解决:统一配置日期格式:

OBJECT_MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
OBJECT_MAPPER.registerModule(new JavaTimeModule());

坑4:循环引用

问题:对象之间有循环引用,序列化时栈溢出。

解决:配置FAIL_ON_EMPTY_BEANS并在需要的地方加@JsonIgnore:

OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

// 或者在实体类上加注解
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User {
    // ...
}

九、总结

9.1 三种方案的对比

方案 优点 缺点 适用场景
XML指定类型 无需改代码 配置繁琐 临时修复
泛型Handler 一个类通用 需要传Class 类型多且统一
具体实现类 简单直接 代码重复 常用类型固定

9.2 最佳实践建议

  1. 常用类型预定义:String、Long、Integer等基础类型,预定义好Handler
  2. 复杂对象单独处理:自定义对象可以单独写Handler
  3. 统一JSON配置:全局统一ObjectMapper配置
  4. 单元测试覆盖:每种Handler都要写测试
  5. 日志记录:保留DEBUG日志,方便排查问题

如果觉得文章对你有帮助,欢迎点赞、收藏、关注!有问题可以在评论区交流。

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐