位置:首页 > Java > MyBatisPlus自定义TypeHandler实现方法与示例

MyBatisPlus自定义TypeHandler实现方法与示例

时间:2026-08-18  |  作者:火苗实验室  |  阅读:0

背景

第一步:MyBatis 默认是怎么把数据库字段映射到 Ja va 属性的?

当你执行一个查询,MyBatis 拿到 ResultSet(数据库返回的结果集),接着需要把每一行数据转换成你的 Ja va 实体对象。这个过程里,MyBatis 内置了一套类型处理器(TypeHandler),专门负责把数据库列类型转换成 Ja va 类型。

MyBatisPlus实现自定义TypeHandler示例

数据库类型Ja va 类型谁负责转换
VARCHARString内置 StringTypeHandler
INTInteger内置 IntegerTypeHandler
DATETIMELocalDateTime内置 LocalDateTimeTypeHandler

这些转换全是自动的——MyBatis 的作者早就替你写好了对应处理器。你的实体类里写了 private String name,MyBatis 一看就知道:哦,VARCHAR 列,直接塞给这个字段。这是第一层设计:基本类型的映射,你完全不用操心。

第二步:什么时候默认映射会失效?

假设数据库里有一个 JSON 类型的字段:

CREATE TABLE tb_user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    roles JSON   -- 存储如:["admin", "editor"]
);

你在 Ja va 实体类里想直接映射成 List

public class User {
    private Long id;
    private String name;
    private List roles;  // 想直接拿到 List
}

问题来了:

MyBatis 内置的类型处理器里,根本没有 JSON → List 这个转换逻辑。它看到数据库列是 VARCHAR/JSON 类型,Ja va 属性是 List,两者之间没有内置的桥梁。没有 TypeHandler 的后果就是:查询时 roles 字段的值永远是 null(因为 MyBatis 不知道该怎么把 JSON 字符串变成 List);或者你只能把 roles 定义成 String,然后在业务代码里自己写 JSON.parse(),每次查询完都手动转一遍。插入时也一样,List 对象不能直接当 SQL 参数,得先转成 JSON 字符串。这违背了 ORM 框架的初衷——ORM 的目标是让开发者尽量只操作 Ja va 对象,而不是手动处理数据库和 Ja va 之间的格式转换。

第三步:TypeHandler 被设计出来解决什么问题?

因为上面的痛点,MyBatis 设计了一个接口 TypeHandler,它的职责只有一个:

在 Ja va 类型和 JDBC 类型之间做双向转换。

它规定了两个核心行为:

  • 写入数据库时(Ja va → SQL):把 Ja va 对象转换成数据库能接受的格式
  • 从数据库读取时(SQL → Ja va):把数据库返回的原始值转换成 Ja va 对象
public interface TypeHandler {
    // 写入:PreparedStatement.setXxx()
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType);
    
    // 读取:ResultSet.getXxx(),三种重载对应不同场景
    T getResult(ResultSet rs, String columnName);
    T getResult(ResultSet rs, int columnIndex);
    T getResult(CallableStatement cs, int columnIndex);
}

为什么这样设计?因为 MyBatis 的作者心里清楚,他不可能预先知道所有项目里会出现的自定义类型转换需求(JSON、加密、压缩、特殊枚举等)。所以他把转换逻辑抽象成一个接口,让你自己实现具体的转换规则,然后 MyBatis 在需要转换的地方直接调用你写的逻辑。这就是扩展性。

一、自定义 TypeHandler

TypeHandler 是 MyBatis 中负责 Ja va 类型 数据库类型 之间转换的处理器。当内置的处理器满足不了需求时,就需要自己动手写一个。

二、使用场景

最典型的就是数据库存 JSON 字符串,Ja va 里想直接用对象接收:

数据库:{"name":"张三","age":22} Ja va:Address 对象

三、实现步骤

第一步:定义 Ja va 对象

@Data
public class Address {
    private String province;
    private String city;
}

第二步:编写自定义 TypeHandler

继承 BaseTypeHandler,实现 4 个方法:

