在我们日常的开发工作中,经常会遇到一些敏感数据需要存储,比如用户的手机号、身份证号、银行卡号等。为了保障数据安全,我们通常会对这些敏感信息进行加密后再存入数据库。
如果每次在存入数据库之前都手动调用加密方法,在查询出来之后再手动调用解密方法,不仅繁琐,而且容易出错,也不利于代码的维护。
有一种更优雅、更通用的方式来解决这个问题,答案是:使用MyBatis的TypeHandler。
我们通过对用户手机号进行加密存储和解密查询来详细讲解如何自定义和使用 MyBatis的TypeHandler
目录👑
[1. 核心概念:什么是 TypeHandler?](#1. 核心概念:什么是 TypeHandler?)
[2. 实战场景](#2. 实战场景)
[3. 代码实现步骤](#3. 代码实现步骤)
[步骤 1:定义 Encrypt 类](#步骤 1:定义 Encrypt 类)
[步骤 2:实现 EncryptTypeHandler](#步骤 2:实现 EncryptTypeHandler)
[步骤 3:配置 MyBatis](#步骤 3:配置 MyBatis)
[步骤 4:在业务代码中使用](#步骤 4:在业务代码中使用)
[4. 总结与优势](#4. 总结与优势)
1. 核心概念:什么是 TypeHandler?
TypeHandler,顾名思义,就是类型处理器 。它在 MyBatis 中扮演着一个非常关键的角色:在 Java 对象和数据库 JDBC 类型之间进行转换。
- 写入数据库时:MyBatis 会使用 TypeHandler 将你的 Java 对象(比如一个String)转换成数据库可以理解的 JDBC 类型(比如VARCHAR)。
- 从数据库读取时:MyBatis 会使用 TypeHandler 将从数据库中查询到的 JDBC 类型(比如VARCHAR)转换回你的 Java 对象(比如一个String)。
MyBatis 内置了大量的 TypeHandler 来处理常见的 Java 类型和 JDBC 类型之间的转换。但当我们有特殊需求时,比如需要对数据进行加密解密,就需要自定义 TypeHandler。
2. 实战场景
- 加密:当我们将一个 Java 对象(比如User)中的手机号(String类型)存入数据库时,MyBatis 能自动将其加密成一个密文String后再存储。
- 解密:当我们从数据库查询出这个密文String时,MyBatis 能自动将其解密成原始的手机号String后,再设置到 Java 对象中。
我们可以让TypeHandler来处理指定的java类型将其转换成jdbc中的类型,
而为了避免将所有的String类型都通过TypeHandler来进行转换(因为我们只想转换存储手机号的String)也是为了让这个过程更清晰,
我们引入一个自定义的 Java 类型Encrypt,用它来标记需要进行加密解密的字段。
3. 代码实现步骤
- 定义一个标记接口 / 类:创建Encrypt类。
- 实现自定义 TypeHandler:创建EncryptTypeHandler类,实现加密和解密逻辑。
- 配置 TypeHandler:告诉 MyBatis 我们的 TypeHandler 存在。
- 在代码中使用:修改Mapper接口,享受自动加密解密的便利。
步骤 1:定义 Encrypt 类
这个类的主要作用是作为一个 "标记",告诉 MyBatis:"凡是用这个Encrypt类型声明的字段,都需要使用对应的EncryptTypeHandler来处理"。
它可以非常简单,甚至可以是空的。
java
package com.dao.dataobject;
import lombok.Data;
/**
* 一个标记接口,用于标识需要加密/解密的字段
*/
@Data
public class Encrypt {
private String value;
public Encrypt(byte[] decrypt) {
}
public Encrypt(String value) {
this.value = value;
}
}
步骤 2:实现 EncryptTypeHandler
这是整个功能的核心。我们需要实现 MyBatis 的TypeHandler接口,并在其中编写 AES 加密和解密的逻辑。
java
package com.dao.handler;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.bit.lotterySystem.dao.dataobject.Encrypt;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 自定义的 TypeHandler,用于处理 Encrypt 类型与 VARCHAR 类型之间的转换(包含加密解密)
*/
@MappedTypes(Encrypt.class)//告诉 MyBatis,这个 Handler 是处理 Java 中的 Encrypt 类型的。
@MappedJdbcTypes(JdbcType.VARCHAR)//处理后的jdbc类型
public class EncryptTypeHandler implements TypeHandler<Encrypt> {
// 注意:在生产环境中,密钥绝不能硬编码在代码里!应从配置文件或环境变量中读取。
private final byte[] key = "123456789abcdefg".getBytes(StandardCharsets.UTF_8); // 16位密钥,AES-128
/**
* 设置参数
* 将 Encrypt 对象中的明文加密后,以 String 形式存入数据库。
*
* @param ps SQL预编译对象
* @param i 需要赋值的索引位置
* @param parameter 原本位置i需要赋的值
* @param jdbcType jdbc的类型
* @throws SQLException
*/
@Override
public void setParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null || parameter.getValue() == null) {
ps.setString(i, null);
return;
}
// 1. 获取需要加密的明文
String plaintext = parameter.getValue();
// 2. 使用 Hutool 的 SecureUtil 进行 AES 加密
AES aes = SecureUtil.aes(key);
String ciphertext = aes.encryptHex(plaintext); // encryptHex 返回十六进制字符串
// 3. 将加密后的密文设置到 PreparedStatement
ps.setString(i, ciphertext);
}
/**
* 获取值
* 将从数据库读出的密文 String 解密后,封装成 Encrypt 对象。
*
* @param rs 结果集
* @param columnName 索引名
* @return
* @throws SQLException
*/
*/
@Override
public Encrypt getResult(ResultSet rs, String columnName) throws SQLException {
String ciphertext = rs.getString(columnName);
return decrypt(ciphertext);
}
/**
* 获取值,从 ResultSet 中根据列索引获取值(从数据库读取)
* @param rs 结果集
* @param columnIndex 索引位置
* @return
* @throws SQLException
*/
@Override
public Encrypt getResult(ResultSet rs, int columnIndex) throws SQLException {
String ciphertext = rs.getString(columnIndex);
return decrypt(ciphertext);
}
/**
* 获取值,从 CallableStatement 中获取值(存储过程相关)
* @param cs 结果集
* @param columnIndex 索引位置
* @return
* @throws SQLException
*/
@Override
public Encrypt getResult(CallableStatement cs, int columnIndex) throws SQLException {
String ciphertext = cs.getString(columnIndex);
return decrypt(ciphertext);
}
/**
* 通用的解密方法
*/
private Encrypt decrypt(String ciphertext) {
if (ciphertext == null) {
return null;
}
// 1. 使用 Hutool 的 SecureUtil 进行 AES 解密
AES aes = SecureUtil.aes(key);
String plaintext = aes.decryptStr(ciphertext, StandardCharsets.UTF_8);
// 2. 将解密后的明文封装成 Encrypt 对象返回
return new Encrypt(plaintext);
}
}
代码解读:
-
@MappedTypes和@MappedJdbcTypes:这两个注解是关键,它们建立了Encrypt类型和VARCHAR类型之间的映射关系,让 MyBatis 能够自动找到并使用这个TypeHandler。 -
setParameter:在执行insert、update时,MyBatis 会调用此方法。我们在这里实现加密逻辑。 -
getResult(三个重载方法) :在执行select 后,MyBatis 会调用这些方法来填充结果对象。我们在这里实现解密逻辑。 -
依赖 :代码中使用了Hutool工具库来简化 AES 加密解密的代码。需要添加依赖:
XML<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.41</version> </dependency>
步骤 3:配置 MyBatis
我们需要让 MyBatis 知道自定义的TypeHandler在哪里 他才会起作用。
有两种常用方式:
方式一:在 MyBatis 配置文件中注册
XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeHandlers>
<package name="com.dao.handler"/>
</typeHandlers>
</configuration>
这种方式最推荐,它会扫描指定包下所有实现了TypeHandler接口并带有@MappedTypes注解的类。
方式二:在 Spring Boot 中通过注解自动扫描
如果你使用 Spring Boot,并且在**application.yml** 或**application.properties** 中配置了**mybatis.type-handlers-package**,
Spring Boot 启动时会自动扫描并注册该包下的 TypeHandler。
java
# application.yml
mybatis:
type-handlers-package: com.dao.handler
# 其他配置...
步骤 4:在业务代码中使用
现在,我们的TypeHandler已经准备就绪,可以在Mapper接口中使用了。
假设我们有一个UserMapper,需要根据手机号查询是否有用户。
java
package com.dao.mapper;
import com.bit.lotterySystem.dao.dataobject.Encrypt;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
/**
* 根据手机号统计用户数量
* @param phoneNumber 注意这里的参数类型是 Encrypt
* @return 用户数量
*/
@Select("SELECT count(*) FROM user WHERE phone_number = #{phoneNumber}")
int countPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);
}
使用它:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 校验手机号是否已被使用
* @param phoneNumber 原始的手机号明文
* @return 是否被使用
*/
private boolean checkPhoneNumberUsed(String phoneNumber) {
// 我们只需要将明文手机号封装进 Encrypt 对象即可
// MyBatis 会在后台自动调用 EncryptTypeHandler 进行加密
int count = userMapper.countPhoneNumber(new Encrypt(phoneNumber));
return count > 0;
}
// ...
}
魔法发生了:
- 当你调用 **
userMapper.countPhoneNumber(new Encrypt(phoneNumber))**时,MyBatis 发现参数类型是Encrypt - MyBatis 找到注册的EncryptTypeHandler
- MyBatis 调用EncryptTypeHandler.setParameter()方法。
- setParameter方法将传入的手机号加密成密文(例如
a1b2c3d4...) - 最终执行的 SQL 变成了
SELECT count(*) FROM user WHERE phone_number = 'a1b2c3d4...'
如果是查询操作,假设数据库中**phone_number** 字段存储的是密文,MyBatis 会在获取结果时自动调用**getResult** 方法进行解密,将密文转换回 Encrypt对象。
4. 总结与优势
通过以上步骤,我们成功实现了数据库字段的自动加密和解密。
这种方式的优势非常明显:
- 代码解耦:加密解密逻辑被封装在TypeHandler中,业务代码变得非常干净,无需关心加密细节。
- 通用性强:只要在任何需要加密的地方使用 Encrypt类型声明字段或参数,MyBatis 就会自动处理。
- 可维护性高:如果未来需要更换加密算法或密钥,只需要修改 EncryptTypeHandler这一个地方即可,无需改动所有业务代码。
- 优雅高效:利用了 MyBatis 的扩展机制,是处理这类问题的 "最佳实践" 之一。