Mybatis优雅存取json字段的解决方案 – TypeHandler (一)

起因

在业务开发过程中,会经常碰到一些不需要检索,仅仅只是查询后使用的字段,例如配置信息,管理后台操作日志明细等,我们会将这些信息以json的方式存储在RDBMS表里

假设某表foo的结构如下,字段bar就是以json的方式进行存储的

id bar create_time
1 {"name":"Shary","quz":10,"timestamp":1574698533370} 2019-11-26 00:15:50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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的字段里。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;
}

这种方式,存在两个问题

  1. 需要在实体类添加额外的非数据库字段(barObj)
  2. 需要在业务逻辑里手动转换,业务逻辑糅杂非业务代码,不够优雅

Mybatis 预定义的基础类型转换是靠TypeHandler实现的,那我们是不是也可以借鉴MyBatis的转换思路,来转换我们自定义的类型呢?

解决方案

  1. 定义一个抽象类,继承于org.apache.ibatis.type.BaseTypeHandler,用作对象类型的换转基类;之后但凡想varchar(longvarchar)对象互转,继承此基类即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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. 定义具体实现类,继承上述步骤1中定义的AbstractObjectTypeHandler,泛型中填上要转换的Java类型Bar
1
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
  1. 删除FooString bar,并将Bar barObj 改成Bar bar,让Foo的字段名跟数据库字段名一一对应
1
2
3
4
5
6
@Data
public class Foo {
    private Long id;
    private Bar bar;
    private Date createTime;
}

  1. 配置类型处理器扫包路径
  • 如果使用mybatis-spring-boot-starter,可以在application.properties里配置mybatis.typeHandlersPackage={BarTypeHandler所在包路径}
  • 如果只使用mybatis-spring,可以构造一个SqlSessionFactoryBean对象,并调用其setTypeHandlersPackage方法设置类型处理器扫包路径
  • 使用其它Mybatis扩展组件的,例如mybatis-plus,同理配置typeHandlersPackage属性即可

经过上述四个步骤之后,程序就能正常运行,无论插入数据,或者从数据库获取数据,都由Mybatis调用我们注册的BarTypeHandler进行转换,对于业务代码,做到了无感知使用,也不再存在冗余字段

1
2
3
4
5
6
7
8
9
@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.2mybatis 3.4.6 为例进行分析。本文比较难理解,建议手里就着源码进行阅读,体验会更佳

Configuration

使用mybatis-spring时,需要构造的一个核心对象是SqlSessionFactoryBean,它是一个Spring的FactoryBean,用于产生SqlSessionFactory对象。同时还实现了InitializingBean接口,受到Spring Bean的生命周期回调,执行afterPropertiesSet方法,在回调中构造了sqlSessionFactory对象

1
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
1
2
3
4
5
6
7
8
9
@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原始的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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还中持有非常多的对象,比如MapperRegistryTypeHandlerRegistryTypeAliasRegistryLanguageDriverRegistry,其中TypeHandlerRegistry用于TypeHandler的注册与管理,也是本文的主角

TypeHandlerRegistry的构造函数中,默认注册了几十个类型转化器,它们的存在,正是Mybatis非常便于使用的原因之一:帮助各种Java类型与JdbcType互转,比如java.util.DateJdbcType.TIMESTAMP互相转化,java.lang.StringJdbcType.VARCHARJdbcType.LONGVARCHAR互相转化,而JdbcType默认又与数据库类型有对应关系,为了便于理解,可以简单记为Java类型与数据库字段类型的转换。其中一部分示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 registerB registerC register,如果不撸清他们之间的关系,容易混乱:我是谁,我在哪,我在干什么

下面按照1个、2个、3个参数的register分类进行讲解

