关于@TableField中TypeHandler属性,自定义的类型处理器的使用(密码加密与解密举例)

注:另一种方式参考关于@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 的 PreparedStatementResultSet 来处理 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;

java 复制代码
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用户视图对象

java 复制代码
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用户入参对象

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

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

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

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

java 复制代码
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加密类型处理器

java 复制代码
package com.zsh.test.swagger3test.handler;


public class Sm4TypeHandler extends Sm4AbstractObjectTypeHandler<String> {


}

5.9 Sm4AbstractObjectTypeHandler密码sm4加密类型处理器具体业务类

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

java 复制代码
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 用户接口

java 复制代码
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)查询时解密,明文展示

相关推荐
魔道不误砍柴功5 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2345 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨8 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity3 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天3 小时前
java的threadlocal为何内存泄漏
java