MyBatis-Plus 自定义字段类型处理器 BaseTypeHandler

2,877 阅读4分钟

使用场景

在使用MyBatisPlus时往往会遇见JavaType与JDBCType之间的类型转换或一些敏感数据内容进行编码处理的场景时,就可以使用 BaseTypeHandler,其进行类型转换的原理类似于拦截器。
比如:

  • 实体类中字段存储类型为Map<String,?>,而映射到数据库的字段类型是varchar。
  • 用户名、密码、身份证号展示给客户端页面时需要进行特殊编码处理。

BaseTypeHandler 抽象类

这里需要提到一个MyBatis中一个非常重要的泛型接口TypeHandler(类型处理器),用于处理参数类型,包括入参形式和返回结果集相关参数的转换。从源码中看到该接口定义了四个方法,需要子类去继承并实现以下方法:

image.png

  • setParameter() 方法:使用TypeHandler通过PreparedStatement对象进行设置SQL参数的时候使用的具体方法,其中i是参数在SQL的下标,parameter是参数,jdbcType是数据库类型。
  • getResult() 方法:从JDBC结果集中获取数据进行转换,用于为设置字段对应的结果集,可以通过字段的名字和下标来设置对应的结果集。该方法也是BaseTypeHandler类的一个抽象方法,通过子类的getNullableResult()方法实现结果集的填充。

BaseTypeHandler 其实就是实现了TypeHandler接口本身的方法外,还扩展了相关的方法,后续MyBatis包中的类型处理器都是他的实现类,为了满足我们的需求,创建自定义类去实现BaseTypeHandler中的抽象方法即可:

public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {

  /**
   * @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
   */
  @Deprecated
  protected Configuration configuration;

  /**
   * Sets the configuration.
   *
   * @param c
   *          the new configuration
   * @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This property will remove future.
   */
  @Deprecated
  public void setConfiguration(Configuration c) {
    this.configuration = c;
  }

 /**
  * 当参数 parameter 和 jdbcType 同时为空时,MyBatis 将抛出异常。如果能明确 jdbcType,则会进行空设置;如果参数不为空,那么它将采用 setNonNullParameter 方法设置参数
  */
  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
  }
    
  /**
  * 非空结果集是通过 getNullableResult 方法获取的。如果判断为空,则返回 null。
  */
  @Override
  public T getResult(ResultSet rs, String columnName) throws SQLException {
    try {
      return getNullableResult(rs, columnName);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
    }
  }

  @Override
  public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(rs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set.  Cause: " + e, e);
    }
  }

  @Override
  public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(cs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from callable statement.  Cause: " + e, e);
    }
  }

  public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * Gets the nullable result.
   *
   * @param rs
   *          the rs
   * @param columnName
   *          Column name, when configuration <code>useColumnLabel</code> is <code>false</code>
   * @return the nullable result
   * @throws SQLException
   *           the SQL exception
   */
  public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

  public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;

  public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;

}

直接上代码:首先需要在实体类中标记需要进行类型转换的字段,然后是在Mapper.xml中对映射字段添加typeHandler属性

SysUser 系统用户实体

@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "用户表(SysUser)表实体类")
@TableName(value = "sys_user", autoResultMap = true)//必要的注解, 其意思类似生成策略,autoResultMap并不能使用在自定义的方法上,只在MyBatis Plus内置方法上生效
public class SysUser extends Model<SysUser> {
    private static final long serialVersionUID = 1L;
    /**
     * 用户ID,主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty(value = "用户ID,主键")
    private Long id;
    /**
     * 登录账号
     */
    @ApiModelProperty(value = "登录账号")
    private String username;
    /**
    * 密码
    */
    @ApiModelProperty(value = "密码")
    private String password;

    /**
     * 用户邮箱
     */
    @TableField(value = "email", typeHandler = BaseMybatisDataEncoderHandler.class)//标注需要类型转换的字段
    @ApiModelProperty(value = "用户邮箱")
    private String email;
    /**
     * 手机号码
     */
    @TableField(value = "phone_number", typeHandler = BaseMybatisDataEncoderHandler.class)
    @ApiModelProperty(value = "手机号码")
    private String phoneNumber;
    /**
     * 身份证
     */
    @TableField(value = "phone_number", typeHandler = BaseMybatisDataEncoderHandler.class)
    @ApiModelProperty(value = "身份证")
    private String idcard;
}

SysUserMapper.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="eca.basicservice.admin.biz.mapper.SysUserMapper">
    <resultMap id="BaseResultMap" type="eca.basicservice.admin.api.vo.UserVo">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="email" property="email"
                typeHandler="cn.common.mybatis.handler.BaseMybatisDataEncoderHandler"/>
        <result column="phone_number" property="phoneNumber"
                typeHandler="cn.common.mybatis.handler.BaseMybatisDataEncoderHandler"/>
        <result column="idcard" property="phoneNumber"
                typeHandler="cn.common.mybatis.handler.BaseMybatisDataEncoderHandler"/>
    </resultMap>
</mapper>

BaseMybatisDataEncoderHandler 数据编码处理器

@MappedTypes({String.class})//MappedTypes映射类型,标记当前输入字段JavaType类型为String
@MappedJdbcTypes(JdbcType.VARCHAR)//MappedJdbcTypes:映射JDBC类型,表示输入字段需要映射到JDBC中的类型
public class BaseMybatisDataEncoderHandler extends BaseTypeHandler<String> {

    /**
     * 这里放置数据编码和解码时的密钥,一般定义在yml中映射的ConfigProperties对象中
     */
    private final String ENCODE_KEY="123456";

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
        preparedStatement.setString(i, encrypt(s));
    }

    @Override
    public String getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        String data = resultSet.getString(columnName);
        return StrUtil.isBlank(data) ? null : decrypt(data);
    }

    @Override
    public String getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        String data = resultSet.getString(columnIndex);
        return StrUtil.isBlank(data) ? null : decrypt(data);
    }

    @Override
    public String getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        String data = callableStatement.getString(columnIndex);
        return StrUtil.isBlank(data) ? null : decrypt(data);
    }

    /**
     * 明文加密
     *
     * @param plaintext
     * @return
     */
    private String encrypt(String plaintext) {
        return AesUtils.encrypt(plaintext, ENCODE_KEY.trim());
    }

    /**
     * 密文解密
     *
     * @param ciphertext
     * @return
     */
    private String decrypt(String ciphertext) {
        return AesUtils.decrypt(ciphertext, ENCODE_KEY.trim());
    }
}