前情回顾
在 《MyBatis基础入门《十》Spring Boot 整合 MyBatis》 中,我们完成了企业级项目的基础搭建。
但现实业务中,数据库字段和 Java 对象往往 不是简单的一一对应

  • MySQL 的 JSON 字段要映射为 Map<String, Object> 或自定义对象;
  • 枚举值(如 OrderStatus.PAID)需存为数字或字符串;
  • 敏感字段(手机号、身份证)入库前加密,查询后解密。

如何统一、安全、高效地处理这些转换?

答案:使用 MyBatis TypeHandler
本文将从原理到实战,手把手教你编写自定义类型处理器。


一、什么是 TypeHandler?

TypeHandler 是 MyBatis 提供的 类型转换器,负责:

  • Java Type → JDBC Type(设置参数时,如 PreparedStatement.setXXX()
  • JDBC Type → Java Type(获取结果时,如 ResultSet.getXXX()

MyBatis 内置了大量 TypeHandler(如 StringTypeHandler, IntegerTypeHandler),但面对复杂类型时,我们需要自定义。


二、实战场景一:MySQL JSON 字段 ↔ Java 对象

场景说明

用户表中有一个 profile JSON 字段,存储用户扩展信息:

CREATE TABLE tbl_user (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    profile JSON
);

Java 实体:

public class User {
    private Integer id;
    private String username;
    private UserProfile profile; // 自定义对象
}

public class UserProfile {
    private String avatar;
    private String city;
    // getter / setter
}

目标:查询时自动将 JSON 字符串转为 UserProfile;插入时反向转换。


步骤 1:引入 JSON 工具(如 Jackson)

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

步骤 2:编写自定义 TypeHandler

// JsonTypeHandler.java
package com.charles.typehandler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;

public class JsonTypeHandler<T> extends BaseTypeHandler<T> {

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private Class<T> type;

    public JsonTypeHandler(Class<T> type) {
        if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) 
            throws SQLException {
        try {
            String json = objectMapper.writeValueAsString(parameter);
            ps.setString(i, json); // 存为 VARCHAR/TEXT
        } catch (Exception e) {
            throw new SQLException("Error converting " + type.getSimpleName() + " to JSON", e);
        }
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return parseJson(rs.getString(columnName));
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return parseJson(rs.getString(columnIndex));
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return parseJson(cs.getString(columnIndex));
    }

    private T parseJson(String json) throws SQLException {
        if (json == null || json.isEmpty()) return null;
        try {
            return objectMapper.readValue(json, type);
        } catch (Exception e) {
            throw new SQLException("Error parsing JSON to " + type.getSimpleName(), e);
        }
    }
}

✅ 继承 BaseTypeHandler<T>,实现四个核心方法; ✅ 使用泛型支持任意 Java 对象; ✅ 异常统一包装为 SQLException


步骤 3:在实体类中注册 TypeHandler

public class User {
    private Integer id;
    private String username;
    
    @Results({
        @Result(property = "profile", column = "profile", 
                typeHandler = JsonTypeHandler.class)
    })
    private UserProfile profile;
}

或更简洁地,在字段上使用 @TypeHandler(MyBatis 3.4+):

public class User {
    // ...
    
    @TypeHandler(JsonTypeHandler.class)
    private UserProfile profile;
}

🔔 注意:若使用 XML 映射,可在 <result> 标签中指定 typeHandler


步骤 4:测试效果

@Test
public void testJsonTypeHandler() {
    User user = new User();
    user.setUsername("张三");
    
    UserProfile profile = new UserProfile();
    profile.setAvatar("avatar.jpg");
    profile.setCity("深圳");
    user.setProfile(profile);

    userMapper.insert(user); // 自动转为 JSON 字符串存入数据库

    User saved = userMapper.selectById(user.getId());
    System.out.println(saved.getProfile().getCity()); // 输出:深圳
}

✅ 成功!无需手动序列化/反序列化!


三、实战场景二:枚举存储(Enum ↔ Integer/String)

需求

订单状态:CREATED=0, PAID=1, SHIPPED=2

public enum OrderStatus {
    CREATED(0), PAID(1), SHIPPED(2);
    
    private final int code;
    OrderStatus(int code) { this.code = code; }
    public int getCode() { return code; }
    
    public static OrderStatus fromCode(int code) {
        for (OrderStatus s : values()) {
            if (s.code == code) return s;
        }
        throw new IllegalArgumentException("Invalid code: " + code);
    }
}

自定义 EnumTypeHandler

public class OrderStatusTypeHandler extends BaseTypeHandler<OrderStatus> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) 
            throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return rs.wasNull() ? null : OrderStatus.fromCode(code);
    }

    // ... 其他 getNullableResult 方法类似
}

Order 实体中使用:

@TypeHandler(OrderStatusTypeHandler.class)
private OrderStatus status;

💡 优势:数据库存整数,Java 用枚举,安全又语义清晰!


四、全局注册 TypeHandler(可选)

避免每个字段重复声明,可在 mybatis-config.xml 中全局注册:

<typeHandlers>
    <typeHandler handler="com.charles.typehandler.JsonTypeHandler"
                 javaType="com.charles.entity.UserProfile"
                 jdbcType="VARCHAR" />
</typeHandlers>

或在 Spring Boot 中通过配置类:

@Configuration
public class MyBatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        
        // 注册全局 TypeHandler
        TypeHandlerRegistry registry = factory.getObject().getConfiguration().getTypeHandlerRegistry();
        registry.register(UserProfile.class, new JsonTypeHandler<>(UserProfile.class));
        
        return factory.getObject();
    }
}

五、注意事项 & 最佳实践

⚠️ 1. 线程安全

  • ObjectMapper 是线程安全的(Jackson 2.8+),可共享;
  • 避免在 TypeHandler 中使用非线程安全的成员变量。

⚠️ 2. 异常处理

  • 必须捕获内部异常并转为 SQLException,否则 MyBatis 无法正确回滚。

✅ 3. 性能

  • TypeHandler 在每次 SQL 执行时调用,避免做耗时操作;
  • 可缓存反射结果(如枚举 code 映射)。

🔄 4. 与 JSON 数据库类型兼容

  • MySQL 5.7+ 支持 JSON 类型,但 JDBC 驱动仍将其视为 VARCHAR
  • 因此 setString / getString 完全适用。

六、总结:TypeHandler 应用场景速查

场景 解决方案
JSON 字段 ↔ 对象 自定义 JsonTypeHandler
枚举 ↔ 数字/字符串 自定义 EnumTypeHandler
加密字段(如手机号) 入库加密,出库解密
日期格式定制(如只存年月) 自定义 LocalDateTypeHandler
多值字段(如逗号分隔) 转为 List<String>

核心价值
“让数据库字段与 Java 对象自由对话,代码更干净,逻辑更内聚!”

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