@MappedTypes(Address.class)       // 声明处理的Ja va类型
@MappedJdbcTypes(JdbcType.VARCHAR) // 声明处理的数据库类型
public class AddressTypeHandler extends BaseTypeHandler
{ private static final ObjectMapper mapper = new ObjectMapper(); // 写入数据库:Ja va对象 → 字符串 @Override public void setNonNullParameter(PreparedStatement ps, int i, Address address, JdbcType jdbcType) throws SQLException { ps.setString(i, mapper.writeValueAsString(address)); } // 查询时映射:字符串 → Ja va对象(按列名) @Override public Address getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } // 查询时映射:字符串 → Ja va对象(按列下标) @Override public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } // 存储过程用 @Override public Address getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private Address parse(String json) { try { if (json == null) return null; return mapper.readValue(json, Address.class); } catch (Exception e) { throw new RuntimeException("JSON解析失败", e); } } }

第三步:实体类中使用

@TableName(value = "users", autoResultMap = true)  // 必须开启
@Data
public class Users {
    private Integer id;
    private String username;

    @TableField(typeHandler = AddressTypeHandler.class)  // 指定处理器
    private Address address;
}

第四步:注册 TypeHandler(二选一)

方式一,配置文件注册:

mybatis-plus:
  type-handlers-package: com.example.handler  # 扫描你的handler包

方式二,直接在 @TableField 上指定——这样就不需要全局注册,用哪个指哪个即可。

四、整个链路

插入时:
Address对象 → AddressTypeHandler → JSON字符串 → 数据库
查询时:
数据库 → JSON字符串 → AddressTypeHandler → Address对象

五、和 JacksonTypeHandler 的关系

MyBatis-Plus 内置了 JacksonTypeHandler 和 FastjsonTypeHandler。如果只是简单的 JSON 对象映射,直接用内置的就行,不需要自己写:

@TableField(typeHandler = JacksonTypeHandler.class)
private Address address;

自定义 TypeHandler 适合处理内置处理器搞不定的场景,比如加密存储、特殊格式转换、压缩存储等。

六、完整的例子1

来一个完整案例:用户表中有一个 hobbies 字段,数据库存的是 JSON 字符串 ["篮球","足球","游泳"],Ja va 里想用 List 接收。

第一步:先看数据库表结构

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    hobbies VARCHAR(500)  -- 存 JSON 字符串,比如 ["篮球","足球"]
);

第二步:理解 TypeHandler 要做什么

你可以把 TypeHandler 理解成一个翻译官

  • 存数据时:List ["篮球","足球"] → 翻译成 → 字符串 '["篮球","足球"]' → 存入数据库
  • 取数据时:字符串 '["篮球","足球"]' → 翻译成 → List ["篮球","足球"] → 返回给 Ja va

第三步:编写 TypeHandler

package com.example.handler;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;

import ja va.sql.CallableStatement;
import ja va.sql.PreparedStatement;
import ja va.sql.ResultSet;
import ja va.sql.SQLException;
import ja va.util.List;

@MappedTypes(List.class)  // 告诉MyBatis,这个处理器是处理 List 类型的
public class ListTypeHandler extends BaseTypeHandler> {

    // ObjectMapper 是 Jackson 库的核心类,用来做 JSON 转换
    private static final ObjectMapper mapper = new ObjectMapper();

    /**
     * 存数据时调用:把 Ja va 的 List 转成 JSON 字符串存入数据库
     * ps:可以理解成数据库操作对象
     * i:第几个参数
     * parameter:就是你传进来的 List
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    List parameter, JdbcType jdbcType) throws SQLException {
        try {
            // 把 ["篮球","足球"] 这个List转成字符串 '["篮球","足球"]'
            String json = mapper.writeValueAsString(parameter);
            ps.setString(i, json);
        } catch (Exception e) {
            throw new SQLException("List转JSON失败", e);
        }
    }

    /**
     * 取数据时调用(按列名查询):把数据库的 JSON 字符串转成 List
     * columnName:数据库列名,比如 "hobbies"
     */
    @Override
    public List getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return parse(rs.getString(columnName));
    }

    /**
     * 取数据时调用(按列的下标查询):把数据库的 JSON 字符串转成 List
     * columnIndex:第几列,从1开始
     */
    @Override
    public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return parse(rs.getString(columnIndex));
    }

    /**
     * 存储过程时调用,一般用不到,但必须实现
     */
    @Override
    public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return parse(cs.getString(columnIndex));
    }

    /**
     * 抽取一个公共方法:把 JSON 字符串转成 List
     */
    private List parse(String json) {
        try {
            if (json == null || json.isEmpty()) {
                return null;
            }
            // 把字符串 '["篮球","足球"]' 转回 List
            return mapper.readValue(json, new TypeReference>() {});
        } catch (Exception e) {
            throw new RuntimeException("JSON转List失败,原始值:" + json, e);
        }
    }
}

