注:另一种方式参考关于@JsonSerialize序列化与@JsonDeserialize反序列化注解的使用(密码加密与解密举例)http://t.csdnimg.cn/1IlBp
1.简介
在 MyBatis 和 MyBatis-Plus 中,
TypeHandler
是一个用于处理 Java 类型和 JDBC 类型之间转换的接口。MyBatis 默认已经提供了很多类型处理器,用于处理常见的Java类型与JDBC类型之间的转换。然而,在某些特定场景下,通过自定义
TypeHandler
,你可以控制数据库字段和 Java 对象属性之间的数据转换逻辑,这在处理特殊数据类型时非常有用,自定义类型处理器来满足特定的需求,比如:
- 将Java对象转换为数据库中特定的列类型(如枚举类型、加密后的字符串等)。
- 从数据库读取特定列类型时,转换为Java中的对象。
具体可以参考:字段类型处理器 | MyBatis-Plus
2.为什么要使用 TypeHandler
默认情况下,MyBatis 会使用 JDBC 的
PreparedStatement
和ResultSet
来处理 SQL 语句,并自动将 Java 类型转换为 JDBC 类型,然后再转换回 Java 类型。但是,这种自动转换在某些场景下可能不够用,比如:
- 加密字段:如果数据库中存储的是加密后的数据(如你的例子中使用 SM4 加密的密码),那么在查询时你需要将这些加密的数据解密成 Java 中的字符串类型。
- 枚举类型:当数据库中的某个字段表示的是枚举值时,你可能希望 MyBatis 自动将数据库中的值转换为 Java 枚举类型。
- 复杂类型:对于某些数据库中的特殊类型(如 JSON、XML 等),你可能需要自定义解析逻辑。
3.自定义 TypeHandler 的步骤
创建 TypeHandler 类:
- 实现
org.apache.ibatis.type.TypeHandler
接口。- 或者继承
org.apache.ibatis.type.BaseTypeHandler
类,它提供了默认实现的一些方法。实现 TypeHandler 方法:
- 实现
setNonNullParameter
方法,用于保存数据到数据库时的处理。- 实现
getNullableResult
方法,用于从数据库读取数据时的处理。注册 TypeHandler:
- 在 Spring Boot 配置类中注册
TypeHandler
。- 使用
ConfigurationCustomizer
或者直接在 MyBatis 的配置文件中注册。在实体类中使用 TypeHandler:
- 在实体类的字段上使用
@TableField
注解,并指定typeHandler
属性。
4.如何实现 TypeHandler?
俩种方式:
继承
org.apache.ibatis.type.TypeHandler<T>
接口或者org.apache.ibatis.type.BaseTypeHandler接口(其实BaseTypeHandler也是实现的TypeHandler)
4.1继承TypeHandler
实现 TypeHandler
需要继承 org.apache.ibatis.type.TypeHandler<T>
接口,并实现其中的四个方法:
setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
:将 Java 类型转换为 JDBC 类型,用于 SQL 语句的预处理阶段。getResult(ResultSet rs, String columnName)
:从ResultSet
中根据列名获取数据,并将其转换为 Java 类型。getResult(ResultSet rs, int columnIndex)
:从ResultSet
中根据列索引获取数据,并将其转换为 Java 类型。getResult(CallableStatement cs, int columnIndex)
:从CallableStatement
中根据列索引获取数据,并将其转换为 Java 类型。
4.2 继承BaseTypeHandler
在MyBatis中,BaseTypeHandler<T>
是一个抽象类,它为类型处理器提供了基础实现,用于在Java类型和JDBC类型之间进行转换。T
是Java类型参数,它表示这个类型处理器将要处理的Java类型。
当你想要自定义类型处理器时,你可以继承 BaseTypeHandler<T>
并实现其方法。以下是 BaseTypeHandler<T>
类的四个抽象方法的详细解释:
(1)
setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
这个方法用于将Java类型
T
的参数设置到PreparedStatement
中,以供后续的SQL执行。参数如下:
PreparedStatement ps
: 要设置的预处理语句。int i
: 参数的索引位置。T parameter
: 要设置的Java类型参数。JdbcType jdbcType
: JDBC类型代码(通常是null
,除非你明确知道你要处理的类型)。注:实现这个方法时,你应该将
parameter
转换为适当的JDBC类型,并设置到PreparedStatement
中。
(2)getNullableResult(ResultSet rs, String columnName)
这个方法用于从
ResultSet
中获取指定列名的值,并将其转换为Java类型T
。参数如下:
ResultSet rs
: 结果集。String columnName
: 要获取的列名。注:实现这个方法时,你应该从
ResultSet
中获取指定列的值,并将其转换为T
类型。
(3)
getNullableResult(ResultSet rs, int columnIndex)
这个方法与
getNullableResult(ResultSet rs, String columnName)
类似,但它使用列的索引而不是列名来获取值。参数如下:
ResultSet rs
: 结果集。int columnIndex
: 要获取的列的索引位置。
(4)getNullableResult(CallableStatement cs, int columnIndex)
这个方法用于从
CallableStatement
中获取指定列索引的值,并将其转换为Java类型T
。参数如下:
CallableStatement cs
: 调用语句。int columnIndex
: 要获取的列的索引位置。注:实现这个方法时,你应该从
CallableStatement
中获取指定列的值,并将其转换为T
类型。
5.密码加密与解密举例
5.1 User用户实体
注意:使用mybatis-plus实现crud。一定要在用户实体上开启自动映射:autoResultMap = true
@TableName(value = "user",autoResultMap = true)
@TableField(value = "phone",typeHandler = Sm4TypeHandler.class)
@Schema(name="phone",description=" 电话 ")
private String phone;
@TableField(value = "account",typeHandler = Sm4TypeHandler.class)
@Schema(name="account",description=" 账号 ")
private String account;
@TableField(value = "pwd",typeHandler = Sm4TypeHandler.class)
@Schema(name="pwd",description=" 密码 ")
private String pwd;
package com.zsh.test.swagger3test.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.zsh.test.swagger3test.handler.Sm4TypeHandler;
import lombok.Data;
import lombok.experimental.Accessors;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
/**
* @Description
* @Author ZhaoShuhao
* @Date: 2024-07-21 12:45:39
*/
@Data
@Accessors(chain = true)
@Schema(name="用户信息")
@TableName(value = "user",autoResultMap = true)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.ASSIGN_ID)
@Schema(name="id",description=" 主键 ")
private Long id;
@TableField(value = "name")
@Schema(name="name",description=" 姓名 ")
private String name;
@TableField(value = "age")
@Schema(name="age",description=" 年龄 ")
private Integer age;
@TableField(value = "phone",typeHandler = Sm4TypeHandler.class)
@Schema(name="phone",description=" 电话 ")
private String phone;
@TableField(value = "account",typeHandler = Sm4TypeHandler.class)
@Schema(name="account",description=" 账号 ")
private String account;
@TableField(value = "pwd",typeHandler = Sm4TypeHandler.class)
@Schema(name="pwd",description=" 密码 ")
private String pwd;
@TableField(value = "sex")
@Schema(name="sex",description=" 性别 ")
private Integer sex;
@TableField(value = "creat_time")
@Schema(name="reatTime",description=" 创建时间 ")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date creatTime;
@TableField(value = "update_time")
@Schema(name="updateTime",description=" 更新时间 ")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
}
5.2 UserVo用户视图对象
package com.zsh.test.swagger3test.model.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.zsh.test.swagger3test.serializer.SexSerializer;
import com.zsh.test.swagger3test.serializer.UserPwdSerializertest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @Description
* @Author ZhaoShuhao
* @Date: 2024-07-21 12:45:39
*/
@Data
@Accessors(chain = true)
@Schema(name=" user ", description=" null ")
public class UserVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(name="id",description=" 主键 ")
private Long id;
@Schema(name="name",description=" 姓名 ")
private String name;
@Schema(name="age",description=" 年龄 ")
private Integer age;
@Schema(name="phone",description=" 电话 ")
private String phone;
@Schema(name="account",description=" 账号 ")
private String account;
@Schema(name="pwd",description=" 密码 ")
// @JsonSerialize(using= UserPwdSerializertest.class)
private String pwd;
@Schema(name="sex",description=" 性别 ")
@JsonSerialize(using = SexSerializer.class)
private Integer sex;
}
5.3 UserDto用户入参对象
package com.zsh.test.swagger3test.model.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.zsh.test.swagger3test.serializer.UserPwdDeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* @Description
* @Author ZhaoShuhao
* @Date: 2024-07-21 12:45:39
*/
@Data
public class UserDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(name="name",description=" 姓名 ")
private String name;
@Schema(name="age",description=" 年龄 ")
private Integer age;
@Schema(name="phone",description=" 电话 ")
private String phone;
@Schema(name="account",description=" 账号 ")
private String account;
// @JsonDeserialize(using= UserPwdDeSerializer.class)
@Schema(name="pwd",description=" 密码 ")
private String pwd;
@Schema(name="sex",description=" 性别 ")
private Integer sex;
}
5.4 UserMapper
package com.zsh.test.swagger3test.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zsh.test.swagger3test.model.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author KeepHappy
* @description 针对表【user】的数据库操作Mapper
* @createDate 2024-07-21 12:55:52
* @Entity src/main/java/com/zsh/test/swagger3test.model.User
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
5.5 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsh.test.swagger3test.mapper.UserMapper">
</mapper>
5.6 UserService
package com.zsh.test.swagger3test.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zsh.test.swagger3test.model.dto.UserDto;
import com.zsh.test.swagger3test.model.entity.User;
import com.zsh.test.swagger3test.model.vo.UserVo;
import java.util.List;
/**
* @author KeepHappy
* @description 针对表【user】的数据库操作Service
* @createDate 2024-07-21 12:55:52
*/
public interface UserService extends IService<User> {
}
5.7 UserServiceImpl
package com.zsh.test.swagger3test.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zsh.test.swagger3test.model.dto.UserDto;
import com.zsh.test.swagger3test.mapper.UserMapper;
import com.zsh.test.swagger3test.model.entity.User;
import com.zsh.test.swagger3test.model.vo.UserVo;
import com.zsh.test.swagger3test.service.UserService;
import io.swagger.v3.core.util.Json;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author KeepHappy
* @description 针对表【user】的数据库操作Service实现
* @createDate 2024-07-21 12:55:52
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
5.8 Sm4TypeHandler密码sm4加密类型处理器
package com.zsh.test.swagger3test.handler;
public class Sm4TypeHandler extends Sm4AbstractObjectTypeHandler<String> {
}
5.9 Sm4AbstractObjectTypeHandler密码sm4加密类型处理器具体业务类
package com.zsh.test.swagger3test.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.zsh.test.swagger3test.util.DesensitizedUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Sm4AbstractObjectTypeHandler<T> extends BaseTypeHandler<T> {
static final ObjectMapper objectMapper = new ObjectMapper();
public JavaType javaType;
@SuppressWarnings("unchecked")
public Sm4AbstractObjectTypeHandler() {
Type type = ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
javaType = TypeFactory.defaultInstance().constructType(type);
}
@Override
@SuppressWarnings("unchecked")
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
Object decrypt = parameter;
//如果没加密再加密
if(!DesensitizedUtils.isEncryptioned(decrypt.toString())){
decrypt = DesensitizedUtils.encryption(parameter.toString());
}
ps.setObject(i, decrypt);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
@SuppressWarnings("unchecked")
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
try {
String data = rs.getString(columnName);
//如果没加密不需要解密
if (StrUtil.isBlank(data)||!DesensitizedUtils.isEncryptioned(data)){
return Convert.convert(javaType, data);
}
//return StrUtil.isBlank(data) ? null : objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
return Convert.convert(javaType,DesensitizedUtils.decrypt(data));
//return objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
@SuppressWarnings("unchecked")
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
try {
String data = rs.getString(columnIndex);
//如果没加密不需要解密
if (StrUtil.isBlank(data)||!DesensitizedUtils.isEncryptioned(data)){
return objectMapper.readValue(data, javaType);
}
// return StrUtil.isBlank(data) ? null : objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
return objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
@SuppressWarnings("unchecked")
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
try {
String data = cs.getString(columnIndex);
if (StrUtil.isBlank(data)||!DesensitizedUtils.isEncryptioned(data)){
return objectMapper.readValue(data, javaType);
}
// return StrUtil.isBlank(data) ? null : objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
return objectMapper.readValue(DesensitizedUtils.decrypt(data), javaType);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
5.10 EncryptionConfigProperties加密密钥
package com.zsh.test.swagger3test.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@Data
public class EncryptionConfigProperties {
/**
* 加密密钥
*/
private String secretKey = "79C37CDB1D6FAB9D0619F511B2031234";
}
5.11 UserController 用户接口
package com.zsh.test.swagger3test.controller;
import cn.hutool.core.bean.BeanUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zsh.test.swagger3test.config.Result;
import com.zsh.test.swagger3test.model.dto.UserDto;
import com.zsh.test.swagger3test.model.entity.User;
import com.zsh.test.swagger3test.model.vo.UserVo;
import com.zsh.test.swagger3test.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author ZhaoShuhao
* @data 2024/7/21 15:12
*/
@Tag(name = "用户接口")
@RestController
@RequestMapping("/user/api")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/save")
@Operation( summary= "添加用户信息")
public Result saveUserInfo(@RequestBody List<UserDto> userList) {
List<User> users = BeanUtil.copyToList(userList, User.class);
boolean b = userService.saveBatch(users);
return b ? Result.success() : Result.error("添加失败");
}
@PostMapping("/getAllUserInfo")
@Operation(summary = "查询所有用户信息")
public Result<List<UserVo>> getAllUserInfo(){
List<User> list = userService.list();
List<UserVo> userVos = BeanUtil.copyToList(list, UserVo.class);
return Result.success((userVos));
}
}
5.12 效果展示
(1)保存时加密
(2)查询时解密,明文展示