起因
在業務開發過程中,會經常碰到一些不需要檢索,僅僅只是查詢後使用的字段,例如配置信息,管理後臺操作日誌明細等,我們會將這些信息以json的方式存儲在RDBMS
表裏
假設某表foo
的結構如下,字段bar
就是以json的方式進行存儲的
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;
}
在代碼中,比較原始的解決方式是手動解決
:查詢時,將json串轉成對象,放進對象字段裏;保存時,手動將對象轉成json串,然後放進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;
}
這種方式,存在兩個問題
- 需要在實體類添加額外的非數據庫字段(
barObj
) - 需要在業務邏輯裏手動轉換,業務邏輯糅雜非業務代碼,不夠優雅
Mybatis
預定義的基礎類型轉換是靠TypeHandler
實現的,那我們是不是也可以借鑑MyBatis
的轉換思路,來轉換我們自定義的類型呢?
解決方案
- 定義一個抽象類,繼承於
org.apache.ibatis.type.BaseTypeHandler
,用作對象
類型的換轉基類;之後但凡想varchar(longvarchar)
與對象
互轉,繼承此基類即可
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());
}
}
- 定義具體實現類,繼承上述
步驟1
中定義的AbstractObjectTypeHandler
,泛型中填上要轉換的Java類型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
- 刪除
Foo
中String bar
,並將Bar barObj
改成Bar bar
,讓Foo
的字段名跟數據庫字段名一一對應
@Data
public class Foo {
private Long id;
private Bar bar;
private Date createTime;
}
- 配置類型處理器掃包路徑
- 如果使用
mybatis-spring-boot-starter
,可以在application.properties
裏配置mybatis.typeHandlersPackage={BarTypeHandler所在包路徑}
; - 如果只使用
mybatis-spring
,可以構造一個SqlSessionFactoryBean
對象,並調用其setTypeHandlersPackage
方法設置類型處理器掃包路徑 - 使用其它
Mybatis
擴展組件的,例如mybatis-plus
,同理配置typeHandlersPackage
屬性即可
經過上述四個步驟之後,程序就能正常運行,無論插入數據,或者從數據庫獲取數據,都由Mybatis
調用我們註冊的BarTypeHandler
進行轉換,對於業務代碼,做到了無感知使用,也不再存在冗餘字段
@Override
public Foo getById(Long id) {
return fooMapper.selectByPrimaryKey(id);
}
@Override
public boolean save(Foo foo) {
return fooMapper.insert(foo) > 0;
}
原理分析
如果只是於使用而言,按照步驟1234走即可,而且4只需要走一次。但是,我們顯然不能止步於此,知其然,知其所以然,才能用的安心,用的放心,用的順手
接下來會以mybatis-spring 1.3.2
,mybatis 3.4.6
爲例進行分析。本文比較難理解,建議手裏就着源碼進行閱讀,體驗會更佳
Configuration
使用mybatis-spring
時,需要構造的一個核心對象是SqlSessionFactoryBean
,它是一個Spring的FactoryBean
,用於產生SqlSessionFactory
對象。同時還實現了InitializingBean
接口,受到Spring Bean的生命週期回調,執行afterPropertiesSet
方法,在回調中構造了sqlSessionFactory
對象
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
方法中,構造了Mybatis
的核心配置類Configuration
,並且進行了初始化。當Mybatis
不結合Spring
使用時,就需要自己構造Configuration
對象,這個對應於mybatis-config.xml
配置文件,具體使用規則可以參考官網 。當然,mybatis-spring
幫我們搞定了配置Configuration
的事,同時也拋棄了mybatis-config.xml
原始的配置文件
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
// ...(省略)
configuration = new Configuration();
// ...(省略)
if (hasLength(this.typeHandlersPackage)) { //配置的類型處理器所在包
String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeHandlersPackageArray) {
// 掃包進行註冊
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
還中持有非常多的對象,比如MapperRegistry
、TypeHandlerRegistry
、TypeAliasRegistry
、LanguageDriverRegistry
,其中TypeHandlerRegistry
用於TypeHandler
的註冊與管理,也是本文的主角
TypeHandlerRegistry
的構造函數中,默認註冊了幾十個類型轉化器,它們的存在,正是Mybatis非常便於使用的原因之一:幫助各種Java類型與JdbcType互轉,比如java.util.Date
與JdbcType.TIMESTAMP
互相轉化,java.lang.String
與JdbcType.VARCHAR
、JdbcType.LONGVARCHAR
互相轉化,而JdbcType默認又與數據庫類型有對應關係,爲了便於理解,可以簡單記爲Java類型與數據庫字段類型的轉換。其中一部分示例如下
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
有十餘個名爲register
的重載方法,乍一看容易讓人頭昏眼花,更讓人崩潰的是,A register
還會調B register
,B register
調C register
,如果不擼清他們之間的關係,容易混亂:我是誰,我在哪,我在幹什麼
下面按照1個、2個、3個參數的register
分類進行講解
1個參數
- register(String packageName)
- 掃描packageName包下的TypeHandler類,如果非匿名內部類、非接口、非抽象類,就調用
register(typeHandlerClass)
進行註冊
- 掃描packageName包下的TypeHandler類,如果非匿名內部類、非接口、非抽象類,就調用
- register(Class<?> typeHandlerClass)
- 如果
typeHandlerClass
上有MappedTypes
註解,且註解裏配置了映射的類型,就調用register(javaTypeClass, typeHandlerClass)
進行註冊 - 否則,調用
getInstance
生成TypeHandler
實例,並調用register(typeHandler)
進行註冊
- 如果
- register(TypeHandler typeHandler)
- 如果
typeHandler
的Class上有MappedTypes
註解,且註解裏配置了映射的類型,就調用register(handledType, typeHandler)
進行註冊 - 否則,
typeHandler
如果是TypeReference
的實例,就調用register(typeReference.getRawType(), typeHandler)
進行註冊。typeReference.getRawType()
獲得的結果是TypeReference
的泛型 - 否則,調用
register((Class<T>) null, typeHandler)進行註冊
- 如果
2個參數
- register(String javaTypeClassName, String typeHandlerClassName)
Mybatis
並沒有直接使用到,內部是將javaTypeClassName
、typeHandlerClassName
分別轉成Class類型,並調用register(javaTypeClass, typeHandlerClass)
進行註冊
- register(TypeReference javaTypeReference, TypeHandler<? extends T> handler)
Mybatis
並沒有直接使用到,內部是從javaTypeReference
獲取到rawType
之後,調用register(javaType, typeHandler)
進行註冊
- register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
- 調用
getInstance
生成TypeHandler
實例後,調用register(javaTypeClass, typeHandler)
進行註冊 - 該方法在
TypeHandlerRegistry
構造函數中被大量調用,主要用於支持JSR310
的日期類型處理(Since Mybatis 3.4.5),如this.register(Instant.class, InstantTypeHandler.class)
。不過需要吐槽的一點是,由於開發者與之前不同,因此註冊的風格與之前不同,調用的API也不同,增加了學習成本
- 調用
- register(Type javaType, TypeHandler<? extends T> typeHandler)
- 如果
typeHandler
的Class上有MappedJdbcTypes
註解- 註解裏配置了JdbcType,
調用register(javaType, handledJdbcType, typeHandler)
進行註冊 - 否則,若
includeNullJdbcType = true
,調用register(javaType, null, typeHandler)
進行註冊
- 註解裏配置了JdbcType,
- 否則,調用
register(javaType, null, typeHandler)
進行註冊
- 如果
- register(Class javaType, TypeHandler<? extends T> typeHandler)
- 內部調用
register(javaType, typeHandler)
- 該方法在
TypeHandlerRegistry
構造函數中被大量調用,如register(Date.class, new DateTypeHandler())
- 內部調用
- register(JdbcType jdbcType, TypeHandler<?> handler)
- 將
<JdbcType, TypeHandler>
的映射關係保存到JDBC_TYPE_HANDLER_MAP
- 該方法在
TypeHandlerRegistry
構造函數中被大量調用,如register(JdbcType.INTEGER, new IntegerTypeHandler())
- 將
3個參數
- register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
- 調用
getInstance
生成TypeHandler
實例後,調用register(javaTypeClass, jdbcType, typeHandler)
進行註冊 - 很少用到,只有在
Mybatis
解析``mybatis-config.xml的
typeHandlers`元素時,可能會調用該方法進行註冊,而前文已說過,與spring結合後,該文件已經被拋棄,故不用太關注
- 調用
- register(Class type, JdbcType jdbcType, TypeHandler<? extends T> handler)
- 內部將type強轉爲
Type
類型後,直接調用register((Type) javaType, jdbcType, handler)
- 內部將type強轉爲
- register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
- 若
javaType
非空,將<JavaType, <JdbcType, TypeHandler>>
的映射關係保存到TYPE_HANDLER_MAP
中,從中可以看出,對於一個javaType
,可能存在多個typeHandler
,用於跟不同的jdbcType
進行轉換 - 將
<TypeHandlerClass, TypeHandler>
的映射關係保存到ALL_TYPE_HANDLERS_MAP
中
- 若
以上是從代碼的角度進行解讀,確保邏輯無誤,但容易讓人云裏霧裏,不便於理解,因此有必要在此基礎上總結一下規律:
- 單參數的
register
方法有3個,雙參數的6個,三參數的3個,共計12個;將擁有相同入參數量的register
方法歸爲同一層,各層次內部有調用的關係,上層也會調用下層方法,但不存在跨層調用,而最下層,是將註冊的各個類型保存到Map維護起來 - 12個
register
方法,目的都是爲了尋找JavaType、JdbcType、TypeHandler
及他們之間的關係,最終維護在3個Map中:JDBC_TYPE_HANDLER_MAP
、TYPE_HANDLER_MAP
、ALL_TYPE_HANDLERS_MAP
javaType、javaTypeClass
描述的是待轉換java的類型,在例子中就是Bar.class
;JdbcType
是一個枚舉類型,代表Jdbc類型,典型的取值有JdbcType.VARCHAR、JdbcType.BIGINT
;typeHandler、BarTypeHandler
分別代表類型轉換器實例及其Class實例,在例子中就是BarTypeHandler、BarTypeHandler.class
MappedTypes
、MappedJdbcTypes
是兩個註解,作用於TypeHandler
上,用於指示、限定其所能支持的JavaType
以及JdbcType
出於篇幅原因以及理解複雜度的考慮,本篇不涉及註解方案,會在後續篇章繼續介紹註解的使用姿勢及原理,消化了本篇所介紹的內容,屆時會更容易理解註解的使用。
接着,回到buildSqlSessionFactory
掃包處接着往下看,找到符合條件的類型處理器並調用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);
}
}
}
邏輯會走到下邊部分,根據(null, typeHandlerClass)
獲取TypeHandler
實例,方法第一個入參爲javaTypeClass
,而此處並不知道javaTypeClass
是什麼,因此傳入的值null
,而獲取實例的方法也很簡單,根據javaTypeClass
是否爲空來判斷使用哪個typeHandlerClass
的構造函數來構造例實。獲取實例之後調用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年發佈Mybatis 3.1.0
開始,支持自動發現mapped type
的特性,這兒的mapped type
指的是前文中提到的JavaType
。Mybatis 3.1.0
新增了一個抽象類TypeReference
,它是BaseTypeHandler
的抽象基類,該類只有一個能力,就是使用"標準姿勢"提取泛型具體類,即提取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;
}
// ...(省略)
}
調用register(javaType, null, typeHandler)
,該方法第二個參數是JdbcType
,而我們沒有配置MappedJdbcTypes
註解,因此爲null
,代表的是對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);
}
}
終於來到最後維護Map的方法,根據源碼,很容易看出主要是維護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
是如何註冊的,接下來分析它是如何與mapper.xml
關聯起來的
注: 由於接下來基本與mapper.xml
相關,如無特殊說明,將用xml
來指代mapper.xml
,而不是mybatis-config.xml
繼續回到buildSqlSessionFactory
方法,往下看,mapperLocations
的類型是Resource[]
,代表xml
資源集合,遍歷每一個文件,並進行解析
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
元素的值,並將結果傳入configurationElement
進行更深層次的解析。任意打開一個xml
文件,在DOCTYPE
聲明後緊跟着的第一行即是mapper
元素,它可能長<mapper namespace="com.example.demo.mapper.FooMapper" >
這樣,該元素很常見,只是容易讓人忽視
// 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
等,這些元素我們已經很熟悉,而parameterMap
已經被Mybatis
打入冷宮,連官網都不願着筆墨介紹,不需要關注。
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: 請求參數的映射關係,是對xml
中每個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: 結果集的映射關係,是對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個同名參數需要我們重點關注:javaType
、jdbcType
、typeHandler
。我們可以手動指定ParameterMapping
或ResultMapping
的typeHandler
,若未明確指定,Mybatis
會在應用啓動解析xml
文件過程中,爲其智能匹配上合適的值,若匹配不到,會拋出異常No typehandler found for property ...
。這也暗示着一個事實:MyBatis
依託於無論內置的還是自定義的typeHandler
做JavaType
與JdbcType
之間的轉換,是框架得以正常運轉的前提,是賴以生存的基礎能力
構造ParameterMapping
與ResultMapping
的代碼有高度一致性,甚至就typeHandler
相關而言,基本完全一樣,因此本文僅用ParameterMapping
介紹
回到configurationElement
方法,方法內部調用buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
讀取xml
文件所有statement元素,遍歷該元素集合並調用statementParser.parseStatementNode()
解析集合裏的每一個元素
// 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
方法內部代碼雖比較多,但是本身並不難理解,主要是提取並解析statement各類屬性值,比如resultType
、parameterType
、timeout
、flushCache
等,爲了突出重點,把其餘的省略。
SqlSouce: 代表從XML
或者註解中解析出來的SQL語句的封裝
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);
}
接下來以insert
方法爲例,方法簽名是int insert(Foo record);
,對應的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>
接着調用到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代表入參的類型,在我們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中提取的原始未經加工的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);
// 重點
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
來到org.apache.ibatis.parsing.GenericTokenParser#parse
,該方法根據傳入的原始sql,解析裏邊#{}
所代表的內容,在我們的case中,結果是bar,jdbcType=VARCHAR
,將結果保存在expression
變量中,調用ParameterMappingTokenHandler#handleToken
進行處理。每一個#{}
代表了原始SQL中的?
,因此handleToken
方法的返回值就是?
,使用過JDBC編程的同學應該也明白?
代表的含義---->從此處我們也證實了,#{}的方式屏蔽了SQL注入的風險,與原生JDBC編程中使用?
的預防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
方法根據傳入的expression,解析出javaType
、jdbcType
、typeHandler
等屬性,構建並填充ParameterMapping
對象
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
,二是校驗typeHandler
是否爲空,如果爲空,則拋出異常。爲什麼需要再次解析?是因爲有可能在#{}中未明確指定使用哪個typeHandler
,即parameterMapping.typeHandler == null
,這時候Mybatis
會智能去匹配,當然,有時候也不是那麼智能,匹配的結果跟我們預期的不太一樣,這時候手動指定會更合適
// 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();
// 根據javaType、jdbcType去typeHandlerRegistry中找typeHandler
parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
}
}
private void validate() {
// javaType爲ResultSet類型,這種使用姿勢較少,可以跳過
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
的條件,調用typeHandlerRegistry.getTypeHandler
方法進行智能匹配
先根據javaType
調用getJdbcHandlerMap
方法拿到jdbcHandlerMap
,而
getJdbcHandlerMap
其實只是根據javaType
從TYPE_HANDLER_MAP
取,從前文中我們知道,TYPE_HANDLER_MAP
中存在這麼一條entry <Bar.class, <null, BarTypeHandler>>
,因此jdbcHandlerMap
爲 <null, BarTypeHandler>
。
再根據jdbcType
到jdbcHandlerMap
中找typeHandler
。此處經過兩次查找:第一次以jdbcType(VARCHAR)
爲key,第二次以null
爲key。由於我們註冊的BarTypeHandler
並沒有明確指定jdbcType
,前文也提及到,不明確指定,就意味着不限制,就會將<null, BarTypeHandler>
註冊到jdbcHandlerMap
,第一次通過通過jdbcHandlerMap.get(VARCHAR)
拿不到,第二次通過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;
}
經過上述分析,我們對於一個<insert>
statement,拿到了對應的SqlSource,裏面包含着解析後的SQL(如:insert into foo (bar, create_time) values (?, ?)
)以及ParameterMapping
集合等信息,之所以是集合,是因爲一個statement裏可能包含多個#{},而每一個#{}都對應着一個ParameterMapping
接下來,我們看執行insert
方法的時候,發生了什麼事情
// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 拿出啓動過程過程構建的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
// 調用typeHandler的setParameter方法,完成JavaType到數據庫字段的轉化
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
對象 json化,並調用ps.setString
方法,最終轉換成VARCHAR
保存起來
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));
}
// ...(省略)
}
總結
- 本文一開始提出在表中存儲json串的需求,並展示了
手動
將對象與json互轉的原始方式,隨後給出了Mybatis
優雅存取json字段的解決方案 -TypeHandler
- 接着,從
TypeHandler
的註冊過程開始介紹,分析了12個register
方法之間錯綜複雜的關係,最終得出註冊過程就是構建三個Map的過程,核心是TYPE_HANDLER_MAP
,它維護着<JavaType, <JdbcType, TypeHandler>>
的映射關係,在構造ParameterMapping
、ResultMapping
時使用到 - 然後,詳細闡述了在應用啓動過程中,
Mybatis
如何根據Mapper.xml
和TYPE_HANDLER_MAP
構造ParameterMapping
- 最後,簡述了當一個
<insert>
方法被調用時,typeHandler
如何工作
本文力求圍繞核心主題,緊着一條主脈落進行講解,爲避免被過多的分支幹擾,省略了不少旁枝末節,其中還包含一些比較重要的特性,因此下一篇,將分析typeHandler
結合MappedTypes
、MappedJdbcTypes
註解的使用方式