【MyBatis源码】深入分析TypeHandler原理和源码

文章目录

原始 JDBC 存在的问题

MyBatis 之所以设计了 TypeHandler(类型处理器),是为了解决 JDBC 在处理 数据类型映射 时存在的问题,简化开发者操作,并提供更灵活的类型转换机制。在 JDBC 中,我们需要手动将数据库字段与 Java 对象的属性进行映射。通常涉及调用 ResultSet.getXXX() 和 PreparedStatement.setXXX() 方法,如:

java 复制代码
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM user WHERE id = ?");
stmt.setInt(1, 123);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
    int id = rs.getInt("id");
    String name = rs.getString("name");
    Date birthDate = rs.getDate("birth_date");
}

这种映射需要针对每种数据类型手动写 get 和 set 方法,代码繁琐且容易出错。

数据库中的数据类型(如 VARCHAR、DATE、TIMESTAMP、NUMERIC 等)与 Java 的数据类型(如 String、int、double、Date 等)不完全对应,导致在处理数据时需要进行额外的类型转换。例如,数据库中的 DATE 类型可能需要转换为 Java 的 LocalDate 或 java.util.Date。

如果数据库字段的数据类型发生变化,开发者需要修改所有相关的 JDBC 代码,非常容易出错且维护成本高。原生 JDBC 无法直接处理数据库中的复杂数据类型(如 JSON、XML、枚举等)。如果需要支持这些类型,需要在代码中手动解析和转换,增加了开发难度。

自定义 TypeHandler 实现

在 MyBatis 中,我们可以通过自定义 TypeHandler 来处理 特殊的数据类型映射。一个常见的应用场景是将数据库中的 JSON 字符串与 Java 对象之间进行自动转换。下面将介绍如何自定义 TypeHandler 实现 JSON 类型的转换。

t_user表的jsonInfo存储的是json文本字符串,我希望在查询t_user数据的时候可以直接解析出来这个json文本信息为对象。

java 复制代码
public class UserBean {
  private Integer id;

  private String name;

  private String mobile_no;
  private MyObject jsonInfo;
}
@Data
public class MyObject {
  private String name;
  private Integer age;
}

实现自定义 TypeHandler

java 复制代码
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zy.client.bean.MyObject;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.*;

public class JsonTypeHandler extends BaseTypeHandler<MyObject> {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, MyObject parameter, JdbcType jdbcType) throws SQLException {
    try {
      // 将 Java 对象序列化为 JSON 字符串
      String json = objectMapper.writeValueAsString(parameter);
      ps.setString(i, json);
    } catch (JsonProcessingException e) {
      throw new SQLException("Failed to convert  JSON string.", e);
    }
  }

  @Override
  public MyObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String json = rs.getString(columnName);
    return parseJson(json);
  }

  @Override
  public MyObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String json = rs.getString(columnIndex);
    return parseJson(json);
  }

  @Override
  public MyObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String json = cs.getString(columnIndex);
    return parseJson(json);
  }

  private MyObject parseJson(String json) throws SQLException {
    try {
      if (json != null && !json.isEmpty()) {
        return objectMapper.readValue(json, MyObject.class);
      }
    } catch (JsonProcessingException e) {
      throw new SQLException("Failed to convert JSON string to MyObject.", e);
    }
    return null;
  }
}

在 mybatis-config.xml 中注册自定义的 TypeHandler:

java 复制代码
  <typeHandlers>
    <typeHandler handler="com.zy.client.test.JsonTypeHandler" javaType="java.util.List"/>
  </typeHandlers>

或者通过 @Mapper 的 @TypeHandler 注解:

java 复制代码
  @Select("select * from t_user where id=#{id}")
  @Result(column = "jsonInfo", property = "jsonInfo", typeHandler = JsonTypeHandler.class)
  UserBean selectDataById(int id);

测试类:

