起因
在業(yè)務(wù)開(kāi)發(fā)過(guò)程中类垫,會(huì)經(jīng)常碰到一些不需要檢索,僅僅只是查詢后使用的字段琅坡,例如配置信息悉患,管理后臺(tái)操作日志明細(xì)等,我們會(huì)將這些信息以json的方式存儲(chǔ)在RDBMS
表里
假設(shè)某表foo
的結(jié)構(gòu)如下榆俺,字段bar
就是以json的方式進(jìn)行存儲(chǔ)的
id | bar | create_time |
---|---|---|
1 | {"name":"Shary","quz":10,"timestamp":1574698533370} | 2019-11-26 00:15:50 |
@Data
public class Foo {
private Long id;
private String bar;
private Bar barObj;
private Date createTime;
}
@Data
public class Bar {
private String name;
private Integer quz;
private Date timestamp;
}
在代碼中购撼,比較原始的解決方式是手動(dòng)解決
:查詢時(shí),將json串轉(zhuǎn)成對(duì)象谴仙,放進(jìn)對(duì)象字段里迂求;保存時(shí),手動(dòng)將對(duì)象轉(zhuǎn)成json串晃跺,然后放進(jìn)String
的字段里揩局。如下所示
@Override
public Foo getById(Long id) {
Foo foo = fooMapper.selectByPrimaryKey(id);
String bar = foo.getBar();
Bar barObj = JsonUtil.fromJson(bar, Bar.class);
foo.setBarObj(barObj);
return foo;
}
@Override
public boolean save(Foo foo) {
Bar barObj = foo.getBarObj();
foo.setBar(JsonUtil.toJson(barObj));
return fooMapper.insert(foo) > 0;
}
這種方式,存在兩個(gè)問(wèn)題
- 需要在實(shí)體類(lèi)添加額外的非數(shù)據(jù)庫(kù)字段(
barObj
) - 需要在業(yè)務(wù)邏輯里手動(dòng)轉(zhuǎn)換掀虎,業(yè)務(wù)邏輯糅雜非業(yè)務(wù)代碼凌盯,不夠優(yōu)雅
Mybatis
預(yù)定義的基礎(chǔ)類(lèi)型轉(zhuǎn)換是靠TypeHandler
實(shí)現(xiàn)的,那我們是不是也可以借鑒MyBatis
的轉(zhuǎn)換思路烹玉,來(lái)轉(zhuǎn)換我們自定義的類(lèi)型呢驰怎?
解決方案
- 定義一個(gè)抽象類(lèi),繼承于
org.apache.ibatis.type.BaseTypeHandler
二打,用作對(duì)象
類(lèi)型的換轉(zhuǎn)基類(lèi)县忌;之后但凡想varchar(longvarchar)
與對(duì)象
互轉(zhuǎn),繼承此基類(lèi)即可
public abstract class AbstractObjectTypeHandler<T> extends BaseTypeHandler<T> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtil.toJson(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName)
throws SQLException {
String data = rs.getString(columnName);
return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, (Class<T>) getRawType());
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String data = rs.getString(columnIndex);
return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, (Class<T>) getRawType());
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
String data = cs.getString(columnIndex);
return StringUtils.isBlank(data) ? null : JsonUtil.fromJson(data, (Class<T>) getRawType());
}
}
- 定義具體實(shí)現(xiàn)類(lèi)继效,繼承上述
步驟1
中定義的AbstractObjectTypeHandler
症杏,泛型中填上要轉(zhuǎn)換的Java類(lèi)型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
- 刪除
Foo
中String bar
,并將Bar barObj
改成Bar bar
瑞信,讓Foo
的字段名跟數(shù)據(jù)庫(kù)字段名一一對(duì)應(yīng)
@Data
public class Foo {
private Long id;
private Bar bar;
private Date createTime;
}
- 配置類(lèi)型處理器掃包路徑
- 如果使用
mybatis-spring-boot-starter
厉颤,可以在application.properties
里配置mybatis.typeHandlersPackage={BarTypeHandler所在包路徑}
; - 如果只使用
mybatis-spring
凡简,可以構(gòu)造一個(gè)SqlSessionFactoryBean
對(duì)象逼友,并調(diào)用其setTypeHandlersPackage
方法設(shè)置類(lèi)型處理器掃包路徑 - 使用其它
Mybatis
擴(kuò)展組件的精肃,例如mybatis-plus
,同理配置typeHandlersPackage
屬性即可
經(jīng)過(guò)上述四個(gè)步驟之后帜乞,程序就能正常運(yùn)行司抱,無(wú)論插入數(shù)據(jù),或者從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)挖函,都由Mybatis
調(diào)用我們注冊(cè)的BarTypeHandler
進(jìn)行轉(zhuǎn)換状植,對(duì)于業(yè)務(wù)代碼浊竟,做到了無(wú)感知使用怨喘,也不再存在冗余字段
@Override
public Foo getById(Long id) {
return fooMapper.selectByPrimaryKey(id);
}
@Override
public boolean save(Foo foo) {
return fooMapper.insert(foo) > 0;
}
原理分析
如果只是于使用而言,按照步驟1234走即可振定,而且4只需要走一次必怜。但是,我們顯然不能止步于此后频,知其然梳庆,知其所以然,才能用的安心卑惜,用的放心膏执,用的順手
接下來(lái)會(huì)以mybatis-spring 1.3.2
,mybatis 3.4.6
為例進(jìn)行分析露久。本文比較難理解更米,建議手里就著源碼進(jìn)行閱讀,體驗(yàn)會(huì)更佳
Configuration
使用mybatis-spring
時(shí)毫痕,需要構(gòu)造的一個(gè)核心對(duì)象是SqlSessionFactoryBean
征峦,它是一個(gè)Spring的FactoryBean
,用于產(chǎn)生SqlSessionFactory
對(duì)象消请。同時(shí)還實(shí)現(xiàn)了InitializingBean
接口栏笆,受到Spring Bean的生命周期回調(diào),執(zhí)行afterPropertiesSet
方法臊泰,在回調(diào)中構(gòu)造了sqlSessionFactory
對(duì)象
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
而在buildSqlSessionFactory
方法中蛉加,構(gòu)造了Mybatis
的核心配置類(lèi)Configuration
,并且進(jìn)行了初始化缸逃。當(dāng)Mybatis
不結(jié)合Spring
使用時(shí)七婴,就需要自己構(gòu)造Configuration
對(duì)象,這個(gè)對(duì)應(yīng)于mybatis-config.xml
配置文件察滑,具體使用規(guī)則可以參考官網(wǎng) 打厘。當(dāng)然,mybatis-spring
幫我們搞定了配置Configuration
的事贺辰,同時(shí)也拋棄了mybatis-config.xml
原始的配置文件
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
// ...(省略)
configuration = new Configuration();
// ...(省略)
if (hasLength(this.typeHandlersPackage)) { //配置的類(lèi)型處理器所在包
String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeHandlersPackageArray) {
// 掃包進(jìn)行注冊(cè)
configuration.getTypeHandlerRegistry().register(packageToScan);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
}
}
}
if (!isEmpty(this.typeHandlers)) {
for (TypeHandler<?> typeHandler : this.typeHandlers) {
configuration.getTypeHandlerRegistry().register(typeHandler);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type handler: '" + typeHandler + "'");
}
}
}
// ...(省略)
Configuration
還中持有非常多的對(duì)象户盯,比如MapperRegistry
嵌施、TypeHandlerRegistry
、TypeAliasRegistry
莽鸭、LanguageDriverRegistry
吗伤,其中TypeHandlerRegistry
用于TypeHandler
的注冊(cè)與管理,也是本文的主角
TypeHandlerRegistry
的構(gòu)造函數(shù)中硫眨,默認(rèn)注冊(cè)了幾十個(gè)類(lèi)型轉(zhuǎn)化器足淆,它們的存在,正是Mybatis非常便于使用的原因之一:幫助各種Java類(lèi)型與JdbcType互轉(zhuǎn)礁阁,比如java.util.Date
與JdbcType.TIMESTAMP
互相轉(zhuǎn)化巧号,java.lang.String
與JdbcType.VARCHAR
、JdbcType.LONGVARCHAR
互相轉(zhuǎn)化姥闭,而JdbcType默認(rèn)又與數(shù)據(jù)庫(kù)類(lèi)型有對(duì)應(yīng)關(guān)系丹鸿,為了便于理解,可以簡(jiǎn)單記為Java類(lèi)型與數(shù)據(jù)庫(kù)字段類(lèi)型的轉(zhuǎn)換棚品。其中一部分示例如下
public 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());
// ...(省略)
}
TypeHandlerRegistry
有十余個(gè)名為register
的重載方法靠欢,乍一看容易讓人頭昏眼花,更讓人崩潰的是铜跑,A register
還會(huì)調(diào)B register
门怪,B register
調(diào)C register
,如果不擼清他們之間的關(guān)系锅纺,容易混亂:我是誰(shuí)掷空,我在哪,我在干什么
下面按照1個(gè)伞广、2個(gè)拣帽、3個(gè)參數(shù)的register
分類(lèi)進(jìn)行講解
1個(gè)參數(shù)
- register(String packageName)
- 掃描packageName包下的TypeHandler類(lèi),如果非匿名內(nèi)部類(lèi)嚼锄、非接口减拭、非抽象類(lèi),就調(diào)用
register(typeHandlerClass)
進(jìn)行注冊(cè)
- 掃描packageName包下的TypeHandler類(lèi),如果非匿名內(nèi)部類(lèi)嚼锄、非接口减拭、非抽象類(lèi),就調(diào)用
- register(Class<?> typeHandlerClass)
- 如果
typeHandlerClass
上有MappedTypes
注解区丑,且注解里配置了映射的類(lèi)型拧粪,就調(diào)用register(javaTypeClass, typeHandlerClass)
進(jìn)行注冊(cè) - 否則,調(diào)用
getInstance
生成TypeHandler
實(shí)例沧侥,并調(diào)用register(typeHandler)
進(jìn)行注冊(cè)
- 如果
- register(TypeHandler<T> typeHandler)
- 如果
typeHandler
的Class上有MappedTypes
注解可霎,且注解里配置了映射的類(lèi)型,就調(diào)用register(handledType, typeHandler)
進(jìn)行注冊(cè) - 否則宴杀,
typeHandler
如果是TypeReference
的實(shí)例癣朗,就調(diào)用register(typeReference.getRawType(), typeHandler)
進(jìn)行注冊(cè)。typeReference.getRawType()
獲得的結(jié)果是TypeReference
的泛型 - 否則旺罢,調(diào)用
register((Class<T>) null, typeHandler)進(jìn)行注冊(cè)
- 如果
2個(gè)參數(shù)
- register(String javaTypeClassName, String typeHandlerClassName)
-
Mybatis
并沒(méi)有直接使用到旷余,內(nèi)部是將javaTypeClassName
绢记、typeHandlerClassName
分別轉(zhuǎn)成Class類(lèi)型,并調(diào)用register(javaTypeClass, typeHandlerClass)
進(jìn)行注冊(cè)
-
- register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler)
-
Mybatis
并沒(méi)有直接使用到正卧,內(nèi)部是從javaTypeReference
獲取到rawType
之后蠢熄,調(diào)用register(javaType, typeHandler)
進(jìn)行注冊(cè)
-
- register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
- 調(diào)用
getInstance
生成TypeHandler
實(shí)例后,調(diào)用register(javaTypeClass, typeHandler)
進(jìn)行注冊(cè) - 該方法在
TypeHandlerRegistry
構(gòu)造函數(shù)中被大量調(diào)用炉旷,主要用于支持JSR310
的日期類(lèi)型處理(Since Mybatis 3.4.5)签孔,如this.register(Instant.class, InstantTypeHandler.class)
。不過(guò)需要吐槽的一點(diǎn)是窘行,由于開(kāi)發(fā)者與之前不同饥追,因此注冊(cè)的風(fēng)格與之前不同,調(diào)用的API也不同抽高,增加了學(xué)習(xí)成本
- 調(diào)用
- register(Type javaType, TypeHandler<? extends T> typeHandler)
- 如果
typeHandler
的Class上有MappedJdbcTypes
注解- 注解里配置了JdbcType判耕,
調(diào)用register(javaType, handledJdbcType, typeHandler)
進(jìn)行注冊(cè) - 否則透绩,若
includeNullJdbcType = true
翘骂,調(diào)用register(javaType, null, typeHandler)
進(jìn)行注冊(cè)
- 注解里配置了JdbcType判耕,
- 否則,調(diào)用
register(javaType, null, typeHandler)
進(jìn)行注冊(cè)
- 如果
- register(Class<T> javaType, TypeHandler<? extends T> typeHandler)
- 內(nèi)部調(diào)用
register(javaType, typeHandler)
- 該方法在
TypeHandlerRegistry
構(gòu)造函數(shù)中被大量調(diào)用帚豪,如register(Date.class, new DateTypeHandler())
- 內(nèi)部調(diào)用
- register(JdbcType jdbcType, TypeHandler<?> handler)
- 將
<JdbcType, TypeHandler>
的映射關(guān)系保存到JDBC_TYPE_HANDLER_MAP
- 該方法在
TypeHandlerRegistry
構(gòu)造函數(shù)中被大量調(diào)用碳竟,如register(JdbcType.INTEGER, new IntegerTypeHandler())
- 將
3個(gè)參數(shù)
- register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
- 調(diào)用
getInstance
生成TypeHandler
實(shí)例后,調(diào)用register(javaTypeClass, jdbcType, typeHandler)
進(jìn)行注冊(cè) - 很少用到狸臣,只有在
Mybatis
解析``mybatis-config.xml的
typeHandlers`元素時(shí)莹桅,可能會(huì)調(diào)用該方法進(jìn)行注冊(cè),而前文已說(shuō)過(guò)烛亦,與spring結(jié)合后诈泼,該文件已經(jīng)被拋棄,故不用太關(guān)注
- 調(diào)用
- register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)
- 內(nèi)部將type強(qiáng)轉(zhuǎn)為
Type
類(lèi)型后煤禽,直接調(diào)用register((Type) javaType, jdbcType, handler)
- 內(nèi)部將type強(qiáng)轉(zhuǎn)為
- register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
- 若
javaType
非空铐达,將<JavaType, <JdbcType, TypeHandler>>
的映射關(guān)系保存到TYPE_HANDLER_MAP
中,從中可以看出檬果,對(duì)于一個(gè)javaType
瓮孙,可能存在多個(gè)typeHandler
,用于跟不同的jdbcType
進(jìn)行轉(zhuǎn)換 - 將
<TypeHandlerClass, TypeHandler>
的映射關(guān)系保存到ALL_TYPE_HANDLERS_MAP
中
- 若
以上是從代碼的角度進(jìn)行解讀选脊,確保邏輯無(wú)誤杭抠,但容易讓人云里霧里,不便于理解恳啥,因此有必要在此基礎(chǔ)上總結(jié)一下規(guī)律:
- 單參數(shù)的
register
方法有3個(gè)偏灿,雙參數(shù)的6個(gè),三參數(shù)的3個(gè)钝的,共計(jì)12個(gè)翁垂;將擁有相同入?yún)?shù)量的register
方法歸為同一層忿墅,各層次內(nèi)部有調(diào)用的關(guān)系,上層也會(huì)調(diào)用下層方法沮峡,但不存在跨層調(diào)用疚脐,而最下層,是將注冊(cè)的各個(gè)類(lèi)型保存到Map維護(hù)起來(lái) -
12個(gè)
register
方法邢疙,目的都是為了尋找JavaType棍弄、JdbcType、TypeHandler
及他們之間的關(guān)系疟游,最終維護(hù)在3個(gè)Map中:JDBC_TYPE_HANDLER_MAP
呼畸、TYPE_HANDLER_MAP
、ALL_TYPE_HANDLERS_MAP
-
javaType颁虐、javaTypeClass
描述的是待轉(zhuǎn)換java的類(lèi)型蛮原,在例子中就是Bar.class
;JdbcType
是一個(gè)枚舉類(lèi)型,代表Jdbc類(lèi)型另绩,典型的取值有JdbcType.VARCHAR儒陨、JdbcType.BIGINT
;typeHandler笋籽、BarTypeHandler
分別代表類(lèi)型轉(zhuǎn)換器實(shí)例及其Class實(shí)例蹦漠,在例子中就是BarTypeHandler、BarTypeHandler.class
-
MappedTypes
车海、MappedJdbcTypes
是兩個(gè)注解笛园,作用于TypeHandler
上,用于指示侍芝、限定其所能支持的JavaType
以及JdbcType
出于篇幅原因以及理解復(fù)雜度的考慮研铆,本篇不涉及注解方案,會(huì)在后續(xù)篇章繼續(xù)介紹注解的使用姿勢(shì)及原理州叠,消化了本篇所介紹的內(nèi)容棵红,屆時(shí)會(huì)更容易理解注解的使用。
接著留量,回到buildSqlSessionFactory
掃包處接著往下看窄赋,找到符合條件的類(lèi)型處理器并調(diào)用register(type)
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
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())) {
register(type);
}
}
}
邏輯會(huì)走到下邊部分,根據(jù)(null, typeHandlerClass)
獲取TypeHandler
實(shí)例楼熄,方法第一個(gè)入?yún)?code>javaTypeClass忆绰,而此處并不知道javaTypeClass
是什么,因此傳入的值null
可岂,而獲取實(shí)例的方法也很簡(jiǎn)單错敢,根據(jù)javaTypeClass
是否為空來(lái)判斷使用哪個(gè)typeHandlerClass
的構(gòu)造函數(shù)來(lái)構(gòu)造例實(shí)。獲取實(shí)例之后調(diào)用register(typeHandler)
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
// 本篇不涉及注解使用方式,因此 mappedTypeFound = false
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> javaTypeClass : mappedTypes.value()) {
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
// 走這段邏輯
register(getInstance(null, typeHandlerClass));
}
}
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
// 省略try catch
if (javaTypeClass != null) {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
}
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
}
同樣忽略注解部分稚茅。從2012年發(fā)布Mybatis 3.1.0
開(kāi)始纸淮,支持自動(dòng)發(fā)現(xiàn)mapped type
的特性,這兒的mapped type
指的是前文中提到的JavaType
亚享。Mybatis 3.1.0
新增了一個(gè)抽象類(lèi)TypeReference
咽块,它是BaseTypeHandler
的抽象基類(lèi),該類(lèi)只有一個(gè)能力欺税,就是使用"標(biāo)準(zhǔn)姿勢(shì)"提取泛型具體類(lèi)侈沪,即提取JavaType
,比如public class BarTypeHandler extends AbstractObjectTypeHandler<Bar>
晚凿,提取的就是Bar.class
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
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
}
}
if (!mappedTypeFound) {
register((Class<T>) null, typeHandler);
}
}
public abstract class TypeReference<T> {
private final Type rawType;
protected TypeReference() {
rawType = getSuperclassTypeParameter(getClass());
}
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];
// TODO remove this when Reflector is fixed to return Types
if (rawType instanceof ParameterizedType) {
rawType = ((ParameterizedType) rawType).getRawType();
}
return rawType;
}
// ...(省略)
}
調(diào)用register(javaType, null, typeHandler)
亭罪,該方法第二個(gè)參數(shù)是JdbcType
,而我們沒(méi)有配置MappedJdbcTypes
注解歼秽,因此為null
应役,代表的是對(duì)JdbcType
不做限制
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
register(javaType, null, typeHandler);
}
}
終于來(lái)到最后維護(hù)Map的方法,根據(jù)源碼燥筷,很容易看出主要是維護(hù)ALL_TYPE_HANDLERS_MAP<typeHandlerClass, typeHandler>
箩祥、TYPE_HANDLER_MAP<javaType, jdbcType,typeHandler>
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<JdbcType, TypeHandler<?>>();
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
上面分析typeHandler
是如何注冊(cè)的荆责,接下來(lái)分析它是如何與mapper.xml
關(guān)聯(lián)起來(lái)的
注: 由于接下來(lái)基本與mapper.xml
相關(guān)滥比,如無(wú)特殊說(shuō)明亚脆,將用xml
來(lái)指代mapper.xml
做院,而不是mybatis-config.xml
繼續(xù)回到buildSqlSessionFactory
方法,往下看濒持,mapperLocations
的類(lèi)型是Resource[]
键耕,代表xml
資源集合,遍歷每一個(gè)文件柑营,并進(jìn)行解析
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
// ...(省略)
if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
// ...(省略)
}
}
// ...(省略)
使用XPath
讀取mapper
元素的值屈雄,并將結(jié)果傳入configurationElement
進(jìn)行更深層次的解析。任意打開(kāi)一個(gè)xml
文件官套,在DOCTYPE
聲明后緊跟著的第一行即是mapper
元素酒奶,它可能長(zhǎng)<mapper namespace="com.example.demo.mapper.FooMapper" >
這樣,該元素很常見(jiàn)奶赔,只是容易讓人忽視
// org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解配`xml`文件中 mapper元素
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
// ...(省略)
}
configurationElement
方法惋嚎,主要是解析xml
本身的所有元素,如namespace
站刑、cache-ref
另伍、cache
、resultMap
绞旅、sql
摆尝、select|insert|update|delete
等温艇,這些元素我們已經(jīng)很熟悉,而parameterMap
已經(jīng)被Mybatis
打入冷宮堕汞,連官網(wǎng)都不愿著筆墨介紹勺爱,不需要關(guān)注。
parameterMap– Deprecated! Old-school way to map parameters. Inline parameters are preferred and this element may be removed in the future. Not documented here.
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析resultMap元素
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 解析CRUD 元素
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
ParameterMapping讯检、ResultMapping
ParameterMapping: 請(qǐng)求參數(shù)的映射關(guān)系邻寿,是對(duì)xml
中每個(gè)statement中#{}
的封裝,如<insert>
中的#{bar,jdbcType=VARCHAR}
public class ParameterMapping {
private Configuration configuration;
private String property;
private ParameterMode mode;
private Class<?> javaType = Object.class;
private JdbcType jdbcType;
private Integer numericScale;
private TypeHandler<?> typeHandler;
private String resultMapId;
private String jdbcTypeName;
private String expression;
// ...(省略)
}
ResultMapping: 結(jié)果集的映射關(guān)系视哑,是對(duì)xml
中<resultMap>
中子元素的封裝俄占,如<result column="bar" property="bar" jdbcType="VARCHAR" />
public class ResultMapping {
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private JdbcType jdbcType;
private TypeHandler<?> typeHandler;
private String nestedResultMapId;
private String nestedQueryId;
private Set<String> notNullColumns;
private String columnPrefix;
private List<ResultFlag> flags;
private List<ResultMapping> composites;
private String resultSet;
private String foreignColumn;
private boolean lazy;
// ...(省略)
}
二者有3個(gè)同名參數(shù)需要我們重點(diǎn)關(guān)注:javaType
录别、jdbcType
、typeHandler
。我們可以手動(dòng)指定ParameterMapping
或ResultMapping
的typeHandler
蝌数,若未明確指定,Mybatis
會(huì)在應(yīng)用啟動(dòng)解析xml
文件過(guò)程中嚷量,為其智能匹配上合適的值镇饮,若匹配不到,會(huì)拋出異常No typehandler found for property ...
耗绿。這也暗示著一個(gè)事實(shí):MyBatis
依托于無(wú)論內(nèi)置的還是自定義的typeHandler
做JavaType
與JdbcType
之間的轉(zhuǎn)換苹支,是框架得以正常運(yùn)轉(zhuǎn)的前提,是賴以生存的基礎(chǔ)能力
構(gòu)造ParameterMapping
與ResultMapping
的代碼有高度一致性误阻,甚至就typeHandler
相關(guān)而言债蜜,基本完全一樣,因此本文僅用ParameterMapping
介紹
回到configurationElement
方法究反,方法內(nèi)部調(diào)用buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
讀取xml
文件所有statement元素寻定,遍歷該元素集合并調(diào)用statementParser.parseStatementNode()
解析集合里的每一個(gè)元素
// org.apache.ibatis.builder.xml.XMLMapperBuilder
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
// 省略try catch
statementParser.parseStatementNode();
}
}
parseStatementNode
方法內(nèi)部代碼雖比較多,但是本身并不難理解精耐,主要是提取并解析statement各類(lèi)屬性值狼速,比如resultType
、parameterType
卦停、timeout
向胡、flushCache
等,為了突出重點(diǎn)惊完,把其余的省略僵芹。
SqlSouce: 代表從XML
或者注解中解析出來(lái)的SQL語(yǔ)句的封裝
Represents the content of a mapped statement read from an XML file or an annotation. It creates the SQL that will be passed to the database out of the input parameter received from the user.
public void parseStatementNode() {
// ...(省略)
String parameterType = context.getStringAttribute("parameterType");
// ...(省略)
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
}
接下來(lái)以insert
方法為例,方法簽名是int insert(Foo record);
专执,對(duì)應(yīng)的insert
statement是
<insert id="insert" parameterType="com.example.demo.model.Foo" >
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER" >
SELECT LAST_INSERT_ID()
</selectKey>
insert into foo (bar, create_time)
values (#{bar,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP})
</insert>
接著調(diào)用到langDriver.createSqlSource
// org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 走這兒淮捆,parameterType代表入?yún)⒌念?lèi)型,在我們case中代表Foo.class
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
// sql 代表從statement中提取的原始未經(jīng)加工的SQL,帶有#{bar,jdbcType=VARCHAR}等信息
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// ParameterMapping處理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 解析器攀痊,解析 #{}
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 重點(diǎn)
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
來(lái)到org.apache.ibatis.parsing.GenericTokenParser#parse
桐腌,該方法根據(jù)傳入的原始sql,解析里邊#{}
所代表的內(nèi)容苟径,在我們的case中案站,結(jié)果是bar,jdbcType=VARCHAR
,將結(jié)果保存在expression
變量中棘街,調(diào)用ParameterMappingTokenHandler#handleToken
進(jìn)行處理蟆盐。每一個(gè)#{}
代表了原始SQL中的?
,因此handleToken
方法的返回值就是?
遭殉,使用過(guò)JDBC編程的同學(xué)應(yīng)該也明白?
代表的含義---->從此處我們也證實(shí)了石挂,#{}的方式屏蔽了SQL注入的風(fēng)險(xiǎn),與原生JDBC編程中使用?
的預(yù)防SQL注入的方式是一樣的
// org.apache.ibatis.parsing.GenericTokenParser#parse
public String parse(String text) {
// ...(省略)
builder.append(handler.handleToken(expression.toString()));
// ...(省略)
}
// org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
buildParameterMapping
方法根據(jù)傳入的expression险污,解析出javaType
痹愚、jdbcType
、typeHandler
等屬性蛔糯,構(gòu)建并填充ParameterMapping
對(duì)象
private ParameterMapping buildParameterMapping(String content) {
// ...(省略)
// propertyType = Bar.class
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if // ...(省略)
}
return builder.build();
}
build
方法做了兩件事拯腮,一是再次解析typeHandler
,二是校驗(yàn)typeHandler
是否為空蚁飒,如果為空动壤,則拋出異常。為什么需要再次解析淮逻?是因?yàn)橛锌赡茉?{}中未明確指定使用哪個(gè)typeHandler
琼懊,即parameterMapping.typeHandler == null
,這時(shí)候Mybatis
會(huì)智能去匹配弦蹂,當(dāng)然肩碟,有時(shí)候也不是那么智能,匹配的結(jié)果跟我們預(yù)期的不太一樣凸椿,這時(shí)候手動(dòng)指定會(huì)更合適
// org.apache.ibatis.mapping.ParameterMapping.Builder#build
public ParameterMapping build() {
resolveTypeHandler();
validate();
return parameterMapping;
}
private void resolveTypeHandler() {
// 再次解析typeHandler
if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
Configuration configuration = parameterMapping.configuration;
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 根據(jù)javaType、jdbcType去typeHandlerRegistry中找typeHandler
parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
}
}
private void validate() {
// javaType為ResultSet類(lèi)型翅溺,這種使用姿勢(shì)較少脑漫,可以跳過(guò)
if (ResultSet.class.equals(parameterMapping.javaType)) {
if (parameterMapping.resultMapId == null) {
throw new IllegalStateException("Missing resultmap in property '"
+ parameterMapping.property + "'. "
+ "Parameters of type java.sql.ResultSet require a resultmap.");
}
} else {
// 再次解析后還空,拋出異常
if (parameterMapping.typeHandler == null) {
throw new IllegalStateException("Type handler was null on parameter mapping for property '"
+ parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType ("
+ parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination.");
}
}
}
在我們的case中咙崎,并未明確指定typeHandler
优幸,因此resolveTypeHandler
中,滿足parameterMapping.typeHandler == null
的條件褪猛,調(diào)用typeHandlerRegistry.getTypeHandler
方法進(jìn)行智能匹配
先根據(jù)javaType
調(diào)用getJdbcHandlerMap
方法拿到jdbcHandlerMap
网杆,而
getJdbcHandlerMap
其實(shí)只是根據(jù)javaType
從TYPE_HANDLER_MAP
取,從前文中我們知道,TYPE_HANDLER_MAP
中存在這么一條entry <Bar.class, <null, BarTypeHandler>>
碳却,因此jdbcHandlerMap
為 <null, BarTypeHandler>
队秩。
再根據(jù)jdbcType
到jdbcHandlerMap
中找typeHandler
。此處經(jīng)過(guò)兩次查找:第一次以jdbcType(VARCHAR)
為key昼浦,第二次以null
為key馍资。由于我們注冊(cè)的BarTypeHandler
并沒(méi)有明確指定jdbcType
,前文也提及到关噪,不明確指定鸟蟹,就意味著不限制,就會(huì)將<null, BarTypeHandler>
注冊(cè)到jdbcHandlerMap
使兔,第一次通過(guò)通過(guò)jdbcHandlerMap.get(VARCHAR)
拿不到建钥,第二次通過(guò)jdbcHandlerMap.get(null)
就拿到了不受jdbcType
限制的BarTypeHandler
// org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler
public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
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()) {
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
if (jdbcHandlerMap == null) {
register(clazz, getInstance(clazz, defaultEnumTypeHandler));
return TYPE_HANDLER_MAP.get(clazz);
}
} else {
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
經(jīng)過(guò)上述分析,我們對(duì)于一個(gè)<insert>
statement虐沥,拿到了對(duì)應(yīng)的SqlSource锦针,里面包含著解析后的SQL(如:insert into foo (bar, create_time) values (?, ?)
)以及ParameterMapping
集合等信息,之所以是集合置蜀,是因?yàn)橐粋€(gè)statement里可能包含多個(gè)#{}奈搜,而每一個(gè)#{}都對(duì)應(yīng)著一個(gè)ParameterMapping
接下來(lái),我們看執(zhí)行insert
方法的時(shí)候盯荤,發(fā)生了什么事情
// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 拿出啟動(dòng)過(guò)程過(guò)程構(gòu)建的ParameterMapping
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// ...(省略)
value = metaObject.getValue(propertyName);
}
// 從parameterMapping中取出typeHandler與jdbcType
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
// 忽略try catch
// 調(diào)用typeHandler的setParameter方法馋吗,完成JavaType到數(shù)據(jù)庫(kù)字段的轉(zhuǎn)化
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
// org.apache.ibatis.type.BaseTypeHandler#setParameter
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
// ...(省略)
setNonNullParameter(ps, i, parameter, jdbcType);
}
最終,代碼走到我們自定義的BarTypeHandler
秋秤,在這宏粤,我們將parameter
對(duì)象 json化,并調(diào)用ps.setString
方法灼卢,最終轉(zhuǎn)換成VARCHAR
保存起來(lái)
public abstract class AbstractObjectTypeHandler<T> extends BaseTypeHandler<T> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtil.toJson(parameter));
}
// ...(省略)
}
總結(jié)
- 本文一開(kāi)始提出在表中存儲(chǔ)json串的需求绍哎,并展示了
手動(dòng)
將對(duì)象與json互轉(zhuǎn)的原始方式,隨后給出了Mybatis
優(yōu)雅存取json字段的解決方案 -TypeHandler
- 接著鞋真,從
TypeHandler
的注冊(cè)過(guò)程開(kāi)始介紹崇堰,分析了12個(gè)register
方法之間錯(cuò)綜復(fù)雜的關(guān)系,最終得出注冊(cè)過(guò)程就是構(gòu)建三個(gè)Map的過(guò)程涩咖,核心是TYPE_HANDLER_MAP
海诲,它維護(hù)著<JavaType, <JdbcType, TypeHandler>>
的映射關(guān)系,在構(gòu)造ParameterMapping
檩互、ResultMapping
時(shí)使用到 - 然后特幔,詳細(xì)闡述了在應(yīng)用啟動(dòng)過(guò)程中,
Mybatis
如何根據(jù)Mapper.xml
和TYPE_HANDLER_MAP
構(gòu)造ParameterMapping
- 最后闸昨,簡(jiǎn)述了當(dāng)一個(gè)
<insert>
方法被調(diào)用時(shí)蚯斯,typeHandler
如何工作
本文力求圍繞核心主題薄风,緊著一條主脈落進(jìn)行講解,為避免被過(guò)多的分支干擾拍嵌,省略了不少旁枝末節(jié)遭赂,其中還包含一些比較重要的特性,因此下一篇撰茎,將分析typeHandler
結(jié)合MappedTypes
嵌牺、MappedJdbcTypes
注解的使用方式