解析Mybatis的typeHandlers元素,配置Mybatis的類型轉(zhuǎn)換器
在學(xué)習(xí)本章內(nèi)容之前,可以通過類型轉(zhuǎn)換器(typeHandlers)來了解關(guān)于
TypeHandler的用法。
示例
為了更好的理解mybatis的TypeHandler對象,我們在測試包org.apache.learning下,新建一個(gè)typehandler包,該包下的所有數(shù)據(jù),均用于演示TypeHandler對象的用法。
首先我們在包下新建一個(gè)CreateDB.sql的腳本文件:
drop table users if exists;
create table users
(
id varchar(36),
name varchar(20),
gender varchar(6)
);
insert into users (id, name,gender) values ('38400000-8cf0-11bd-b23e-10b96e4ef00d', 'Panda', '男');
該腳本用于創(chuàng)建一個(gè)users表,并插入一條數(shù)據(jù)以供使用。
之后我們新建一個(gè)mybatis-config.xml文件,該文件用于為mybatis提供一個(gè)基本的數(shù)據(jù)源環(huán)境配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<!-- 使用 hsql 內(nèi)存數(shù)據(jù)庫 -->
<dataSource type="UNPOOLED">
<property name="driver" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:type_handler_test"/>
<property name="username" value="sa"/>
</dataSource>
</environment>
</environments>
</configuration>
然后創(chuàng)建一個(gè)名為EUserGender的枚舉對象:
/**
* 用戶性別
*
* @author HanQi [Jpanda@aliyun.com]
* @version 1.0
* @since 2020/3/14 10:47
*/
@Getter
@AllArgsConstructor
public enum EUserGender {
MALE("男"), FEMALE("女");
private String name;
}
并為EUserGender創(chuàng)建一個(gè)專屬的TypeHandler——EUserGenderTypeHandler:
public class EUserGenderTypeHandler extends BaseTypeHandler<EUserGender> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, EUserGender parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getName());
}
@Override
public EUserGender getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
return findEUserGender(columnValue);
}
@Override
public EUserGender getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return findEUserGender(columnValue);
}
@Override
public EUserGender getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return findEUserGender(columnValue);
}
private EUserGender findEUserGender(String dbName) {
return Stream.of(EUserGender.values())
.filter(g -> g.getName().equals(dbName))
.findFirst()
.orElse(null);
}
}
再新建一個(gè)名為User的實(shí)體對象,對象中的屬性定義和創(chuàng)建的users表中的列保持一致:
@Getter
@Setter
@NoArgsConstructor
public class User {
private String id;
private String name;
private EUserGender gender;
}
緊接著新建一個(gè)Mapper接口,該接口定義了操作User對象的兩個(gè)方法:
public interface Mapper {
List<User> selectByGender(EUserGender gender);
List<User> selectByGenderWithoutSpecify(EUserGender gender);
}
以及Mapper對應(yīng)的xml文件——Mapper.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="org.apache.learning.typehandler.Mapper">
<resultMap type="org.apache.learning.typehandler.User"
id="userResult">
<result property="id" column="id"/>
<result property="name" column="name"/>
<!-- 指定屬性對應(yīng)的TypeHandler-->
<result property="gender" column="gender"
typeHandler="org.apache.learning.typehandler.EUserGenderTypeHandler"/>
</resultMap>
<!-- 指定屬性對應(yīng)的TypeHandler-->
<select id="selectByGender" resultMap="userResult">
select *
from users
where gender = #{gender, typeHandler=org.apache.learning.typehandler.EUserGenderTypeHandler}
</select>
<!-- 未指定屬性對應(yīng)的TypeHandler-->
<select id="selectByGenderWithoutSpecify" resultType="org.apache.learning.typehandler.User">
select *
from users
where gender = #{gender}
</select>
</mapper>
其中selectByGender()的方法針對gender屬性明確指定了EUserGenderTypeHandler作為類型轉(zhuǎn)換器,selectByGenderWithoutSpecify()方法只是一個(gè)普通的方法定義。
完成上述的基礎(chǔ)數(shù)據(jù)的準(zhǔn)備之后,新建一個(gè)名為typeHanlderTest的單元測試類,測試常見的三種場景:
/**
* 類型轉(zhuǎn)換器測試類
*
* @author HanQi [Jpanda@aliyun.com]
* @version 1.0
* @since 2020/3/14 10:45
*/
public class TypeHandlerTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeEach
@SneakyThrows
public void setup() {
@Cleanup
Reader reader = Resources.getResourceAsReader("org/apache/learning/typehandler/mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
sqlSessionFactory.getConfiguration().addMapper(Mapper.class);
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), "org/apache/learning/typehandler/CreateDB.sql");
}
/**
* 聲明語句時(shí),指定類型轉(zhuǎn)換器
*/
@Test
public void testForCustomTypeHandler() {
// 執(zhí)行準(zhǔn)備工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 執(zhí)行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGender(EUserGender.MALE);
Assertions.assertEquals("Panda", users.get(0).getName());
}
/**
* 普通方法且無類型轉(zhuǎn)換器
*/
@Test
public void testForCustomTypeHandlerWithoutSpecify() {
// 執(zhí)行準(zhǔn)備工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 執(zhí)行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
Assertions.assertTrue(users.isEmpty());
}
/**
* 測試普通方法觸發(fā)全局類型轉(zhuǎn)換器
*/
@Test
public void testForCustomTypeHandlerWithoutSpecifyNeedSuccess() {
// 注冊全局的類型轉(zhuǎn)換器
sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register(EUserGenderTypeHandler.class);
// 執(zhí)行準(zhǔn)備工作
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
// 執(zhí)行
Mapper mapper = sqlSession.getMapper(Mapper.class);
List<User> users = mapper.selectByGenderWithoutSpecify(EUserGender.MALE);
Assertions.assertEquals("Panda", users.get(0).getName());
}
}
毫無疑問,上面的單元測試都能正常運(yùn)行,這也就意味著我們自定義的TypeHandler生效了,那么TypeHandler到底是一個(gè)怎樣的東西呢?
類型轉(zhuǎn)換器——TypeHandler
TypeHandler是myabtis中定義的負(fù)責(zé)jdbc類型和java類型互相轉(zhuǎn)換的策略接口,我稱之為類型轉(zhuǎn)換器。
TypeHandler接口有一個(gè)泛型參數(shù)定義,這個(gè)泛型參數(shù)就是要處理的java類型,該定義還約束了標(biāo)準(zhǔn)的類型轉(zhuǎn)換器需要提供下列四種方法:
- 根據(jù)參數(shù)索引和jdbc類型設(shè)置
PreparedStatement語句中的值,即將?替換為實(shí)際值的setParameter(PreparedStatement,int,T,JdbcType). - 獲取指定
ResultSet中指定名稱的列對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型的getResult(ResultSet,String)。 - 獲取指定
ResultSet中指定索引下面的列對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型的getResult(ResultSet,int)。 - 獲取指定存儲過程(
CallableStatement)中指定索引下標(biāo)對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型的getResult(CallableStatement,int。
/**
* 類型轉(zhuǎn)換處理器
*
* @author Clinton Begin
*/
public interface TypeHandler<T> {
/**
* 根據(jù)參數(shù)索引和jdbc類型設(shè)置PreparedStatement語句中的值,即將? 替換為實(shí)際值。
*
* @param ps PreparedStatement 語句
* @param i 參數(shù)索引
* @param parameter 參數(shù)
* @param jdbcType jdbc類型
* @throws SQLException SQL異常
*/
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* 獲取指定ResultSet中指定名稱的列對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型
*
* @param rs ResultSet
* @param columnName 列名稱
* @return 值
* @throws SQLException SQL異常
*/
T getResult(ResultSet rs, String columnName) throws SQLException;
/**
* 獲取指定ResultSet中指定索引下面的列對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型
*
* @param rs ResultSet
* @param columnIndex 列索引
* @return 值
* @throws SQLException SQL異常
*/
T getResult(ResultSet rs, int columnIndex) throws SQLException;
/**
* 獲取指定存儲過程(CallableStatement)中指定索引下標(biāo)對應(yīng)的值,并轉(zhuǎn)換為相應(yīng)的JAVA類型
*
* @param cs 存儲過程
* @param columnIndex 列索引
* @return 值
* @throws SQLException SQL異常
*/
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
為了簡化我們的操作,Mybatis定義了一個(gè)名為BaseTypeHandler的抽象類,實(shí)現(xiàn)了TypeHandler接口,他是Mybatis提供的一個(gè)默認(rèn)的類型轉(zhuǎn)換器的基類。
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
/**
* Mybatis配置
*
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
*/
@Deprecated
protected Configuration 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;
}
/**
* @param ps PreparedStatement
* @param i 參數(shù)索引
* @param parameter 參數(shù)值
* @param jdbcType jdbc類型
*/
@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;
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實(shí)現(xiàn)了TypeHandler接口定義的默認(rèn)方法,添加了基本的數(shù)據(jù)校驗(yàn),并暴露出了新的抽象方法供子類實(shí)現(xiàn)。
雖然他還添加了對Mybatis配置類
Configuration的引用,并提供了對應(yīng)的設(shè)值方法,但是這一功能已被棄用,所以這里就不在討論。
獲取泛型定義原始類型——TypeReference
除此之外,BaseTypeHandler還繼承了TypeReference抽象類,提供了獲取泛型定義原始類型的能力。
public abstract class TypeReference<T> {
/**
* TypeReference<T> 中T的泛型類型
*/
private final Type rawType;
protected TypeReference() {
// 獲取當(dāng)前類的泛型類型
rawType = getSuperclassTypeParameter(getClass());
}
// ... 省略 getSuperclassTypeParameter() 方法
}
TypeReference抽象類中提供了一個(gè)Type類型的rawType屬性,并對外了暴露了該屬性的getter方法,該屬性的作用是保存類上泛型定義的類型,他的初始化工作是在TypeReference的構(gòu)造方法中完成的。
有關(guān)于泛型解析相關(guān)的知識,我們在介紹TypeParameterResolver時(shí)已經(jīng)做了很完整的分析,這里就不在贅述。
我們自己定義的
EUserGenderTypeHandler就繼承了BaseTypeHandler基類,并通過泛型參數(shù)指定了要處理的java類型是EUserGender。
存儲類型轉(zhuǎn)換器的注冊表——TypeHandlerRegistry
在簡單的了解了TypeHandler之后,我們回過頭來繼續(xù)看TypeHandlerRegistry。
Tips:
我想換一種方式來描述對象的功能和實(shí)現(xiàn),之前描述一個(gè)類的源碼時(shí),都是站在程序員的角度去想,去寫,按照方法調(diào)用棧來解析源代碼?,F(xiàn)在我想站在產(chǎn)品的角度來解析代碼,
先給出實(shí)例存在的目的和宗旨,并圍繞著這個(gè)目的和宗旨,逐步的擴(kuò)展出代碼。
TypeHandlerRegistry的作用
TypeHandlerRegistry作為存儲類型轉(zhuǎn)換器的注冊表,他存在的目的就是為了保存所有類型轉(zhuǎn)換器,并提供相應(yīng)的注冊和獲取類型轉(zhuǎn)換器的功能。
編程是一件開放性很高的事情,一千個(gè)人眼中就有一千個(gè)哈姆雷特,一千個(gè)程序員對于同一個(gè)功能可能有不止一萬種實(shí)現(xiàn)方案。
所以在接下來我們會根據(jù)需求去梳理TypeHandlerRegistry的實(shí)現(xiàn),而不是去重新實(shí)現(xiàn)一個(gè)新的TypeHandlerRegistry。
TypeHandlerRegistry的屬性定義
首先我們看一下TypeHandlerRegistry中的屬性定義:
/**
* jdbc類型和處理器的映射關(guān)系
*/
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
/**
* java類型和映射處理器的關(guān)系
*/
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
/**
* 未知類型的對應(yīng)的默認(rèn)處理器
*/
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
/**
* 處理器類型和處理器實(shí)例的映射關(guān)系
*/
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
/**
* 空的類型轉(zhuǎn)換器映射關(guān)系,僅僅是一個(gè)標(biāo)志性對象
*/
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
/**
* 默認(rèn)的枚舉類型轉(zhuǎn)換器
*/
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
仔細(xì)看前兩個(gè)屬性的定義,JDBC_TYPE_HANDLER_MAP用于存放JdbcType和TypeHandler的對應(yīng)關(guān)系。
TYPE_HANDLER_MAP的key值是一個(gè)java類型,value是Map<JdbcType, TypeHandler<?>>集合。
這兩個(gè)屬性的定義很容易讓人誤認(rèn)為:TYPE_HANDLER_MAP的value存放是的指定java類型對應(yīng)的JDBC_TYPE_HANDLER_MAP集合。
但是,事實(shí)上,在梳理源碼后,我們會發(fā)現(xiàn)二者毫無關(guān)系。
JDBC_TYPE_HANDLER_MAP的作用是:維護(hù)一個(gè)JdbcType和其默認(rèn)的TypeHandler之間的對應(yīng)關(guān)系。
TYPE_HANDLER_MAP的作用是:維護(hù)java類型和其所有可轉(zhuǎn)換的JdbcType的關(guān)系,并保留負(fù)責(zé)進(jìn)行轉(zhuǎn)換操作的TypeHandler實(shí)例。
換句話,TYPE_HANDLER_MAP集合中的value雖然類型也是Map<JdbcType, TypeHandler<?>>,但是他存儲的JdbcType和TypeHandler的對應(yīng)關(guān)系可能會因?yàn)樗鶎俚?code>java類型的不同而不同。
比如:
var TYPE_HANDLER_MAP={
'Integer':{
"JdbcType.INTEGER":"IntegerTypeHandler"
},
'Double':{
"JdbcType.INTEGER":"DoubleTypeHandler"
}
}
在上面這個(gè)通過JS描述的偽對象TYPE_HANDLER_MAP中,我們可以看到隨著java類型由Integer變成Double,JdbcType.INTEGER對應(yīng)的TypeHandler實(shí)例也由IntegerTypeHandler變成了DoubleTypeHandler。
除了這個(gè)容易讓人混淆的問題之外,還有一個(gè)問題困擾了我很久,按照前面的理解,一個(gè)jdbc類型應(yīng)該是可以對應(yīng)多個(gè)TypeHandler對象的,畢竟,一個(gè)jdbc類型可能會轉(zhuǎn)換成不同的JAVA對象,可是按照JDBC_TYPE_HANDLER_MAP的定義,一個(gè)jdbc類型只能對應(yīng)一個(gè)TypeHandler,這是為什么呢?
這是因?yàn)?code>JDBC_TYPE_HANDLER_MAP負(fù)責(zé)存儲的是JdbcType和其默認(rèn)的TypeHandler之間的對應(yīng)關(guān)系,JDBC_TYPE_HANDLER_MAP是TYPE_HANDLER_MAP的一個(gè)補(bǔ)充,當(dāng)TYPE_HANDLER_MAP中無法獲取TypeHandler時(shí),就會考慮根據(jù)jdbc類型從JDBC_TYPE_HANDLER_MAP中獲取一個(gè)默認(rèn)的TypeHandler來完成類型轉(zhuǎn)換的處理操作。
TypeHandlerRegistry還有其他幾個(gè)屬性,被final修飾的UNKNOWN_TYPE_HANDLER屬性是一個(gè)UnknownTypeHandler類型的類型轉(zhuǎn)換器。
UnknownTypeHandler雖然聽起來像是用于處理未知類型,但是實(shí)際上在他的實(shí)現(xiàn)中,我們會發(fā)現(xiàn)他更偏向于一個(gè)代理對象,他會在運(yùn)行期間,動態(tài)的去選擇一個(gè)有效的TypeHandler實(shí)例來完成類型轉(zhuǎn)換的工作。
類型為Map<JdbcType, TypeHandler<?>>的NULL_TYPE_HANDLER_MAP屬性和JDBC_TYPE_HANDLER_MAP也沒有任何關(guān)系,他是一個(gè)標(biāo)志性的對象,用來表示一個(gè)空的類型轉(zhuǎn)換器映射關(guān)系。
類型為Map<Class<?>, TypeHandler<?>>的ALL_TYPE_HANDLERS_MAP屬性,他的key是TypeHandler實(shí)例的具體類型,他的value則是TypeHandler實(shí)例本身,他的作用是維護(hù)所有有效的TypeHandler。
最后一個(gè)類型為Class<? extends TypeHandler>的defaultEnumTypeHandler屬性,則定義了默認(rèn)的枚舉類型轉(zhuǎn)換器。
枚舉類型轉(zhuǎn)換器實(shí)際上是一個(gè)比較特殊的處理器,他有別于普通的處理器只用于處理特定的類,枚舉類型轉(zhuǎn)換器會處理任意繼承了Enum的類。
注冊類型轉(zhuǎn)換器功能的實(shí)現(xiàn)
TypeHandlerRegistry的幾個(gè)集合類型的屬性已經(jīng)為我們提供了保存類型轉(zhuǎn)換器的功能,現(xiàn)在我們看一下TypeHandlerRegistry如何實(shí)現(xiàn)注冊類型轉(zhuǎn)換器的功能。
現(xiàn)在已經(jīng)知道一個(gè)類型轉(zhuǎn)換器核心的數(shù)據(jù)有:被處理的Java類型,被處理的JDBC類型以及類型轉(zhuǎn)換器TypeHandler本身這三個(gè)核心要素,同時(shí)Java類型和JDBC類型之間是一個(gè)多對多的關(guān)系。
那么就注冊功能的實(shí)現(xiàn)來講,最好的場景就是我們能夠直接拿到Java類型和JDBC類型以及相應(yīng)的類型轉(zhuǎn)換器TypeHandler實(shí)例這三個(gè)參數(shù)。
如果不能,那就退而求其次,獲取類型轉(zhuǎn)換器TypeHandler加上Java類型與JDBC類型中的一個(gè)。
如果還不行,只有類型轉(zhuǎn)換器TypeHandler也可以。
對于那些缺失的參數(shù),我們的實(shí)現(xiàn)通常是想辦法推導(dǎo)出它們,然后轉(zhuǎn)為完美場景進(jìn)行處理。
TypeHandlerRegistry在注冊類型轉(zhuǎn)換器的功能實(shí)現(xiàn)方面,提供了較多的register方法重載方法。
其中有一個(gè)方法簽名為private void register(Type javaType, JdbcType jdbcType,TypeHandler<?> handler)的重載方法,用于處理三要素齊全的場景,完成了事實(shí)上的類型轉(zhuǎn)換器的注冊工作:
/**
* 注冊類型轉(zhuǎn)換器
*
* @param javaType java類型
* @param jdbcType jdbc類型
* @param handler 類型轉(zhuǎn)換器
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// 獲取該java類型對應(yīng)的jdbc類型轉(zhuǎn)換器的集合
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
// 注冊java類型和【jdbc和處理器的映射關(guān)系】的映射關(guān)系
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
// 注冊處理器類型和實(shí)例的關(guān)系
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
這是register重載方法中最核心的方法,多數(shù)register重載方法的調(diào)用,最終會落在該方法上來完成最終的處理操作。
他的實(shí)現(xiàn)比較簡單,唯一值得注意的就是NULL_TYPE_HANDLER_MAP作為標(biāo)識符的作用就體現(xiàn)在這里。
還有一些register重載方法的入?yún)笔?code>Java類型與JDBC類型中的一個(gè),甚至是二者全部缺失。
我們前面說過,當(dāng)我們?nèi)鄙倌骋粎?shù)時(shí),會盡可能的去推導(dǎo)出缺少屬性的參數(shù)類型,然后轉(zhuǎn)為完美場景進(jìn)行處理。
推導(dǎo)Java類型參數(shù)
在缺失java類型參數(shù)的場景下,TypeHandlerRegistry給出的解決方案是提供MappedTypes注解和TypeReference抽象父類。
MappedTypes注解可以直接提供TypeHandler對象可轉(zhuǎn)換的java類型集合:
/**
* 該注解用于給類型處理器指定可處理的JAVA類型
*
* @author Eduardo Macarron
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {
/**
* 可轉(zhuǎn)換的JAVA類型集合
*/
Class<?>[] value();
}
TypeReference抽象父類則可以獲取其實(shí)現(xiàn)類的泛型參數(shù)定義。
具體的解析方法可以參考register(TypeHandler<T> typeHandler)方法:
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
// 獲取MappedTypes注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
// 根據(jù)注解,注冊類型轉(zhuǎn)換器
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// 解析泛型
// @since 3.1.0 - try to auto-discover the mapped type
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
// 特殊的null類型
if (!mappedTypeFound) {
register((Class<T>) null, typeHandler);
}
}
優(yōu)先解析MappedTypes注解中指定的類型,其次是根據(jù)TypeReference獲取泛型類型,如果還沒能獲取java類型,那就使用特殊的類型null。
推導(dǎo)Jdbc類型參數(shù)
推導(dǎo)jdbc類型參數(shù),TypeHandlerRegistry給出的解決方案是提供MappedJdbcTypes注解,和MappedTypes相似。
MappedJdbcTypes注解的作用是標(biāo)注出指定TypeHandler可轉(zhuǎn)換的jdbc類型集合:
/**
* 給TypeHandler指定處理的JdbcType
* @author Eduardo Macarron
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedJdbcTypes {
// 可處理的jdbc類型集合
JdbcType[] value();
// 是否額外注冊JDBC類型為null的處理器
boolean includeNullJdbcType() default false;
}
MappedJdbcTypes除了可以指定jdbc類型集合之外,可有一個(gè)includeNullJdbcType()方法,用于指定是否額外注冊Jdbc類型為null的處理器。
負(fù)責(zé)推導(dǎo)jdbc類型參數(shù)的方法是private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler):
/**
* 注冊java類型和類型轉(zhuǎn)換出處理器的關(guān)系
*
* @param javaType java類型
* @param typeHandler 類型轉(zhuǎn)換處理器實(shí)例
* @param <T> java類型
*/
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 尋找mappedJdbcTypes注解,mappedJdbcTypes用于給TypeHandler指定處理的JdbcType
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// 注冊java類型處理和jdbc類型以及類型轉(zhuǎn)換處理器的關(guān)系
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
// 注冊JDBC類型為null的處理器
register(javaType, null, typeHandler);
}
} else {
// 注冊JDBC類型為null的處理器
register(javaType, null, typeHandler);
}
}
上面的兩個(gè)方法的實(shí)現(xiàn)上,分別
上面提到的三個(gè)register()方法,是TypeHandlerRegistry中比較核心的三個(gè)注冊方法,這三個(gè)方法相互配合使用基本可以完成絕大多數(shù)注冊場景。
還有一些其他的register()重載方法,這些重載方法中有一部分用于處理java類型的變種表現(xiàn):
比如,將String類型描述的Java類型轉(zhuǎn)換為實(shí)際的Java類型:
/**
* 注冊指定java類型的指定類型轉(zhuǎn)換器
*
* @param javaTypeClassName java類型名稱
* @param typeHandlerClassName 類型轉(zhuǎn)換處理器名稱
*/
public void register(String javaTypeClassName, String typeHandlerClassName) throws ClassNotFoundException {
register(Resources.classForName(javaTypeClassName), Resources.classForName(typeHandlerClassName));
}
比如,獲取TypeReference對應(yīng)的泛型類型,然后進(jìn)行注冊:
/**
* 根據(jù)泛型注冊類型轉(zhuǎn)換處理器
*
* @param javaTypeReference java泛型
* @param handler 處理器
* @param <T> 泛型
*/
public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
register(javaTypeReference.getRawType(), handler);
}
還有一部分register()方法用于處理特殊的場景,比如:
/**
* 注冊JDBC類型轉(zhuǎn)換器
*
* @param jdbcType jdbc類型
* @param handler 處理器
*/
public void register(JdbcType jdbcType, TypeHandler<?> handler) {
// 注冊jdbc類型轉(zhuǎn)換器
JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}
這個(gè)方法注冊的是指定的Jdbc類型與其對應(yīng)的默認(rèn)轉(zhuǎn)換器,和前面提到那些register()方法在實(shí)際意義上完全不同。
還有一個(gè)register()方法用于批量注冊類型轉(zhuǎn)換器:
// 注冊指定包下所有的java類
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 返回當(dāng)前已經(jīng)找到的所有TypeHandler的子類
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// 忽略匿名類、接口以及抽象類
// 注冊類型轉(zhuǎn)換處理器
register(type);
}
}
}
在這個(gè)方法中,他通過ResolverUtils的find()方法來獲取初步符合預(yù)期的類集合,之后排除掉匿名類、成員類和接口,對剩下的類執(zhí)行別名注冊的操作。
有關(guān)于更多
ResolverUtil的信息,在解析TypeAliases元素時(shí),已經(jīng)給出。
獲取類型轉(zhuǎn)換器功能的實(shí)現(xiàn)
在實(shí)現(xiàn)上TypeHandlerRegistry獲取類型轉(zhuǎn)換器的方法大致可以分為兩類,其中一類是用于判斷指定的類型轉(zhuǎn)換器是否存在的hasTypeHandler()方法,一類是實(shí)際獲取類型轉(zhuǎn)換器對象的getTypeHandler()方法。
這兩類方法的實(shí)現(xiàn)邏輯大致是一致的,hasTypeHandler()方法僅僅是對getTypeHandler()方法的返回結(jié)果做了一層非空判斷,并包裝成Boolean值而已。
getTypeHandler()方法的核心重載實(shí)現(xiàn)是:
/**
* 獲取指定類型的處理器
*
* @param type java類型
* @param jdbcType jdbc類型
* @param <T> 處理器類型
*/
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
/*
* 獲取所有指定類型的java處理器
*/
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
// 獲取對應(yīng)的jdbc處理器
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// 選擇一個(gè)適合的惟一的處理器
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// 返回處理器
// type drives generics here
return (TypeHandler<T>) handler;
}
可以看到,方法實(shí)現(xiàn)比較簡單,先獲取指定java類型對應(yīng)的已注冊的jdbc類型及其轉(zhuǎn)換處理器,之后根據(jù)jdbc類型參數(shù)獲取轉(zhuǎn)換器,
如果沒有指定jdbc類型,則調(diào)用pickSoleHandler(jdbcHandlerMap)方法,嘗試獲取一個(gè)有效轉(zhuǎn)換器:
/**
* 從一組類型轉(zhuǎn)換處理器中選擇唯一的一個(gè)轉(zhuǎn)換器,如果超過一個(gè)類型轉(zhuǎn)換處理器,返回null。
*
* @param jdbcHandlerMap jdbc類型對應(yīng)的類型轉(zhuǎn)換處理器集合
*/
private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
TypeHandler<?> soleHandler = null;
for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
if (soleHandler == null) {
soleHandler = handler;
} else if (!handler.getClass().equals(soleHandler.getClass())) {
// More than one type handlers registered.
return null;
}
}
return soleHandler;
}
pickSoleHandler(jdbcHandlerMap)方法對于有效轉(zhuǎn)換器的定義比較簡單,那就是返回java類型對應(yīng)的唯一的類型轉(zhuǎn)換器,這個(gè)唯一的類型轉(zhuǎn)換器可以當(dāng)做是默認(rèn)的類型轉(zhuǎn)換器來使用。
負(fù)責(zé)獲取指定java類型對應(yīng)的已注冊jdbc類型及其轉(zhuǎn)換處理器的方法getJdbcHandlerMap()在實(shí)現(xiàn)上兼容了枚舉和子類的處理操作。
// 獲取指定java類型對應(yīng)所有類型轉(zhuǎn)換處理器
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
// 獲取當(dāng)前java類型對應(yīng)的類型轉(zhuǎn)換器集合
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
return null;
}
if (jdbcHandlerMap == null && type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (clazz.isEnum()) {
// 根據(jù)枚舉查找類型轉(zhuǎn)換處理器
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
if (jdbcHandlerMap == null) {
// 注冊類型轉(zhuǎn)換處理器
register(clazz, getInstance(clazz, defaultEnumTypeHandler));
// 返回類型轉(zhuǎn)換處理器
return TYPE_HANDLER_MAP.get(clazz);
}
} else {
// 根據(jù)父類查找類型轉(zhuǎn)換處理器
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
// 注冊類型轉(zhuǎn)換處理器
TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
枚舉兼容
在處理枚舉類型時(shí),如果該枚舉沒有定義相關(guān)的類型轉(zhuǎn)換器,將會遞歸調(diào)用getJdbcHandlerMapForEnumInterfaces()方法,嘗試從實(shí)現(xiàn)的接口定義中獲取類型轉(zhuǎn)換器。
如果其接口也沒有對應(yīng)的類型轉(zhuǎn)換器,那么將會使用TypeHandlerRegistry的defaultEnumTypeHandler屬性對應(yīng)的類型轉(zhuǎn)換器作為當(dāng)前枚舉的類型處理器。
/**
* 根據(jù)枚舉接口獲取類型轉(zhuǎn)換器
*
* @param clazz java類型
* @param enumClazz 枚舉類型
* @return 類型轉(zhuǎn)換處理器
*/
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForEnumInterfaces(Class<?> clazz, Class<?> enumClazz) {
for (Class<?> iface : clazz.getInterfaces()) {
// 從指定java類的接口上找類型轉(zhuǎn)換處理器
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(iface);
if (jdbcHandlerMap == null) {
// 沒有對應(yīng)的類型轉(zhuǎn)換器,遞歸往上找
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(iface, enumClazz);
}
if (jdbcHandlerMap != null) {
// 如果找到了類型轉(zhuǎn)換器
// Found a type handler regsiterd to a super interface
HashMap<JdbcType, TypeHandler<?>> newMap = new HashMap<>();
for (Entry<JdbcType, TypeHandler<?>> entry : jdbcHandlerMap.entrySet()) {
// Create a type handler instance with enum type as a constructor arg
// 創(chuàng)建一個(gè)對應(yīng)該類的類型轉(zhuǎn)換器
newMap.put(entry.getKey(), getInstance(enumClazz, entry.getValue().getClass()));
}
return newMap;
}
}
return null;
}
子類兼容
在處理普通類型時(shí),如果該類型沒有定義相關(guān)的類型轉(zhuǎn)換器,將會遞歸調(diào)用getJdbcHandlerMapForSuperclass()方法,嘗試從繼承的父類定義中獲取類型轉(zhuǎn)換器。
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || Object.class.equals(superclass)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(superclass);
if (jdbcHandlerMap != null) {
return jdbcHandlerMap;
} else {
return getJdbcHandlerMapForSuperclass(superclass);
}
}
無論是針對普通類還是枚舉,在查找到新的類型轉(zhuǎn)換器關(guān)系之后,都會重新注冊到
TypeHandlerRegistry中。
我思考了很久,為什么在做子類兼容時(shí),只遞歸查找父類對應(yīng)的類型轉(zhuǎn)換器集合,而不是連父接口對應(yīng)的類型轉(zhuǎn)換器一并查找?
先說結(jié)論:因?yàn)闆]必要。
首先,我們要明確一點(diǎn):接口,是抽象方法的集合,方法定義的是一個(gè)對象的行為,所以接口定義的是一類對象的行為,而不是特性。
被類型轉(zhuǎn)換器處理的java類型通常需要具有一定程度上的一致性和可預(yù)見性,比如,給定Jdbc數(shù)值A,可以獲取java對象a,那么理論上可預(yù)見的是:通過java對象a可以獲得Jdbc數(shù)值A。
這種特性意味著,被類型轉(zhuǎn)換器處理的java類型通常具有簡單性,不會是一個(gè)復(fù)雜的業(yè)務(wù)對象,這時(shí)候強(qiáng)調(diào)的往往是該對象的某一特性,而不是行為,這時(shí)候,這種對象通常會被直接定義為類而不是接口。
那么枚舉對象為什么適配的是接口而不是類呢?
枚舉對象也具有簡單性,但是枚舉默認(rèn)繼承了Enum父類,所以只能實(shí)現(xiàn)接口,沒有辦法直接定義屬性,所以退而求其次,通過方法來間接的限制屬性。
getTypeHandler()方法還有一些其他重載實(shí)現(xiàn),邏輯大致與register()方法重載實(shí)現(xiàn)一致,這里就不做討論了。
到這里,TypeHandlerRegistry就完成了保存所有類型轉(zhuǎn)換器,以及相應(yīng)的注冊和獲取類型轉(zhuǎn)換器的功能的實(shí)現(xiàn)。
其他
TypeHandlerRegistry中還有一些其他方法定義,比如defaultEnumTypeHandler屬性的getter/setter方法和UNKNOWN_TYPE_HANDLER屬性的getter方法,以及通過反射獲取TypeHandler對象實(shí)例的getInstance方法。
這些方法的實(shí)現(xiàn)比較簡單,這里就不在贅述了。
解析typeHandlers元素
現(xiàn)在我們回到typeHandlers元素的解析工作中。
在Mybatis中關(guān)于typeHandlers的DTD是如此定義的:
<!ELEMENT typeHandlers (typeHandler*,package*)>
在typeHandlers下面可以出現(xiàn)零個(gè)或多個(gè)typeHandler或者package標(biāo)簽。
typeHandler元素用于注冊單個(gè)TypeHandler實(shí)例,它有三個(gè)屬性javaType,jdbcType以及handler,其中javaType和jdbcType是可選的,他們分別表示java類型和jdbc類型,handler屬性是必填的,他表示類型轉(zhuǎn)換器的類型,這三個(gè)屬性都可以使用類型別名。
<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>
package元素用于批量注冊TypeHandler實(shí)例,它只有一個(gè)必填的name屬性,他表示用戶需要注冊TypeHandler的基礎(chǔ)包名,Mybatis將會遞歸處理該基礎(chǔ)包及其子包下所有可用的TypeHandler實(shí)例。
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
調(diào)用解析的入口(XmlConfigBuilder):
private void parseConfiguration(XNode root) {
// ...
// 注冊類型轉(zhuǎn)換器
typeHandlerElement(root.evalNode("typeHandlers"));
// ...
}
解析并注冊TypeHandler實(shí)例:
// 解析typeHandlers元素
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 整包注冊
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 單個(gè)注冊
// 獲取java類型的名稱
String javaTypeName = child.getStringAttribute("javaType");
// 獲取jdbc類型的名稱
String jdbcTypeName = child.getStringAttribute("jdbcType");
// 獲取類型轉(zhuǎn)換處理器的名稱
String handlerTypeName = child.getStringAttribute("handler");
// 解析出java類型
Class<?> javaTypeClass = resolveClass(javaTypeName);
// 解析jdbc類型
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
// 解析出類型轉(zhuǎn)換處理器的類型
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
// 注冊類型處理器
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
根據(jù)子元素的不同,TypeHandler實(shí)例的解析注冊也分為兩種:
一類是解析typeHandler元素,嘗試獲取注冊TypeHandler實(shí)例的三要素完成注冊工作。
一類是解析package元素,獲取用于批量注冊的包名,調(diào)用TypeHandlerRegistry的register()方法完成整包批量注冊。
關(guān)于具體注冊方法的實(shí)現(xiàn),前面已經(jīng)有了詳細(xì)的闡述。
至此,整個(gè)typeHandlers元素的解析也已經(jīng)完成,Mybatis的基礎(chǔ)準(zhǔn)備工作也準(zhǔn)備的差不多了。
接下來就是解析Mybatis的Mapper配置文件。