MyBatis 類型處理器(typeHandlers)簡介及源碼分析 ...
一、類型處理器(typeHandlers)簡介
typeHandlers又名類型管理器,類似於JDBC裡面將資料庫類型轉換成JAVA類型的功能一樣,typeHandlers就是MyBatis的類型轉換器。和別名一樣,MyBatis中的類型處理器也存在系統定義的和自定義兩種,MyBatis會根據javaType和jdbcType來決定採用哪個typeHandler來處理這些轉換規則,而且系統定義的能滿足大部分需求,可以說是非常好用,用戶只需要自定義一些特有的轉換規則,如枚舉類型。無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。
在myBatis中註冊類型管理器非常簡單,只需要在配置文件中配置下麵的的標簽:
<!-- Configuration中有兩種方式註冊類型管理器 --> <typeHandlers> <!-- 直接註冊類型管理器 --> <typeHandler handler="org.mybatis.example.ExampleTypeHandler" javaType="java.util.Date" jdbcType="TIMESTAMP"/> <!-- 定義一個掃描包,該包下麵的所有類全部註冊 --> <package name="org.mybatis.example"/> </typeHandlers>
具體MyBatis是如何將配置文件解析註冊的可以參考我的上一篇文章:MyBatis學習筆記(三) Configuration類
二、系統自定義的typeHandlers
MyBatis在初始化配置的時候,預設註冊了豐富的類型處理器,基本上這些處理器就能滿足我們大部分需求,下麵我們來看看都有那些:
以下是在TypeHandlerRegistry類裡面註冊的
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()); 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 ClobTypeHandler()); 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 ClobTypeHandler()); 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, UNKNOWN_TYPE_HANDLER); register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER); register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER); 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()); // mybatis-typehandlers-jsr310 try { register("java.time.Instant", "org.apache.ibatis.type.InstantTypeHandler"); register("java.time.LocalDateTime", "org.apache.ibatis.type.LocalDateTimeTypeHandler"); register("java.time.LocalDate", "org.apache.ibatis.type.LocalDateTypeHandler"); register("java.time.LocalTime", "org.apache.ibatis.type.LocalTimeTypeHandler"); register("java.time.OffsetDateTime", "org.apache.ibatis.type.OffsetDateTimeTypeHandler"); register("java.time.OffsetTime", "org.apache.ibatis.type.OffsetTimeTypeHandler"); register("java.time.ZonedDateTime", "org.apache.ibatis.type.ZonedDateTimeTypeHandler"); } catch (ClassNotFoundException e) { // no JSR-310 handlers } // issue #273 register(Character.class, new CharacterTypeHandler()); register(char.class, new CharacterTypeHandler());
其中當屬StringTypeHandler用的最頻繁,下麵我們看下它的源碼
/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * @author Clinton Begin */ public class StringTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } }
從上述代碼可以看出它繼承了一個叫做BaseTypeHandler<String>的類,這個類的範型是String,即javaType,幾個方法如下:
1⃣️setNonNullParameter:這個方法是用來將javaType轉換成jdbcTpe
2⃣️getNullableResult:這個方法用來將從結果集根據列名稱獲取到的數據的jdbcType轉換成javaType
3⃣️getNullableResult:這個方法用來將從結果集根據列索引獲取到的數據的jdbcType轉換成javaType
4⃣️getNullableResult:這個方法用在存儲過程中
其實主要就是完成不同類型之間的轉換,下麵來看一下它所繼承的BaseTypeHandler<String>類
再來看看BaseTypeHandler<String>類的源碼
/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.executor.result.ResultMapException; import org.apache.ibatis.session.Configuration; /** * @author Clinton Begin * @author Simone Tripodi */ public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> { protected Configuration configuration; 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 { T result; try { result = getNullableResult(rs, columnName); } catch (Exception e) { throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e); } if (rs.wasNull()) { return null; } else { return result; } } @Override public T getResult(ResultSet rs, int columnIndex) throws SQLException { T result; try { result = getNullableResult(rs, columnIndex); } catch (Exception e) { throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from result set. Cause: " + e, e); } if (rs.wasNull()) { return null; } else { return result; } } @Override public T getResult(CallableStatement cs, int columnIndex) throws SQLException { T result; try { result = getNullableResult(cs, columnIndex); } catch (Exception e) { throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from callable statement. Cause: " + e, e); } if (cs.wasNull()) { return null; } else { return result; } } public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; 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; }
此類是一個抽象類,重寫了四個介面之外,還有四個抽象方法,也就是在StringTypeHandler中實現的介面,那麼重寫的四個介面是什麼呢?我們去看一下它所實現的TypeHandler<T>介面
最後看一下TypeHandler<T>介面的源碼
/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * @author Clinton Begin */ 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; }
這裡也定義了四個介面,一個set方法用來將javaType轉為jdbcType,三個get方法用來將jdbcType轉為javaType,而在BaseTypeHandler中實現的就是這四個方法。
所以當我們自定義自己的typeHandler時有兩種方法:
第一種:繼承BaseTypeHandler類
第二種:實現TypeHandler介面
三、自定義typeHandler
自定義typeHandler用的最多的應該是枚舉類型了
若想映射枚舉類型 Enum,則需要從 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中選一個來使用。
比如說我們想存儲取近似值時用到的舍入模式。預設情況下,MyBatis 會利用 EnumTypeHandler 來把 Enum 值轉換成對應的名字。
註意 EnumTypeHandler 在某種意義上來說是比較特別的,其他的處理器只針對某個特定的類,而它不同,它會處理任意繼承了 Enum 的類。
不過,我們可能不想存儲名字,相反我們的 DBA 會堅持使用整形值代碼。那也一樣輕而易舉: 在配置文件中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可, 這樣每個 RoundingMode 將通過他們的序數值來映射成對應的整形數值。
四、使用typeHandler
MyBatis定義註冊的typeHandler不需要在mapper文件中顯示的聲明,MyBatis會自動的使用,我們自己定義的需要在結果集中顯示的指出,告訴MyBatis,“這兩個類型按照我定義的類轉換”,使用也非常簡單,下麵是官方給出的實例。
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.apache.ibatis.submitted.rounding.Mapper"> <resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="funkyNumber" property="funkyNumber"/> <result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/> </resultMap>
<select id="getUser2" resultMap="usermap2"> select * from users2 </select>
<insert id="insert2"> insert into users2 (id, name, funkyNumber, roundingMode) values ( #{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler} ) </insert> </mapper>