java 复制代码
  @Test
  public void test2() throws SQLException, IOException {
    InputStream resource = Resources.getResourceAsStream(MybatisTest.class.getClassLoader(), "mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource);
    Configuration configuration = sqlSessionFactory.getConfiguration();
    // 手动注册mapper
    configuration.addMapper(UserMapper.class);
    configuration.setProxyFactory(new MyLoggingProxyFactory());
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    UserBean userBean = mapper.selectDataById(1);
    System.out.println(userBean);
  }
java 复制代码
输出结果:
UserBean(id=1, name=zhangSan, mobile_no=188, jsonInfo=MyObject(name=Alice, age=18))

通过自定义 TypeHandler,MyBatis 可以方便地处理复杂的数据类型,避免繁琐的手动类型转换操作。尤其在处理 JSON 数据时,通过 TypeHandler 实现自动序列化和反序列化,可以极大简化代码逻辑,提高开发效率。

TypeHandler详解

java 复制代码
public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

TypeHandler接口中定义了4个方法,setParameter()方法用于为PreparedStatement对象参数的占位符设置值,另外3个重载的getResult()方法用于从ResultSet对象中获取列的值,或者获取存储过程调用结果。

BaseTypeHandler类

MyBatis中的BaseTypeHandler类实现了TypeHandler接口,对调用setParameter()方法,参数为Null的情况做了通用的处理。对调用getResult()方法,从ResultSet对象或存储过程调用结果中获取列的值出现的异常做了处理。因此,当我们需要自定义TypeHandler时,只需要继承BaseTypeHandler类即可。

在类型处理器相关类的设计中采用了模板模式,BaseTypeHandler<T>作为所有类型处理器的基类,定义了模板的框架。而在各个具体的实现类中,则实现了具体的细节。

以BaseTypeHandler中 getResult(ResultSet,String)方法为例,该方法完成了异常处理等统一的工作,而与具体类型相关的 getNullableResult(ResultSet,String)操作则通过抽象方法交给具体的类型处理器实现。这就是典型的模板模式。

java 复制代码
  @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);
    }
  }


public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

BaseTypeHandler<T>交给具体的类型处理器实现的抽象方法一共只有四个。在每个类型处理器都需要实现这四个方法。

· void setNonNullParameter(PreparedStatement,int,T,JdbcType)​:向PreparedStatement对象中的指定变量位置写入一个不为 null的值;

· T getNullableResult(ResultSet,String)​:从 ResultSet 中按照字段名读出一个可能为null的数据;

· T getNullableResult(ResultSet,int)​:从 ResultSet 中按照字段编号读出一个可能为null的数据;

· T getNullableResult(CallableStatement,int)​:从 CallableStatement中按照字段编号读出一个可能为 null的数据。

因为上面的抽象方法跟具体的类型相关,因此存在泛型参数 T。在每种类型处理器中,都给出了泛型参数的值。以 IntegerTypeHandler 为例,它设置泛型参数值为Integer。

java 复制代码
public class IntegerTypeHandler extends BaseTypeHandler<Integer>

TypeReference类型参考器

43个类型处理器可以处理不同 Java类型的数据,而这些类型处理器都是 TypeHandler接口的子类,因此可以都作为 TypeHandler来使用。

那会不会遇到一个问题,当 MyBatis取到某一个 TypeHandler 时,却不知道它到底是用来处理哪一种 Java类型的处理器?

为了解决这一问题,MyBatis 定义了一个 TypeReference 类。它能够判断出一个TypeHandler用来处理的目标类型。而它判断的方法也很简单:取出 TypeHandler实现类中的泛型参数 T的类型,这个值的类型也便是该 TypeHandler能处理的目标类型。该功能由getSuperclassTypeParameter方法实现,该方法能将找出的目标类型存入类中的 rawType属性。

java 复制代码
  Type getSuperclassTypeParameter(Class<?> clazz) {
    Type genericSuperclass = clazz.getGenericSuperclass();
    if (genericSuperclass instanceof Class) {
      // try to climb up the hierarchy until meet something useful
      if (TypeReference.class != genericSuperclass) {
        return getSuperclassTypeParameter(clazz.getSuperclass());
      }
      throw new TypeException("'" + getClass() + "' extends TypeReference but misses the type parameter. "
        + "Remove the extension or add a type parameter to it.");
    }
    Type rawType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
    if (rawType instanceof ParameterizedType) {
      rawType = ((ParameterizedType) rawType).getRawType();
    }
    return rawType;
  }

TypeReference 类是 BaseTypeHandler 的父类,因此所有的类型处理器都继承了TypeReference 的功能。这意味着对任何一个类型处理器调用getSuperclassTypeParameter方法,都可以得到该处理器用来处理的目标类型。

