起因
在业务开发过程中,会经常碰到一些不需要检索,仅仅只是查询后使用的字段,例如配置信息,管理后台操作日志明细等,我们会将这些信息以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 extends BaseTypeHandler {
@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) 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) 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) getRawType());
}
}
- 定义具体实现类,继承上述
步骤1
中定义的AbstractObjectTypeHandler
,泛型中填上要转换的Java类型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler {}
- 删除
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, InitializingBean, ApplicationListener {
@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
) 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)
- 将
的映射关系保存到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
非空,将
的映射关系保存到> TYPE_HANDLER_MAP
中,从中可以看出,对于一个javaType
,可能存在多个typeHandler
,用于跟不同的jdbcType
进行转换 - 将
的映射关系保存到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> resolverUtil = new ResolverUtil>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set>> 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 TypeHandler getInstance(Class> javaTypeClass, Class> typeHandlerClass) {
// 省略try catch
if (javaTypeClass != null) {
Constructor> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler) c.newInstance(javaTypeClass);
}
Constructor> c = typeHandlerClass.getConstructor();
return (TypeHandler) 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.class
public void register(TypeHandler 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 typeReference = (TypeReference) 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) null, typeHandler);
}
}
public abstract class TypeReference {
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 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
、TYPE_HANDLER_MAP
private void register(Type javaType, JdbcType jdbcType, TypeHandler> handler) {
if (javaType != null) {
Map> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap>();
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
元素,它可能长
这样,该元素很常见,只是容易让人忽视
// 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中#{}
的封装,如
中的#{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
中
中子元素的封装,如
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 notNullColumns;
private String columnPrefix;
private List flags;
private List 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 list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List 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: and were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
}
接下来以insert
方法为例,方法签名是int insert(Foo record);
,对应的insert
statement是
SELECT LAST_INSERT_ID()
insert into foo (bar, create_time)
values (#{bar,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP})
接着调用到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());
}
public SqlSource parse(String originalSql, Class> parameterType, Map 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 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
,因此jdbcHandlerMap
为
。
再根据jdbcType
到jdbcHandlerMap
中找typeHandler
。此处经过两次查找:第一次以jdbcType(VARCHAR)
为key,第二次以null
为key。由于我们注册的BarTypeHandler
并没有明确指定jdbcType
,前文也提及到,不明确指定,就意味着不限制,就会将
注册到jdbcHandlerMap
,第一次通过通过jdbcHandlerMap.get(VARCHAR)
拿不到,第二次通过jdbcHandlerMap.get(null)
就拿到了不受jdbcType
限制的BarTypeHandler
// org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler
public TypeHandler getTypeHandler(Class type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
private TypeHandler getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map> 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) handler;
}
private Map> getJdbcHandlerMap(Type type) {
Map> 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;
}
经过上述分析,我们对于一个
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 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 extends BaseTypeHandler {
@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
,它维护着
的映射关系,在构造> ParameterMapping
、ResultMapping
时使用到 - 然后,详细阐述了在应用启动过程中,
Mybatis
如何根据Mapper.xml
和TYPE_HANDLER_MAP
构造ParameterMapping
- 最后,简述了当一个
方法被调用时,typeHandler
如何工作
本文力求围绕核心主题,紧着一条主脉落进行讲解,为避免被过多的分支干扰,省略了不少旁枝末节,其中还包含一些比较重要的特性,因此下一篇,将分析typeHandler
结合MappedTypes
、MappedJdbcTypes
注解的使用方式