第四步:实体类中使用

@TableName(value = "users", autoResultMap = true)  // autoResultMap必须为true,否则查询时不生效
@Data
public class Users {
    
    @TableId(type = IdType.AUTO)
    private Integer id;
    
    private String username;
    
    // 指定用我们自定义的 ListTypeHandler 来处理这个字段
    @TableField(typeHandler = ListTypeHandler.class)
    private List hobbies;
}

第五步:注册 TypeHandler

application.yml 中告诉 MyBatis-Plus 去哪里找处理器:

mybatis-plus:
  type-handlers-package: com.example.handler  # 改成你自己的包路径

第六步:测试效果

存数据:

Users user = new Users();
user.setUsername("张三");
user.setHobbies(List.of("篮球", "足球", "游泳"));
usersService.sa ve(user);

此时数据库 hobbies 字段存的是:

["篮球","足球","游泳"]

取数据:

Users user = usersService.getById(1);
List hobbies = user.getHobbies();
System.out.println(hobbies);  // [篮球, 足球, 游泳]

自动就转回 List 了,完全不需要手动处理。

整体流程图

存数据:
Ja va代码 → List["篮球","足球"] 
         → ListTypeHandler.setNonNullParameter() 
         → '["篮球","足球"]' 
         → 数据库
取数据:
数据库 → '["篮球","足球"]' 
       → ListTypeHandler.getNullableResult() 
       → parse() 方法解析 
       → List["篮球","足球"] 
       → Ja va代码

【备注】:对于 List 的 Ja va 数据库映射,其实不需要自己手写 TypeHandler,以上只是示例。实际只需要加:@TableField(typeHandler = JacksonTypeHandler.class) 即可。

常见错误

查询时字段一直是 null?检查 @TableName 里有没有加 autoResultMap = true,这是最常见的遗漏。

JSON 解析报错?检查数据库里存的值格式是否正确,手动查一下是不是合法的 JSON 格式。

找不到 TypeHandler?检查 application.yml 里的包路径是否和你的 Handler 实际所在包一致。

七、完整示例2

场景描述

用户的手机号、身份证号属于敏感信息,监管要求必须加密存储在数据库中,但 Ja va 代码里操作的时候要用明文。存入数据库:13812345678 → 加密 → a3f8c2d1e9b7...(密文);从数据库取:a3f8c2d1e9b7...(密文) → 解密 → 13812345678。这种场景用 JacksonTypeHandler 完全搞不定,必须自定义。

第一步:准备一个简单的加密工具类

public class AesUtil {
    
    private static final String KEY = "1234567890abcdef";  // 16位密钥,实际项目放配置文件
    
    // 加密:明文 → 密文
    public static String encrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(content.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败", e);
        }
    }

    // 解密:密文 → 明文
    public static String decrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(content);
            return new String(cipher.doFinal(decoded));
        } catch (Exception e) {
            throw new RuntimeException("解密失败", e);
        }
    }
}

第二步:自定义 TypeHandler

@MappedTypes(String.class)
public class EncryptTypeHandler extends BaseTypeHandler {

    // 存数据库时:明文 → 加密 → 存密文
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                    String plainText, JdbcType jdbcType) throws SQLException {
        ps.setString(i, AesUtil.encrypt(plainText));
    }

    // 取数据库时:密文 → 解密 → 返回明文(按列名)
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return decrypt(rs.getString(columnName));
    }

    // 取数据库时:密文 → 解密 → 返回明文(按列下标)
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return decrypt(rs.getString(columnIndex));
    }

    // 存储过程
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return decrypt(cs.getString(columnIndex));
    }

    private String decrypt(String cipherText) {
        if (cipherText == null || cipherText.isEmpty()) return null;
        return AesUtil.decrypt(cipherText);
    }
}

第三步:实体类中使用