43个类型处理器

type 包共有 43 个类型处理器,这些类型处理器的名称也均以"TypeHandler"结尾。而 TypeHandler和 BaseTypeHandler则分别是类型处理器接口和类型处理器基类。

MyBatis 提供了多种内置的 TypeHandler,如 StringTypeHandler、IntegerTypeHandler 等,也可以自定义 TypeHandler 来满足特定需求。

StringTypeHandler 用于处理 String 类型的数据,在以下场景中会自动使用:

• 当 MyBatis 映射的 Java 类型为 String,而数据库字段的类型为 VARCHAR、CHAR、TEXT 等字符串类型时。

• 当没有为 String 类型字段显式配置 TypeHandler 时,MyBatis 会默认使用 StringTypeHandler。

java 复制代码
public class StringTypeHandler extends BaseTypeHandler<String> {

    @Override
    public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {
            ps.setNull(i, Types.VARCHAR);
        } else {
            ps.setString(i, parameter);
        }
    }

    @Override
    public String getResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
    }

    @Override
    public String getResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
    }

    @Override
    public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
}

类型注册表:3个

SimpleTypeRegistry:基本类型注册表,内部使用 Set 维护了所有 Java 基本数据类型的集合;-TypeAliasRegistry:类型别名注册表,内部使用 HashMap维护了所有类型的别名和类型的映射关系;

TypeHandlerRegistry:类型处理器注册表,内部维护了所有类型与对应类型处理器的映射关系。

定义了大量的类型处理器之后,MyBatis 还需要在遇到某种类型的数据时,快速找到与数据的类型对应的类型处理器。这个过程就需要各种类型注册表的帮助。

type 包中的类型注册表有三个:SimpleTypeRegistry、TypeAliasRegistry 和TypeHandlerRegistry。SimpleTypeRegistry 是一个非常简单的注册表,其内部使用一个 SIMPLE_TYPE_SET变量维护所有 Java基本类型。SIMPLE_TYPE_SET中的赋值是在 static代码块中进行的。这说明在 SimpleTypeRegistry初始化结束后,就已经将所有的Java基本类型维护到了 SIMPLE_TYPE_SET中。

java 复制代码
public class SimpleTypeRegistry {

  private static final Set<Class<?>> SIMPLE_TYPE_SET = new HashSet<>();

  static {
    SIMPLE_TYPE_SET.add(String.class);
    SIMPLE_TYPE_SET.add(Byte.class);
    SIMPLE_TYPE_SET.add(Short.class);
    SIMPLE_TYPE_SET.add(Character.class);
    SIMPLE_TYPE_SET.add(Integer.class);
    SIMPLE_TYPE_SET.add(Long.class);
    SIMPLE_TYPE_SET.add(Float.class);
    SIMPLE_TYPE_SET.add(Double.class);
    SIMPLE_TYPE_SET.add(Boolean.class);
    SIMPLE_TYPE_SET.add(Date.class);
    SIMPLE_TYPE_SET.add(Class.class);
    SIMPLE_TYPE_SET.add(BigInteger.class);
    SIMPLE_TYPE_SET.add(BigDecimal.class);
  }

TypeAliasRegistry是一个类型别名注册表,其内部使用 typeAliases变量维护类型的别名与类型的对应关系。有了这个注册表,我们就可以在很多场合使用类型的别名来指代具体的类型。

java 复制代码
public class TypeAliasRegistry {

  private final Map<String, Class<?>> typeAliases = new HashMap<>();

  public TypeAliasRegistry() {
    registerAlias("string", String.class);

    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);

    registerAlias("byte[]", Byte[].class);
    registerAlias("long[]", Long[].class);
    registerAlias("short[]", Short[].class);
    registerAlias("int[]", Integer[].class);
    registerAlias("integer[]", Integer[].class);
    registerAlias("double[]", Double[].class);
    registerAlias("float[]", Float[].class);
    registerAlias("boolean[]", Boolean[].class);

    registerAlias("_byte", byte.class);
    registerAlias("_long", long.class);
    registerAlias("_short", short.class);
    registerAlias("_int", int.class);
    registerAlias("_integer", int.class);
    registerAlias("_double", double.class);
    registerAlias("_float", float.class);
    registerAlias("_boolean", boolean.class);