1个参数
  • register(String packageName)
    • 扫描packageName包下的TypeHandler类,如果非匿名内部类、非接口、非抽象类,就调用register(typeHandlerClass)进行注册
  • 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并没有直接使用到,内部是将javaTypeClassNametypeHandlerClassName分别转成Class类型,并调用register(javaTypeClass, typeHandlerClass)进行注册
  • register(TypeReference javaTypeReference, TypeHandler 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 typeHandler)
    • 如果typeHandler的Class上有MappedJdbcTypes注解
      • 注解里配置了JdbcType,调用register(javaType, handledJdbcType, typeHandler)进行注册
      • 否则,若includeNullJdbcType = true,调用register(javaType, null, typeHandler)进行注册
    • 否则,调用register(javaType, null, typeHandler)进行注册
  • register(Class javaType, TypeHandler 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.xmltypeHandlers`元素时,可能会调用该方法进行注册,而前文已说过,与spring结合后,该文件已经被抛弃,故不用太关注
  • register(Class type, JdbcType jdbcType, TypeHandler handler)
    • 内部将type强转为Type类型后,直接调用register((Type) javaType, jdbcType, handler)
  • register(Type javaType, JdbcType jdbcType, TypeHandler handler)
    • javaType非空,将>的映射关系保存到TYPE_HANDLER_MAP中,从中可以看出,对于一个javaType,可能存在多个typeHandler,用于跟不同的jdbcType进行转换
    • 的映射关系保存到ALL_TYPE_HANDLERS_MAP

以上是从代码的角度进行解读,确保逻辑无误,但容易让人云里雾里,不便于理解,因此有必要在此基础上总结一下规律:

  1. 单参数的register方法有3个,双参数的6个,三参数的3个,共计12个;将拥有相同入参数量的register方法归为同一层,各层次内部有调用的关系,上层也会调用下层方法,但不存在跨层调用,而最下层,是将注册的各个类型保存到Map维护起来
  2. 12register方法,目的都是为了寻找JavaType、JdbcType、TypeHandler及他们之间的关系,最终维护在3个Map中:JDBC_TYPE_HANDLER_MAPTYPE_HANDLER_MAPALL_TYPE_HANDLERS_MAP
  3. javaType、javaTypeClass 描述的是待转换java的类型,在例子中就是Bar.class;JdbcType是一个枚举类型,代表Jdbc类型,典型的取值有JdbcType.VARCHAR、JdbcType.BIGINTtypeHandler、BarTypeHandler分别代表类型转换器实例及其Class实例,在例子中就是BarTypeHandler、BarTypeHandler.class
  4. MappedTypesMappedJdbcTypes是两个注解,作用于TypeHandler上,用于指示、限定其所能支持的JavaType以及JdbcType

出于篇幅原因以及理解复杂度的考虑,本篇不涉及注解方案,会在后续篇章继续介绍注解的使用姿势及原理,消化了本篇所介绍的内容,届时会更容易理解注解的使用。

接着,回到buildSqlSessionFactory扫包处接着往下看,找到符合条件的类型处理器并调用register(type)

1
2
3
4
5
6
7
8
9
10
11
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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指的是前文中提到的JavaTypeMybatis 3.1.0新增了一个抽象类TypeReference,它是BaseTypeHandler的抽象基类,该类只有一个能力,就是使用"标准姿势"提取泛型具体类,即提取JavaType,比如public class BarTypeHandler extends AbstractObjectTypeHandler,提取的就是Bar.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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不做限制

1
2
3
4
5
6
7
8
9
10
11
12
13
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_MAPTYPE_HANDLER_MAP

1
2
3
4
5
6
7
8
9
10
11
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资源集合,遍历每一个文件,并进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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元素,它可能长这样,该元素很常见,只是容易让人忽视

1
2
3
4
5
6
7
8
9
10
11
// 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本身的所有元素,如namespacecache-refcacheresultMapsqlselect|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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中子元素的封装,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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个同名参数需要我们重点关注:javaTypejdbcTypetypeHandler。我们可以手动指定ParameterMappingResultMappingtypeHandler,若未明确指定,Mybatis会在应用启动解析xml文件过程中,为其智能匹配上合适的值,若匹配不到,会抛出异常No typehandler found for property ...。这也暗示着一个事实:MyBatis依托于无论内置的还是自定义的typeHandlerJavaTypeJdbcType之间的转换,是框架得以正常运转的前提,是赖以生存的基础能力

构造ParameterMappingResultMapping的代码有高度一致性,甚至就typeHandler相关而言,基本完全一样,因此本文仅用ParameterMapping介绍

回到configurationElement方法,方法内部调用buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 读取xml文件所有statement元素,遍历该元素集合并调用statementParser.parseStatementNode()解析集合里的每一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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各类属性值,比如resultTypeparameterTypetimeoutflushCache等,为了突出重点,把其余的省略。

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.

1
2
3
4
5
6
7
8
9
10
11
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是

1
2
3
4
5
6
7
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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注入的方式是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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,解析出javaTypejdbcTypetypeHandler等属性,构建并填充ParameterMapping对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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会智能去匹配,当然,有时候也不是那么智能,匹配的结果跟我们预期的不太一样,这时候手动指定会更合适

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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其实只是根据javaTypeTYPE_HANDLER_MAP取,从前文中我们知道,TYPE_HANDLER_MAP中存在这么一条entry >,因此jdbcHandlerMap

再根据jdbcTypejdbcHandlerMap中找typeHandler。此处经过两次查找:第一次以jdbcType(VARCHAR)为key,第二次以null为key。由于我们注册的BarTypeHandler并没有明确指定jdbcType,前文也提及到,不明确指定,就意味着不限制,就会将注册到jdbcHandlerMap,第一次通过通过jdbcHandlerMap.get(VARCHAR)拿不到,第二次通过jdbcHandlerMap.get(null)就拿到了不受jdbcType限制的BarTypeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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;
}

经过上述分析,我们对于一个 statement,拿到了对应的SqlSource,里面包含着解析后的SQL(如:insert into foo (bar, create_time) values (?, ?))以及ParameterMapping集合等信息,之所以是集合,是因为一个statement里可能包含多个#{},而每一个#{}都对应着一个ParameterMapping

接下来,我们看执行insert方法的时候,发生了什么事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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保存起来

1
2
3
4
5
6
7
8
9
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));
    }

    // ...(省略)
}

总结

  1. 本文一开始提出在表中存储json串的需求,并展示了手动将对象与json互转的原始方式,随后给出了Mybatis优雅存取json字段的解决方案 - TypeHandler
  2. 接着,从TypeHandler的注册过程开始介绍,分析了12个register方法之间错综复杂的关系,最终得出注册过程就是构建三个Map的过程,核心是TYPE_HANDLER_MAP,它维护着>的映射关系,在构造ParameterMappingResultMapping时使用到
  3. 然后,详细阐述了在应用启动过程中,Mybatis如何根据Mapper.xmlTYPE_HANDLER_MAP构造ParameterMapping
  4. 最后,简述了当一个方法被调用时,typeHandler如何工作

本文力求围绕核心主题,紧着一条主脉落进行讲解,为避免被过多的分支干扰,省略了不少旁枝末节,其中还包含一些比较重要的特性,因此下一篇,将分析typeHandler结合MappedTypesMappedJdbcTypes注解的使用方式