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
@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;
}

这种方式,存在两个问题

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

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

解决方案

  1. 定义一个抽象类,继承于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. 定义具体实现类,继承上述步骤1中定义的AbstractObjectTypeHandler,泛型中填上要转换的Java类型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler {}
  1. 删除FooString bar,并将Bar barObj 改成Bar bar,让Foo的字段名跟数据库字段名一一对应
@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进行转换,对于业务代码,做到了无感知使用,也不再存在冗余字段

@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对象

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还中持有非常多的对象,比如MapperRegistryTypeHandlerRegistryTypeAliasRegistryLanguageDriverRegistry,其中TypeHandlerRegistry用于TypeHandler的注册与管理,也是本文的主角

TypeHandlerRegistry的构造函数中,默认注册了几十个类型转化器,它们的存在,正是Mybatis非常便于使用的原因之一:帮助各种Java类型与JdbcType互转,比如java.util.DateJdbcType.TIMESTAMP互相转化,java.lang.StringJdbcType.VARCHARJdbcType.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 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)


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指的是前文中提到的JavaTypeMybatis 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 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

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本身的所有元素,如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.

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个同名参数需要我们重点关注: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()解析集合里的每一个元素

// 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各类属性值,比如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.

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,解析出javaTypejdbcTypetypeHandler等属性,构建并填充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其实只是根据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

// 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));
    }

    // ...(省略)
}

总结

  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注解的使用方式

你可能感兴趣的:(Mybatis优雅存取json字段的解决方案 - TypeHandler (一))