MyBatisPlus实现自定义 TypeHandler示例

 更新时间:2026年08月05日 10:06:08   作者:ruleslol  
本文主要介绍了MyBatisPlus实现自定义 TypeHandler示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

背景

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

当你执行一个查询,MyBatis 拿到 ResultSet(数据库返回的结果集)后,需要把每一行数据转换成你的 Java 实体对象。

这个过程中,MyBatis 内置了一套类型处理器(TypeHandler),负责把数据库的列类型转换成 Java 类型:

数据库类型Java 类型谁负责转换
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"]
);

你在 Java 实体类里想直接映射成 List<String>

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

问题出现了:

MyBatis 内置的类型处理器里没有 JSON → List<String> 这个转换逻辑。它看到数据库列是 VARCHAR/JSON 类型,Java 属性是 List,这两个类型之间没有内置的桥梁。

没有 TypeHandler 的弊端:

  • 查询时,roles 字段的值会是 null,因为 MyBatis 不知道该怎么把 JSON 字符串变成 List
  • 或者你只能把 roles 定义成 String,然后在业务代码里自己写 JSON.parse(),每次查询完都手动转换
  • 插入时也一样,List 对象不能直接当成 SQL 参数塞进去,需要先转成 JSON 字符串

这违背了 ORM 框架的初衷——ORM 的目标是让开发者尽量只操作 Java 对象,而不是手动处理数据库和 Java 之间的格式转换。

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

因为上面的问题,MyBatis 设计了一个接口 TypeHandler<T>,它的职责只有一个:

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

它规定了两个核心行为:

  • 写入数据库时(Java → SQL):把 Java 对象转换成数据库能接受的格式
  • 从数据库读取时(SQL → Java):把数据库返回的原始值转换成 Java 对象
public interface TypeHandler<T> {
    // 写入: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 中负责 Java 类型 ↔ 数据库类型 之间转换的处理器。

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

二、使用场景

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

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

三、实现步骤

第一步:定义 Java 对象

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

第二步:编写自定义 TypeHandler

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

@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);
        }
    }
}

第三步:实体类中使用

@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 字符串 ["篮球","足球","游泳"],Java 里想用 List<String> 来接收。

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

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

第二步:理解 TypeHandler 要做什么

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

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

第三步:编写 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 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);
        }
    }
}

第四步:实体类中使用

@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 去哪里找我们的处理器:

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

第六步:测试效果

存数据:

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

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

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

取数据:

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 完全搞不定,必须自定义。

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

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<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);
    }
}

第三步:实体类中使用

@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.save(user);

数据库实际存的是:

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

取数据:

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:

INSERT INTO users (phone) VALUES (?)

这个 ? 是占位符,MyBatis 需要把你 Java 里的 "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 就是你 Java 代码里传进来的原始值,比如 "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.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...')

↓ 数据库存的是密文

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

九、总结逻辑链

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

核心记忆点:

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

到此这篇关于MyBatisPlus实现自定义 TypeHandler示例的文章就介绍到这了,更多相关MyBatisPlus自定义TypeHandler内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中获取年份月份的几种常见方法

    Java中获取年份月份的几种常见方法

    这篇文章主要给大家介绍了关于Java中获取年份月份的几种常见方法,在开发应用程序时,经常需要获取当前的年、月、日,并以特定格式进行展示或处理,需要的朋友可以参考下
    2023-09-09
  • 阿里四面之Spring Exception的原理解析

    阿里四面之Spring Exception的原理解析

    本文给大家介绍阿里四面之Spring Exception的原理解析,本文通过错误场景分析给大家详细介绍Spring异常处理流程,感兴趣的朋友一起看看吧
    2021-10-10
  • JavaWeb中Tomcat底层机制和Servlet运行原理详解

    JavaWeb中Tomcat底层机制和Servlet运行原理详解

    这篇文章主要介绍了JavaWeb中Tomcat底层机制和Servlet运行原理详解,Tomcat是一个开源的Java Web服务器,它是基于Java Servlet和JavaServer Pages(JSP)技术的,下面是关于Tomcat底层机制和Servlet运行原理的简要说明,需要的朋友可以参考下
    2023-10-10
  • 修改若依框架Token的过期时间问题

    修改若依框架Token的过期时间问题

    本文介绍了如何修改若依框架中Token的过期时间,通过修改`application.yml`文件中的配置来实现,默认单位为分钟,希望此经验对大家有所帮助,也欢迎支持脚本之家
    2025-02-02
  • 在SpringBoot里自定义Spring MVC配置的三种方法

    在SpringBoot里自定义Spring MVC配置的三种方法

    SpringBoot帮我们自动配好了 MVC 的核心组件,但实际开发中,光用默认配置肯定不够今天就教你 3 种自定义 Spring MVC 配置的方法,从简单到复杂,新手也能一步步跟着做,需要的朋友可以参考下
    2026-03-03
  • Java中的Semaphore信号量简析

    Java中的Semaphore信号量简析

    这篇文章主要介绍了Java中的Semaphore信号量简析,Semaphore:信号量,用来限制能同时访问共享资源的线程上限,使用Semaphore实现简单连接池,对比享元模式下的实现(用wait和notify),性能和可读性要更好,需要的朋友可以参考下
    2023-12-12
  • SpringMVC实现登录与注册功能的详细步骤

    SpringMVC实现登录与注册功能的详细步骤

    本文介绍了如何通过Maven配置依赖,创建前端登录和注册页面,并实现后端逻辑,详细步骤包括配置文件、创建User类、配置中文过滤器及DispatcherServlet,并使用Spring MVC和JQuery处理前端请求,需要的朋友可以参考下
    2024-11-11
  • Spring事务和事务传播机制操作大全

    Spring事务和事务传播机制操作大全

    Spring中事务管理主要分为编程式事务和声明式事务,声明式事务推荐使用,声明式事务通过`@Transactional`注解实现,可以简化事务操作,本文给大家介绍Spring事务和事务传播机制,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • Java泛型常见面试题(面试必问)

    Java泛型常见面试题(面试必问)

    泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。java泛型知识点也是Java开发岗位必问的一个话题,今天小编就给大家普及下Java泛型常见面试题,感兴趣的朋友一起看看吧
    2021-06-06
  • JAVA中的数组和List转换方法场景分析

    JAVA中的数组和List转换方法场景分析

    文章介绍了Java中List与数组之间的转换方法,包括List转数组和数组转List,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2026-01-01

最新评论