文章目录
- [原始 JDBC 存在的问题](#原始 JDBC 存在的问题)
- [自定义 TypeHandler 实现](#自定义 TypeHandler 实现)
- TypeHandler详解
- BaseTypeHandler类
- TypeReference类型参考器
- 43个类型处理器
- 类型注册表:3个
- 注解类
- 枚举类
原始 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)
....
}