@TableName(value = "users", autoResultMap = true)
@Data
public class Users {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String username;

    // 手机号加密存储
    @TableField(typeHandler = EncryptTypeHandler.class)
    private String phone;

    // 身份证号加密存储
    @TableField(typeHandler = EncryptTypeHandler.class)
    private String idCard;

    // 普通字段,不需要加密
    private String email;
}

测试效果

存数据:

Users user = new Users();
user.setUsername("张三");
user.setPhone("13812345678");      // 传明文
user.setIdCard("110101199001011234");  // 传明文
usersService.sa ve(user);

数据库实际存的是:

phone: a3f8c2d1e9b74f2a... (密文,看不出原始手机号)

idCard: 9c2e1d8f3a7b6e4c... (密文)

取数据:

Users user = usersService.getById(1);
System.out.println(user.getPhone());   // 13812345678  自动解密成明文
System.out.println(user.getIdCard());  // 110101199001011234  自动解密成明文

为什么这个场景必须自定义?

因为这个需求是在 Ja va 和数据库之间做了额外的业务处理(加解密),不是简单的类型转换,任何内置的 TypeHandler 都做不到,只能自己写。类似的场景还有:数据压缩存储、手机号脱敏显示、特殊格式转换等,都是自定义 TypeHandler 的典型使用场景。

八、setNonNullParameter()方法详解

先理解这个方法是干什么的

当你执行 usersService.sa ve(user) 时,MyBatis-Plus 底层会构建一条 SQL:

INSERT INTO users (phone) VALUES ()

这个 是占位符,MyBatis 需要把你 Ja va 里的 "13812345678" 填进去。填之前,就会调用这个方法,你可以在这里对值做任何处理,然后再填入。

逐个参数解释

public void setNonNullParameter(
    PreparedStatement ps,    // 参数1
    int i,                   // 参数2
    String plainText,        // 参数3
    JdbcType jdbcType        // 参数4
)

PreparedStatement ps 就是那条带 的 SQL 语句对象,可以理解成一个容器,等着你把值填进去。int i 是第几个 ,从1开始。比如 SQL 是:INSERT INTO users (phone, idCard) VALUES (, ),phone 对应 i=1,idCard 对应 i=2。String plainText 就是你 Ja va 代码里传进来的原始值,比如 "13812345678"。JdbcType jdbcType 是数据库的字段类型,比如 VARCHAR、INT 等,这里一般用不到。

方法体解释

ps.setString(i, AesUtil.encrypt(plainText));

拆开来看就是:

String cipherText = AesUtil.encrypt(plainText); // 第一步:把明文加密成密文
ps.setString(i, cipherText);                    // 第二步:把密文填入第i个占位符

ps.setString(i, 值) 的意思就是:把值填入 SQL 的第 i 个问号。

整个流程串起来

你写的代码:
user.setPhone("13812345678")
usersService.sa ve(user)

↓ MyBatis构建SQL

INSERT INTO users (phone) VALUES ()

↓ 调用 setNonNullParameter(ps, 1, "13812345678", VARCHAR)

↓ 方法内部执行
AesUtil.encrypt("13812345678") → "a3f8c2d1..."
ps.setString(1, "a3f8c2d1...")

↓ 最终执行的SQL

INSERT INTO users (phone) VALUES ('a3f8c2d1...')

↓ 数据库存的是密文

所以这个方法就是一个拦截器的作用,在值真正写入数据库之前,偷偷把它加密了。

九、总结逻辑链

  • MyBatis 内置了基本类型的转换,但无法覆盖所有业务场景(如 JSON)
  • 没有 TypeHandler 的弊端:复杂类型无法自动映射,查询为 null,需要手动转换
  • 所以 MyBatis 设计了 TypeHandler 接口,让你自定义转换逻辑
  • MyBatis-Plus 的通用方法不走 XML 的 ResultMap,导致查询时 TypeHandler 配置无法被识别
  • 所以 MyBatis-Plus 设计了 autoResultMap,自动构建包含 TypeHandler 信息的 ResultMap,让通用查询方法也能正确使用自定义类型处理器

核心记忆点:

只要你在实体类字段上写了 @TableField(typeHandler = ...),这个实体类的 @TableName 就必须加上 autoResultMap = true,否则查询结果里这个字段永远是 null。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多