在MyBatis
中,TypeHandler
是一个核心的组件,负责处理数据库字段与Java
对象之间的类型转换。由于不同数据库系统和Java
数据类型之间存在差异,因此需要TypeHandler
来进行数据的转换,以确保数据的正确性和一致性。
主要涉及到下面这几个类:
- TypeHandler 类型转换器的顶层接口
- BaseTypeHandler 抽象类继承自TypeHandler,Mybatis中所有的类型转换器实现均继承他。
- TypeHandlerRegistry 类型转换器注册器,负责存储类型转换器。
- TypeAliasRegistry 类型别名转换器,用来存储类型与别名的对应关系。
TypeHandler的工作原理
TypeHandler 的工作原理主要体现在两个关键环节:参数设置和结果集映射。
参数设置: 当MyBatis
执行SQL
语句时,需要将用户传入的方法参数或者 Mapper XML
文件中定义的参数值设置到PreparedStatement
对象中。对于非基本类型的参数,如自定义对象、枚举或其他复杂类型,MyBatis
将通过查找对应的 TypeHandler
实现类来完成转换工作。即MyBatis
根据参数的Java
类型找到对应的TypeHandler
,然后调用其setParameter
方法,这个方法会将Java
类型的数据转换为JDBC
可识别的数据库类型,并调用PreparedStatement
的set
方法将转换后的数据写入预编译的SQL
语句中。
结果集映射: 在查询执行完毕后,MyBatis
需要将从ResultSet
中读取的数据转换成Java
类型并填充到目标对象属性上。如下即为根据jdbcType
或者javaType
获取对对应的typeHandler
。
1. TypeHandler
TypeHandler
是类型转换器的顶层接口,其定义了类型转换器应该具有的功能,
TypeHandler主要解决了两个问题:
- 可以指定我们在Java实体类所包含的自定义类型存入数据库后的类型是什么(java实体->jdbc)
- 从数据库中取出该数据后自动转换为我们自定义的Java类型(jdbc->java实体)
其源码如下:
public interface TypeHandler<T> {
/**
* 用于定义在Mybatis设置参数时该如何把Java类型的参数转换为对应的数据库类型
* @param ps 当前的PreparedStatement对象
* @param i 当前参数的位置
* @param parameter 当前参数的Java对象
* @param jdbcType 当前参数的数据库类型
* @throws SQLException
*/
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型
* @param rs 当前的结果集
* @param columnName 当前的字段名称
* @return 转换后的Java对象
* @throws SQLException
*/
T getResult(ResultSet rs, String columnName) throws SQLException;
/**
* 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型
* @param rs 当前的结果集
* @param columnIndex 当前字段的位置
* @return 转换后的Java对象
* @throws SQLException
*/
T getResult(ResultSet rs, int columnIndex) throws SQLException;
/**
* 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型
* @param cs 当前的CallableStatement执行后的CallableStatement
* @param columnIndex 当前输出参数的位置
* @return
* @throws SQLException
*/
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
2. BaseTypeHandler
BaseTypeHandler
是一个抽象类,该类实现了TypeHandler
中的方法并实现了异常捕获。继承该类我们可以很容易的实现一个自定义类型转换器,其源码如下:
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;
}
@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);
}
}
}
@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
* Colunm 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;
}
我们可以看到BaseTypeHandler对TypeHandler接口的四个方法做了一个简单的选择,把null值的情况都做了一个过滤,核心的取值和设值的方法还是抽象出来了供子类来实现。使用BaseTypeHandler还有一个好处是它继承了另外一个叫做TypeReference的抽象类,通过TypeReference的getRawType()方法可以获取到当前TypeHandler所使用泛型的原始类型。这对Mybatis在注册TypeHandler的时候是非常有好处的。在没有指定javaType的情况下,Mybatis在注册TypeHandler时可以通过它来获取当前TypeHandler所使用泛型的原始类型作为要注册的TypeHandler的javaType类型,这个在讲到Mybatis注册TypeHandler的方式时将讲到。
3. 注册TypeHandler
为什么Java自带的类型在存取的时候不会出错,我们自定义的类型就会出错?那是因为mybatis已经将这些类型的TypeHandler提前写好了,并且注册好了
具体注册了哪些,我们可以看TypeHandlerRegistry这个类:
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());
register(Reader.class, new ClobReaderTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new StringTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
register(JdbcType.NCHAR, new NStringTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler());
register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
register(JdbcType.ARRAY, new ArrayTypeHandler());
register(BigInteger.class, new BigIntegerTypeHandler());
register(JdbcType.BIGINT, new LongTypeHandler());
register(BigDecimal.class, new BigDecimalTypeHandler());
register(JdbcType.REAL, new BigDecimalTypeHandler());
register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
register(JdbcType.NUMERIC, new BigDecimalTypeHandler());
register(InputStream.class, new BlobInputStreamTypeHandler());
register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler());
register(Object.class, unknownTypeHandler);
register(Object.class, JdbcType.OTHER, unknownTypeHandler);
register(JdbcType.OTHER, unknownTypeHandler);
register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler());
register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
register(Instant.class, new InstantTypeHandler());
register(LocalDateTime.class, new LocalDateTimeTypeHandler());
register(LocalDate.class, new LocalDateTypeHandler());
register(LocalTime.class, new LocalTimeTypeHandler());
register(OffsetDateTime.class, new OffsetDateTimeTypeHandler());
register(OffsetTime.class, new OffsetTimeTypeHandler());
register(ZonedDateTime.class, new ZonedDateTimeTypeHandler());
register(Month.class, new MonthTypeHandler());
register(Year.class, new YearTypeHandler());
register(YearMonth.class, new YearMonthTypeHandler());
register(JapaneseDate.class, new JapaneseDateTypeHandler());
// issue #273
register(Character.class, new CharacterTypeHandler());
register(char.class, new CharacterTypeHandler());
}
4. 实现自定义的TypeHandler
在MyBatis
中,虽然已经提供了丰富的内置TypeHandler
来处理常见的数据类型,但在实际开发中,有时候我们可能需要处理一些特殊的数据类型或者定制化的数据转换逻辑,例如数据库中的某个字段存储的是特定格式的字符串(例如JSON数据类型),但Java
端需要将其转换为枚举或自定义对象。这时候,就需要编写自定义的TypeHandler
来进行数据处理。
我们可以直接继承BaseTypeHandler来实现我们自己的类型转换器
因为我们使用的是Spring boot工程,只需要把JsonTypeHandler放到Spring boot可以扫描的目录下即可。
在XML中使用:
<resultMap id="BaseResultMap" type="com.db.model.SettlementBill">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="receipts_code" jdbcType="VARCHAR" property="receiptsCode" />
<result column="finance_info" jdbcType="VARCHAR" property="financeDetailVO1"
javaType="com.db.model.SettlementBill"
typeHandler="com.mybatis.handler.JsonArrayTypeHandler"
/>
</resultMap>
<select id="test" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select id, receipts_code, finance_info from settlement_bill where id = #{id,jdbcType=INTEGER}
</select>
Mybatis Plus中的使用方式:
@TableName(value = "settlement_bill", autoResultMap = true)
@Data
public class SettlementBill extends CxmBaseModel implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "finance_info",typeHandler = JsonTypeHandler.class)
private BatchFinanceDetailVO1 financeDetailVO1;
}
注意:一定要标注 autoResultMap = true
5. 使用场景
1、入库加解密
public class EncryptTypeHandler extends BaseTypeHandler<String> {
private static final String key = "abc";
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s,
JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, DesUtils.encrypt(s, key));
}
@Override
public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
return DesUtils.decrypt(resultSet.getString(s), key);
}
@Override
public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
return DesUtils.decrypt(resultSet.getString(i), key);
}
@Override
public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return DesUtils.decrypt(callableStatement.getString(i), key);
}
}
@Data
@TableName(value = "user01", autoResultMap = true)
public class User extends BaseEntity {
private String name;
private Integer age;
@TableField(typeHandler = EncryptTypeHandler.class)
private String email;
}
2、JSON序列化与反序列化
我们定义一个专门处理数据JSON
类型数据与Java
对象相互转换的一个抽象的TypeHandler
,它继承了BaseTypeHandler
。
import com.bigo.web.springboot_demo008.domain.AddressBo;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.springframework.util.StringUtils;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public abstract class JsonBaseTypeHandler<T> extends BaseTypeHandler<T> {
private static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//使用null表示集合类型字段是时不抛异常
objectMapper.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
//对象为空时不抛异常
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
private T parse(String json) {
try {
if (StringUtils.isEmpty(json)) {
return null;
}
return (T) objectMapper.readValue(json, specificType());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String toJsonString(T obj) {
if (obj == null) {
return "";
}
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
String content = parameter == null ? null : toJsonString(parameter);
ps.setString(i, content);
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.parse(rs.getString(columnName));
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.parse(rs.getString(columnIndex));
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.parse(cs.getString(columnIndex));
}
protected abstract TypeReference<AddressBo> specificType();
}
这样一个通用的JSON
转Java
对象的通用Handler
就完成了。然后我们具体的字段转换到相应的Java
对象时,只需要继承这个抽象类,把Java
对象传递过去即可。
public class AdressTypeHandler extends JsonBaseTypeHandler<AddressBo>{
@Override
protected TypeReference<AddressBo> specificType() {
return new TypeReference<AddressBo>() {};
}
}
然后我们分别指定查询的ResultMap
以及插入的sql
中的address
字段的TypeHandler
为AddressTypeHandler
的全路径。
@Data
@TableName(value = "user01", autoResultMap = true)
public class User extends BaseEntity {
private String name;
private Integer age;
@TableField(typeHandler = EncryptTypeHandler.class)
private String email;
@TableField(typeHandler = AdressTypeHandler.class)
private AddressBo address;
}
在MyBatis
框架中,采用自定义TypeHandler
实现JSON
数据类型字段与Java
对象的相互转换具有显著的优势。通过精心设计和实现TypeHandler
,可以精准把控从Java
对象到JSON
字符串以及反向转换的过程,确保数据在存入数据库时按照预设格式可靠地序列化,并在读取时准确无误地还原为对应的Java
实体,从而有效避免因数据格式不兼容引发的运行时异常或数据损坏问题。同时,利用TypeHandler
将数据持久化的具体逻辑进行抽象封装,使业务代码得以聚焦核心功能,不受底层数据库交互细节的影响,极大提升了代码的可读性和维护性。而在整个项目范围内统一应用自定义的TypeHandler
,有利于维持数据操作的一致性和标准化,消除了由于开发人员使用不同处理策略带来的潜在风险,有力推动了项目的整体开发效率和维护质量提升。
3、数据类型转化
public class BigInt2PlaceBigDecimalHandler extends BaseTypeHandler<BigDecimal> {
private final static int SCALE = 2;
private final static BigDecimal COEFFICIENT = BigDecimal.valueOf(Math.pow(10, SCALE));
@Override
public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType) throws SQLException {
BigDecimal decimal = parameter.multiply(COEFFICIENT);
long longValue = decimal.longValue();
ps.setLong(i, longValue);
}
@Override
public BigDecimal getNullableResult(ResultSet rs, String columnName) throws SQLException {
BigDecimal decimal = new BigDecimal(rs.getLong(columnName));
return decimal.divide(COEFFICIENT, SCALE, RoundingMode.HALF_UP);
}
@Override
public BigDecimal getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
BigDecimal decimal = new BigDecimal(rs.getLong(columnIndex));
return decimal.divide(COEFFICIENT, SCALE, RoundingMode.HALF_UP);
}
@Override
public BigDecimal getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
BigDecimal decimal = new BigDecimal(cs.getLong(columnIndex));
return decimal.divide(COEFFICIENT, SCALE, RoundingMode.HALF_UP);
}
}
注意事项:mybatisPlus如果使用wrapper来更新字段时,那么不会生效。更新保存与查询必须以对象为维度。