MyBatis JSON类型处理器实战:完美解决泛型List转换难题
MyBatis JSON类型处理器实战:完美解决泛型List转换难题
|
🌺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!
三、解决方案:三种思路对比
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 整体设计
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 最佳实践建议
- 常用类型预定义:String、Long、Integer等基础类型,预定义好Handler
- 复杂对象单独处理:自定义对象可以单独写Handler
- 统一JSON配置:全局统一ObjectMapper配置
- 单元测试覆盖:每种Handler都要写测试
- 日志记录:保留DEBUG日志,方便排查问题
如果觉得文章对你有帮助,欢迎点赞、收藏、关注!有问题可以在评论区交流。

|
🌺The End🌺点点关注,收藏不迷路🌺
|
更多推荐





所有评论(0)