    registerAlias("_byte[]", byte[].class);
    registerAlias("_long[]", long[].class);
    registerAlias("_short[]", short[].class);
    registerAlias("_int[]", int[].class);
    registerAlias("_integer[]", int[].class);
    registerAlias("_double[]", double[].class);
    registerAlias("_float[]", float[].class);
    registerAlias("_boolean[]", boolean[].class);

    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("bigdecimal", BigDecimal.class);
    registerAlias("biginteger", BigInteger.class);
    registerAlias("object", Object.class);

    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("bigdecimal[]", BigDecimal[].class);
    registerAlias("biginteger[]", BigInteger[].class);
    registerAlias("object[]", Object[].class);

    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);

    registerAlias("ResultSet", ResultSet.class);
  }

TypeHandlerRegistry 是这三个注册表中最为核心的一个,数据类型和相关处理器的对应关系就是由它维护的。

java 复制代码
public final class TypeHandlerRegistry {

  private final Map<JdbcType, TypeHandler<?>>  jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
  private final TypeHandler<Object> unknownTypeHandler;
  private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

  private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

  private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

  /**
   * The default constructor.
   */
  public TypeHandlerRegistry() {
    this(new Configuration());
  }

  /**
   * The constructor that pass the MyBatis configuration.
   *
   * @param configuration a MyBatis configuration
   * @since 3.5.4
   */
  public TypeHandlerRegistry(Configuration configuration) {
    this.unknownTypeHandler = new UnknownTypeHandler(configuration);

    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Byte.class, new ByteTypeHandler());
    register(byte.class, new ByteTypeHandler());
    register(JdbcType.TINYINT, new ByteTypeHandler());

    register(Short.class, new ShortTypeHandler());
    register(short.class, new ShortTypeHandler());
    register(JdbcType.SMALLINT, new ShortTypeHandler());

    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());

    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(Float.class, new FloatTypeHandler());
    register(float.class, new FloatTypeHandler());
    register(JdbcType.FLOAT, new FloatTypeHandler());

    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());

 
  }

注解类

Alias:使用该注解可以给类设置别名,设置后,别名和类型的映射关系便存入TypeAliasRegistry中;

MappedJdbcTypes:有时我们想使用自己的处理器来处理某些JDBC 类型,只需创建 BaseTypeHandler 的子类,然后在上面加上该注解,声明它要处理的JDBC类型即可;

MappedTypes:有时我们想使用自己的处理器来处理某些Java类型,只需创建BaseTypeHandler的子类,然后在上面加上该注解,声明它要处理的 Java类型即可。

枚举类

JdbcType:在 Enum中定义了所有的 JDBC类型,类型来源于java.sql.Types。

java 复制代码
public enum JdbcType {
  ARRAY(Types.ARRAY),
  BIT(Types.BIT),
  TINYINT(Types.TINYINT),
  SMALLINT(Types.SMALLINT),
  INTEGER(Types.INTEGER),
  BIGINT(Types.BIGINT),
  FLOAT(Types.FLOAT),
  REAL(Types.REAL),
  DOUBLE(Types.DOUBLE),
  NUMERIC(Types.NUMERIC)
....
}
相关推荐
武子康3 分钟前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
wqq_99225027729 分钟前
springboot基于微信小程序的食堂预约点餐系统
数据库·微信小程序·小程序
爱上口袋的天空30 分钟前
09 - Clickhouse的SQL操作
数据库·sql·clickhouse
聂 可 以2 小时前
Windows环境安装MongoDB
数据库·mongodb
web前端神器2 小时前
mongodb多表查询,五个表查询
数据库·mongodb
门牙咬脆骨2 小时前
【Redis】redis缓存击穿,缓存雪崩,缓存穿透
数据库·redis·缓存
门牙咬脆骨2 小时前
【Redis】GEO数据结构
数据库·redis·缓存
wusong9992 小时前
mongoDB回顾笔记(一)
数据库·笔记·mongodb
代码小鑫2 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
changuncle3 小时前
MongoDB数据备份与恢复(内含工具下载、数据处理以及常见问题解决方法)
数据库·mongodb