MyBatis-Plus13:自定义 TypeHandler

一、自定义 TypeHandler

TypeHandler 是 MyBatis 中负责 Java 类型 ↔ 数据库类型 之间转换的处理器。

当内置的处理器满足不了需求时,就需要自定义。


二、使用场景

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

复制代码
数据库:{"name":"张三","age":22}  ←→  Java:Address 对象

三、实现步骤

第一步:定义 Java 对象

java 复制代码
@Data
public class Address {
    private String province;
    private String city;
}

第二步:编写自定义 TypeHandler

继承 BaseTypeHandler<T>,实现4个方法:

java 复制代码
@MappedTypes(Address.class)       // 声明处理的Java类型
@MappedJdbcTypes(JdbcType.VARCHAR) // 声明处理的数据库类型
public class AddressTypeHandler extends BaseTypeHandler<Address> {

    private static final ObjectMapper mapper = new ObjectMapper();

    // 写入数据库:Java对象 → 字符串
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    Address address, JdbcType jdbcType) throws SQLException {
        ps.setString(i, mapper.writeValueAsString(address));
    }

    // 查询时映射:字符串 → Java对象(按列名)
    @Override
    public Address getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return parse(rs.getString(columnName));
    }

    // 查询时映射:字符串 → Java对象(按列下标)
    @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);
        }
    }
}

第三步:实体类中使用

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

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

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

方式一,配置文件注册:

bash 复制代码
mybatis-plus:
  type-handlers-package: com.example.handler  # 扫描你的handler包

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


四、整个链路

复制代码
插入时:
Address对象 → AddressTypeHandler → JSON字符串 → 数据库

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

五、和 JacksonTypeHandler 的关系

MyBatis-Plus 内置了 JacksonTypeHandlerFastjsonTypeHandler,如果只是简单的 JSON 对象映射,直接用内置的就行,不需要自定义:

java 复制代码
@TableField(typeHandler = JacksonTypeHandler.class)
private Address address;

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

六、完整的例子1

我用一个完整的例子来讲解:用户表中有一个 hobbies 字段,数据库存的是 JSON 字符串 ["篮球","足球","游泳"],Java 里想用 List<String> 来接收。


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

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

第二步:理解 TypeHandler 要做什么

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

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

第三步:编写 TypeHandler

java 复制代码
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 java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

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

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

    /**
     * 存数据时调用:把 Java 的 List<String> 转成 JSON 字符串存入数据库
     * ps:可以理解成数据库操作对象
     * i:第几个参数
     * parameter:就是你传进来的 List<String>
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    List<String> 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<String>
     * columnName:数据库列名,比如 "hobbies"
     */
    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return parse(rs.getString(columnName));
    }

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

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

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

第四步:实体类中使用

java 复制代码
@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<String> hobbies;
}

第五步:注册 TypeHandler

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

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

第六步:测试效果

存数据:

java 复制代码
Users user = new Users();
user.setUsername("张三");
user.setHobbies(List.of("篮球", "足球", "游泳"));
usersService.save(user);

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

复制代码
["篮球","足球","游泳"]

取数据:

java 复制代码
Users user = usersService.getById(1);
List<String> hobbies = user.getHobbies();
System.out.println(hobbies);  // [篮球, 足球, 游泳]

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


整体流程图

复制代码
存数据:
Java代码 → List<String>["篮球","足球"] 
         → ListTypeHandler.setNonNullParameter() 
         → '["篮球","足球"]' 
         → 数据库

取数据:
数据库 → '["篮球","足球"]' 
       → ListTypeHandler.getNullableResult() 
       → parse() 方法解析 
       → List<String>["篮球","足球"] 
       → Java代码

【备注】:

对于List<String>的java <--->数据库的映射,不需要自己手写typehandler,以上只是示例,实际只需要加:@TableField(typeHandler = JacksonTypeHandler.class)即可。


常见错误

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

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

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

七、完整示例2

场景描述

用户的手机号、身份证号属于敏感信息,监管要求必须加密存储在数据库中,但 Java 代码里操作的时候要用明文。

复制代码
存入数据库:13812345678  →  加密  →  a3f8c2d1e9b7...(密文)
从数据库取:a3f8c2d1e9b7...(密文)  →  解密  →  13812345678

这种场景用 JacksonTypeHandler 完全搞不定,必须自定义。


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

java 复制代码
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

java 复制代码
@MappedTypes(String.class)
public class EncryptTypeHandler extends BaseTypeHandler<String> {

    // 存数据库时:明文 → 加密 → 存密文
    @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);
    }
}

第三步:实体类中使用

java 复制代码
@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;
}

测试效果

存数据:

java 复制代码
Users user = new Users();
user.setUsername("张三");
user.setPhone("13812345678");      // 传明文
user.setIdCard("110101199001011234");  // 传明文
usersService.save(user);

数据库实际存的是:

复制代码
phone:  a3f8c2d1e9b74f2a...  (密文,看不出原始手机号)
idCard: 9c2e1d8f3a7b6e4c...  (密文)

取数据:

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

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

因为这个需求是在 Java 和数据库之间做了额外的业务处理(加解密),不是简单的类型转换,任何内置的 TypeHandler 都做不到,只能自己写。

类似的场景还有:数据压缩存储、手机号脱敏显示、特殊格式转换等,都是自定义 TypeHandler 的典型使用场景。

八、setNonNullParameter()方法详解


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

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

sql 复制代码
INSERT INTO users (phone) VALUES (?)

这个 ? 是占位符,MyBatis 需要把你 Java 里的 "13812345678" 填进去。

填之前,就会调用这个方法,你可以在这里对值做任何处理,然后再填入。


逐个参数解释

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

PreparedStatement ps 就是那条带 ? 的 SQL 语句对象,可以理解成一个容器,等着你把值填进去。

int i 是第几个 ?,从1开始。比如 SQL 是:

sql 复制代码
INSERT INTO users (phone, idCard) VALUES (?, ?)

phone 对应 i=1idCard 对应 i=2

String plainText 就是你 Java 代码里传进来的原始值,比如 "13812345678"

JdbcType jdbcType 是数据库的字段类型,比如 VARCHAR、INT 等,这里一般用不到。


方法体解释

java 复制代码
ps.setString(i, AesUtil.encrypt(plainText));

拆开来看就是:

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

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


整个流程串起来

java 复制代码
你写的代码:
user.setPhone("13812345678")
usersService.save(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...')

↓ 数据库存的是密文

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

相关推荐
ruleslol2 天前
MyBatis-Plus07:rewriteBatchedStatements 参数详解与批处理
mybatis-plus
ruleslol2 天前
MyBatis-Plus08:代码生成
mybatis-plus
ruleslol2 天前
MyBatis-Plus09:静态工具Db
mybatis-plus
树码小子2 天前
Mybatis(17)Mybatis-Plus条件构造器(2)& 自定义 SQL
数据库·sql·mybatis-plus
ruleslol2 天前
MyBatis-Plus10:逻辑删除
mybatis-plus
树码小子3 天前
Mybatis(16)Mybatis-Plus条件构造器(1)
数据库·mybatis-plus
树码小子4 天前
Mybatis(14)Mybatis-Plus入门 & 简单使用
java·mybatis-plus
ruleslol5 天前
MyBatis-Plus06:IService接口Lambda基本用法
mybatis-plus
ruleslol6 天前
MyBatis-Plus02: 常用注解
mybatis-plus