Mybatis優(yōu)雅存取json字段的解決方案 - TypeHandler (一)

起因

在業(yè)務(wù)開發(fā)過程中,會(huì)經(jīng)常碰到一些不需要檢索,僅僅只是查詢后使用的字段,例如配置信息,管理后臺(tái)操作日志明細(xì)等,我們會(huì)將這些信息以json的方式存儲(chǔ)在RDBMS表里

假設(shè)某表foo的結(jié)構(gòu)如下,字段bar就是以json的方式進(jìn)行存儲(chǔ)的

id bar create_time
1 {"name":"Shary","quz":10,"timestamp":1574698533370} 2019-11-26 00:15:50
@Data
public class Foo {
    private Long id;
    private String bar;
    private Bar barObj;
    private Date createTime;
}

@Data
public class Bar {
    private String name;
    private Integer quz;
    private Date timestamp;
}

在代碼中,比較原始的解決方式是手動(dòng)解決:查詢時(shí),將json串轉(zhuǎn)成對(duì)象,放進(jìn)對(duì)象字段里;保存時(shí),手動(dòng)將對(duì)象轉(zhuǎn)成json串,然后放進(jìn)String的字段里。如下所示

@Override
public Foo getById(Long id) {
    Foo foo = fooMapper.selectByPrimaryKey(id);
    String bar = foo.getBar();
    Bar barObj = JsonUtil.fromJson(bar, Bar.class);
    foo.setBarObj(barObj);
    return foo;
}

@Override
public boolean save(Foo foo) {
    Bar barObj = foo.getBarObj();
    foo.setBar(JsonUtil.toJson(barObj));
    return fooMapper.insert(foo) > 0;
}

這種方式,存在兩個(gè)問題

  1. 需要在實(shí)體類添加額外的非數(shù)據(jù)庫字段(barObj)
  2. 需要在業(yè)務(wù)邏輯里手動(dòng)轉(zhuǎn)換,業(yè)務(wù)邏輯糅雜非業(yè)務(wù)代碼,不夠優(yōu)雅

Mybatis 預(yù)定義的基礎(chǔ)類型轉(zhuǎn)換是靠TypeHandler實(shí)現(xiàn)的,那我們是不是也可以借鑒MyBatis的轉(zhuǎn)換思路,來轉(zhuǎn)換我們自定義的類型呢?

解決方案

  1. 定義一個(gè)抽象類,繼承于org.apache.ibatis.type.BaseTypeHandler,用作對(duì)象類型的換轉(zhuǎn)基類;之后但凡想varchar(longvarchar)對(duì)象互轉(zhuǎn),繼承此基類即可
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. 定義具體實(shí)現(xiàn)類,繼承上述步驟1中定義的AbstractObjectTypeHandler,泛型中填上要轉(zhuǎn)換的Java類型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
  1. 刪除FooString bar,并將Bar barObj 改成Bar bar,讓Foo的字段名跟數(shù)據(jù)庫字段名一一對(duì)應(yīng)
@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,可以構(gòu)造一個(gè)SqlSessionFactoryBean對(duì)象,并調(diào)用其setTypeHandlersPackage方法設(shè)置類型處理器掃包路徑
  • 使用其它Mybatis擴(kuò)展組件的,例如mybatis-plus,同理配置typeHandlersPackage屬性即可

經(jīng)過上述四個(gè)步驟之后,程序就能正常運(yùn)行,無論插入數(shù)據(jù),或者從數(shù)據(jù)庫獲取數(shù)據(jù),都由Mybatis調(diào)用我們注冊(cè)的BarTypeHandler進(jìn)行轉(zhuǎn)換,對(duì)于業(yè)務(wù)代碼,做到了無感知使用,也不再存在冗余字段

@Override
public Foo getById(Long id) {
    return fooMapper.selectByPrimaryKey(id);
}

@Override
public boolean save(Foo foo) {
    return fooMapper.insert(foo) > 0;
}

原理分析

如果只是于使用而言,按照步驟1234走即可,而且4只需要走一次。但是,我們顯然不能止步于此,知其然,知其所以然,才能用的安心,用的放心,用的順手

接下來會(huì)以mybatis-spring 1.3.2,mybatis 3.4.6 為例進(jìn)行分析。本文比較難理解,建議手里就著源碼進(jìn)行閱讀,體驗(yàn)會(huì)更佳

Configuration

使用mybatis-spring時(shí),需要構(gòu)造的一個(gè)核心對(duì)象是SqlSessionFactoryBean,它是一個(gè)Spring的FactoryBean,用于產(chǎn)生SqlSessionFactory對(duì)象。同時(shí)還實(shí)現(xiàn)了InitializingBean接口,受到Spring Bean的生命周期回調(diào),執(zhí)行afterPropertiesSet方法,在回調(diào)中構(gòu)造了sqlSessionFactory對(duì)象

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");

  this.sqlSessionFactory = buildSqlSessionFactory();
}

而在buildSqlSessionFactory方法中,構(gòu)造了Mybatis的核心配置類Configuration,并且進(jìn)行了初始化。當(dāng)Mybatis不結(jié)合Spring使用時(shí),就需要自己構(gòu)造Configuration對(duì)象,這個(gè)對(duì)應(yīng)于mybatis-config.xml配置文件,具體使用規(guī)則可以參考官網(wǎng) 。當(dāng)然,mybatis-spring幫我們搞定了配置Configuration的事,同時(shí)也拋棄了mybatis-config.xml原始的配置文件

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

Configuration configuration;

// ...(省略)

  configuration = new Configuration();

// ...(省略)

if (hasLength(this.typeHandlersPackage)) { //配置的類型處理器所在包
  String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
      ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
  for (String packageToScan : typeHandlersPackageArray) {
    // 掃包進(jìn)行注冊(cè)
    configuration.getTypeHandlerRegistry().register(packageToScan);
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
    }
  }
}

if (!isEmpty(this.typeHandlers)) {
  for (TypeHandler<?> typeHandler : this.typeHandlers) {
    configuration.getTypeHandlerRegistry().register(typeHandler);
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Registered type handler: '" + typeHandler + "'");
    }
  }
}
// ...(省略)

Configuration還中持有非常多的對(duì)象,比如MapperRegistry、TypeHandlerRegistry、TypeAliasRegistryLanguageDriverRegistry,其中TypeHandlerRegistry用于TypeHandler的注冊(cè)與管理,也是本文的主角

TypeHandlerRegistry的構(gòu)造函數(shù)中,默認(rèn)注冊(cè)了幾十個(gè)類型轉(zhuǎn)化器,它們的存在,正是Mybatis非常便于使用的原因之一:幫助各種Java類型與JdbcType互轉(zhuǎn),比如java.util.DateJdbcType.TIMESTAMP互相轉(zhuǎn)化,java.lang.StringJdbcType.VARCHAR、JdbcType.LONGVARCHAR互相轉(zhuǎn)化,而JdbcType默認(rèn)又與數(shù)據(jù)庫類型有對(duì)應(yīng)關(guān)系,為了便于理解,可以簡(jiǎn)單記為Java類型與數(shù)據(jù)庫字段類型的轉(zhuǎn)換。其中一部分示例如下

public TypeHandlerRegistry() {
   register(Boolean.class, new BooleanTypeHandler());
   register(boolean.class, new BooleanTypeHandler());
   register(JdbcType.BOOLEAN, new BooleanTypeHandler());
   register(JdbcType.BIT, new BooleanTypeHandler());

   register(Byte.class, new ByteTypeHandler());
   register(byte.class, new ByteTypeHandler());
   register(JdbcType.TINYINT, new ByteTypeHandler());

   register(Short.class, new ShortTypeHandler());
   register(short.class, new ShortTypeHandler());
   register(JdbcType.SMALLINT, new ShortTypeHandler());

   register(Integer.class, new IntegerTypeHandler());
   register(int.class, new IntegerTypeHandler());
   register(JdbcType.INTEGER, new IntegerTypeHandler());

   // ...(省略)
}

TypeHandlerRegistry有十余個(gè)名為register的重載方法,乍一看容易讓人頭昏眼花,更讓人崩潰的是,A register還會(huì)調(diào)B registerB register調(diào)C register,如果不擼清他們之間的關(guān)系,容易混亂:我是誰,我在哪,我在干什么

下面按照1個(gè)、2個(gè)、3個(gè)參數(shù)的register分類進(jìn)行講解

1個(gè)參數(shù)
  • register(String packageName)
    • 掃描packageName包下的TypeHandler類,如果非匿名內(nèi)部類、非接口、非抽象類,就調(diào)用register(typeHandlerClass)進(jìn)行注冊(cè)
  • register(Class<?> typeHandlerClass)
    • 如果typeHandlerClass上有MappedTypes注解,且注解里配置了映射的類型,就調(diào)用register(javaTypeClass, typeHandlerClass)進(jìn)行注冊(cè)
    • 否則,調(diào)用getInstance生成TypeHandler實(shí)例,并調(diào)用register(typeHandler)進(jìn)行注冊(cè)
  • register(TypeHandler<T> typeHandler)
    • 如果typeHandler的Class上有MappedTypes注解,且注解里配置了映射的類型,就調(diào)用register(handledType, typeHandler)進(jìn)行注冊(cè)
    • 否則,typeHandler如果是TypeReference的實(shí)例,就調(diào)用register(typeReference.getRawType(), typeHandler)進(jìn)行注冊(cè)。typeReference.getRawType()獲得的結(jié)果是TypeReference的泛型
    • 否則,調(diào)用register((Class<T>) null, typeHandler)進(jìn)行注冊(cè)
2個(gè)參數(shù)
  • register(String javaTypeClassName, String typeHandlerClassName)
    • Mybatis并沒有直接使用到,內(nèi)部是將javaTypeClassName、typeHandlerClassName分別轉(zhuǎn)成Class類型,并調(diào)用register(javaTypeClass, typeHandlerClass)進(jìn)行注冊(cè)
  • register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler)
    • Mybatis并沒有直接使用到,內(nèi)部是從javaTypeReference獲取到rawType之后,調(diào)用register(javaType, typeHandler)進(jìn)行注冊(cè)
  • register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
    • 調(diào)用getInstance生成TypeHandler實(shí)例后,調(diào)用register(javaTypeClass, typeHandler)進(jìn)行注冊(cè)
    • 該方法在TypeHandlerRegistry構(gòu)造函數(shù)中被大量調(diào)用,主要用于支持JSR310的日期類型處理(Since Mybatis 3.4.5),如this.register(Instant.class, InstantTypeHandler.class)。不過需要吐槽的一點(diǎn)是,由于開發(fā)者與之前不同,因此注冊(cè)的風(fēng)格與之前不同,調(diào)用的API也不同,增加了學(xué)習(xí)成本
  • register(Type javaType, TypeHandler<? extends T> typeHandler)
    • 如果typeHandler的Class上有MappedJdbcTypes注解
      • 注解里配置了JdbcType,調(diào)用register(javaType, handledJdbcType, typeHandler)進(jìn)行注冊(cè)
      • 否則,若includeNullJdbcType = true,調(diào)用register(javaType, null, typeHandler)進(jìn)行注冊(cè)
    • 否則,調(diào)用register(javaType, null, typeHandler)進(jìn)行注冊(cè)
  • register(Class<T> javaType, TypeHandler<? extends T> typeHandler)
    • 內(nèi)部調(diào)用register(javaType, typeHandler)
    • 該方法在TypeHandlerRegistry構(gòu)造函數(shù)中被大量調(diào)用,如register(Date.class, new DateTypeHandler())
  • register(JdbcType jdbcType, TypeHandler<?> handler)
    • <JdbcType, TypeHandler>的映射關(guān)系保存到JDBC_TYPE_HANDLER_MAP
    • 該方法在TypeHandlerRegistry構(gòu)造函數(shù)中被大量調(diào)用,如register(JdbcType.INTEGER, new IntegerTypeHandler())
3個(gè)參數(shù)
  • register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
    • 調(diào)用getInstance生成TypeHandler實(shí)例后,調(diào)用register(javaTypeClass, jdbcType, typeHandler)進(jìn)行注冊(cè)
    • 很少用到,只有在Mybatis解析``mybatis-config.xmltypeHandlers`元素時(shí),可能會(huì)調(diào)用該方法進(jìn)行注冊(cè),而前文已說過,與spring結(jié)合后,該文件已經(jīng)被拋棄,故不用太關(guān)注
  • register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)
    • 內(nèi)部將type強(qiáng)轉(zhuǎn)為Type類型后,直接調(diào)用register((Type) javaType, jdbcType, handler)
  • register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
    • javaType非空,將<JavaType, <JdbcType, TypeHandler>>的映射關(guān)系保存到TYPE_HANDLER_MAP中,從中可以看出,對(duì)于一個(gè)javaType,可能存在多個(gè)typeHandler,用于跟不同的jdbcType進(jìn)行轉(zhuǎn)換
    • <TypeHandlerClass, TypeHandler>的映射關(guān)系保存到ALL_TYPE_HANDLERS_MAP

以上是從代碼的角度進(jìn)行解讀,確保邏輯無誤,但容易讓人云里霧里,不便于理解,因此有必要在此基礎(chǔ)上總結(jié)一下規(guī)律:

  1. 單參數(shù)的register方法有3個(gè),雙參數(shù)的6個(gè),三參數(shù)的3個(gè),共計(jì)12個(gè);將擁有相同入?yún)?shù)量的register方法歸為同一層,各層次內(nèi)部有調(diào)用的關(guān)系,上層也會(huì)調(diào)用下層方法,但不存在跨層調(diào)用,而最下層,是將注冊(cè)的各個(gè)類型保存到Map維護(hù)起來
  2. 12個(gè)register方法,目的都是為了尋找JavaType、JdbcType、TypeHandler及他們之間的關(guān)系,最終維護(hù)在3個(gè)Map中:JDBC_TYPE_HANDLER_MAPTYPE_HANDLER_MAPALL_TYPE_HANDLERS_MAP
  3. javaType、javaTypeClass 描述的是待轉(zhuǎn)換java的類型,在例子中就是Bar.class;JdbcType是一個(gè)枚舉類型,代表Jdbc類型,典型的取值有JdbcType.VARCHAR、JdbcType.BIGINT;typeHandler、BarTypeHandler分別代表類型轉(zhuǎn)換器實(shí)例及其Class實(shí)例,在例子中就是BarTypeHandler、BarTypeHandler.class
  4. MappedTypesMappedJdbcTypes是兩個(gè)注解,作用于TypeHandler上,用于指示、限定其所能支持的JavaType以及JdbcType

出于篇幅原因以及理解復(fù)雜度的考慮,本篇不涉及注解方案,會(huì)在后續(xù)篇章繼續(xù)介紹注解的使用姿勢(shì)及原理,消化了本篇所介紹的內(nèi)容,屆時(shí)會(huì)更容易理解注解的使用。

接著,回到buildSqlSessionFactory掃包處接著往下看,找到符合條件的類型處理器并調(diào)用register(type)


public void register(String packageName) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
  Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
  for (Class<?> type : handlerSet) {
    //Ignore inner classes and interfaces (including package-info.java) and abstract classes
    if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
      register(type);
    }
  }
}

邏輯會(huì)走到下邊部分,根據(jù)(null, typeHandlerClass)獲取TypeHandler實(shí)例,方法第一個(gè)入?yún)?code>javaTypeClass,而此處并不知道javaTypeClass是什么,因此傳入的值null,而獲取實(shí)例的方法也很簡(jiǎn)單,根據(jù)javaTypeClass是否為空來判斷使用哪個(gè)typeHandlerClass的構(gòu)造函數(shù)來構(gòu)造例實(shí)。獲取實(shí)例之后調(diào)用register(typeHandler)

public void register(Class<?> typeHandlerClass) {
  boolean mappedTypeFound = false;
  // 本篇不涉及注解使用方式,因此 mappedTypeFound = false
  MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> javaTypeClass : mappedTypes.value()) {
      register(javaTypeClass, typeHandlerClass);
      mappedTypeFound = true;
    }
  }
  if (!mappedTypeFound) {
    // 走這段邏輯
    register(getInstance(null, typeHandlerClass));
  }
}


public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
  // 省略try catch
  if (javaTypeClass != null) {
    Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
    return (TypeHandler<T>) c.newInstance(javaTypeClass);
  }
  
  Constructor<?> c = typeHandlerClass.getConstructor();
  return (TypeHandler<T>) c.newInstance();
}

同樣忽略注解部分。從2012年發(fā)布Mybatis 3.1.0開始,支持自動(dòng)發(fā)現(xiàn)mapped type的特性,這兒的mapped type指的是前文中提到的JavaTypeMybatis 3.1.0新增了一個(gè)抽象類TypeReference,它是BaseTypeHandler的抽象基類,該類只有一個(gè)能力,就是使用"標(biāo)準(zhǔn)姿勢(shì)"提取泛型具體類,即提取JavaType,比如public class BarTypeHandler extends AbstractObjectTypeHandler<Bar>,提取的就是Bar.class

public <T> void register(TypeHandler<T> typeHandler) {
  boolean mappedTypeFound = false;
  MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> handledType : mappedTypes.value()) {
      register(handledType, typeHandler);
      mappedTypeFound = true;
    }
  }
  // @since 3.1.0 - try to auto-discover the mapped type
  if (!mappedTypeFound && typeHandler instanceof TypeReference) {
    try {
      TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
      register(typeReference.getRawType(), typeHandler);
      mappedTypeFound = true;
    } catch (Throwable t) {
      // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
    }
  }
  if (!mappedTypeFound) {
    register((Class<T>) null, typeHandler);
  }
}
public abstract class TypeReference<T> {

  private final Type rawType;

  protected TypeReference() {
    rawType = getSuperclassTypeParameter(getClass());
  }

  Type getSuperclassTypeParameter(Class<?> clazz) {
    Type genericSuperclass = clazz.getGenericSuperclass();
    if (genericSuperclass instanceof Class) {
      // try to climb up the hierarchy until meet something useful
      if (TypeReference.class != genericSuperclass) {
        return getSuperclassTypeParameter(clazz.getSuperclass());
      }

      throw new TypeException("'" + getClass() + "' extends TypeReference but misses the type parameter. "
        + "Remove the extension or add a type parameter to it.");
    }

    Type rawType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
    // TODO remove this when Reflector is fixed to return Types
    if (rawType instanceof ParameterizedType) {
      rawType = ((ParameterizedType) rawType).getRawType();
    }

    return rawType;
  }
  // ...(省略)
}

調(diào)用register(javaType, null, typeHandler),該方法第二個(gè)參數(shù)是JdbcType,而我們沒有配置MappedJdbcTypes注解,因此為null,代表的是對(duì)JdbcType不做限制

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
  MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
  if (mappedJdbcTypes != null) {
    for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
      register(javaType, handledJdbcType, typeHandler);
    }
    if (mappedJdbcTypes.includeNullJdbcType()) {
      register(javaType, null, typeHandler);
    }
  } else {
    register(javaType, null, typeHandler);
  }
}

終于來到最后維護(hù)Map的方法,根據(jù)源碼,很容易看出主要是維護(hù)ALL_TYPE_HANDLERS_MAP<typeHandlerClass, typeHandler>、TYPE_HANDLER_MAP<javaType, jdbcType,typeHandler>

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
  if (javaType != null) {
    Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
    if (map == null || map == NULL_TYPE_HANDLER_MAP) {
      map = new HashMap<JdbcType, TypeHandler<?>>();
      TYPE_HANDLER_MAP.put(javaType, map);
    }
    map.put(jdbcType, handler);
  }
  ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

上面分析typeHandler是如何注冊(cè)的,接下來分析它是如何與mapper.xml關(guān)聯(lián)起來的

注: 由于接下來基本與mapper.xml相關(guān),如無特殊說明,將用xml來指代mapper.xml,而不是mybatis-config.xml

繼續(xù)回到buildSqlSessionFactory方法,往下看,mapperLocations的類型是Resource[],代表xml資源集合,遍歷每一個(gè)文件,并進(jìn)行解析

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
  // ...(省略)
  
  if (!isEmpty(this.mapperLocations)) {
    for (Resource mapperLocation : this.mapperLocations) {
      if (mapperLocation == null) {
        continue;
      }
      XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
          configuration, mapperLocation.toString(), configuration.getSqlFragments());
      xmlMapperBuilder.parse();
      // ...(省略)
    }
  }
  // ...(省略)

使用XPath讀取mapper元素的值,并將結(jié)果傳入configurationElement進(jìn)行更深層次的解析。任意打開一個(gè)xml文件,在DOCTYPE聲明后緊跟著的第一行即是mapper元素,它可能長(zhǎng)<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等,這些元素我們已經(jīng)很熟悉,而parameterMap已經(jīng)被Mybatis打入冷宮,連官網(wǎng)都不愿著筆墨介紹,不需要關(guān)注。

parameterMap – Deprecated! Old-school way to map parameters. Inline parameters are preferred and this element may be removed in the future. Not documented here.

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析resultMap元素
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 解析CRUD 元素
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}
ParameterMapping、ResultMapping

ParameterMapping: 請(qǐng)求參數(shù)的映射關(guān)系,是對(duì)xml中每個(gè)statement中#{}的封裝,如<insert>中的#{bar,jdbcType=VARCHAR}

public class ParameterMapping {

  private Configuration configuration;

  private String property;
  private ParameterMode mode;
  private Class<?> javaType = Object.class;
  private JdbcType jdbcType;
  private Integer numericScale;
  private TypeHandler<?> typeHandler;
  private String resultMapId;
  private String jdbcTypeName;
  private String expression;

  // ...(省略)
}

ResultMapping: 結(jié)果集的映射關(guān)系,是對(duì)xml<resultMap>中子元素的封裝,如<result column="bar" property="bar" jdbcType="VARCHAR" />

public class ResultMapping {

  private Configuration configuration;
  private String property;
  private String column;
  private Class<?> javaType;
  private JdbcType jdbcType;
  private TypeHandler<?> typeHandler;
  private String nestedResultMapId;
  private String nestedQueryId;
  private Set<String> notNullColumns;
  private String columnPrefix;
  private List<ResultFlag> flags;
  private List<ResultMapping> composites;
  private String resultSet;
  private String foreignColumn;
  private boolean lazy;

  // ...(省略)
}

二者有3個(gè)同名參數(shù)需要我們重點(diǎn)關(guān)注:javaType、jdbcType、typeHandler。我們可以手動(dòng)指定ParameterMappingResultMappingtypeHandler,若未明確指定,Mybatis會(huì)在應(yīng)用啟動(dòng)解析xml文件過程中,為其智能匹配上合適的值,若匹配不到,會(huì)拋出異常No typehandler found for property ...。這也暗示著一個(gè)事實(shí):MyBatis依托于無論內(nèi)置的還是自定義的typeHandlerJavaTypeJdbcType之間的轉(zhuǎn)換,是框架得以正常運(yùn)轉(zhuǎn)的前提,是賴以生存的基礎(chǔ)能力

構(gòu)造ParameterMappingResultMapping的代碼有高度一致性,甚至就typeHandler相關(guān)而言,基本完全一樣,因此本文僅用ParameterMapping介紹

回到configurationElement方法,方法內(nèi)部調(diào)用buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 讀取xml文件所有statement元素,遍歷該元素集合并調(diào)用statementParser.parseStatementNode()解析集合里的每一個(gè)元素

// org.apache.ibatis.builder.xml.XMLMapperBuilder

private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    // 省略try catch 
    statementParser.parseStatementNode();
  }
}

parseStatementNode方法內(nèi)部代碼雖比較多,但是本身并不難理解,主要是提取并解析statement各類屬性值,比如resultType、parameterType、timeout、flushCache等,為了突出重點(diǎn),把其余的省略。

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);,對(duì)應(yīng)的insert statement是

<insert id="insert" parameterType="com.example.demo.model.Foo" >
  <selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER" >
    SELECT LAST_INSERT_ID()
  </selectKey>
  insert into foo (bar, create_time)
  values (#{bar,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP})
</insert>

接著調(diào)用到langDriver.createSqlSource

// org.apache.ibatis.scripting.xmltags.XMLLanguageDriver

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource = null;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    // 走這兒,parameterType代表入?yún)⒌念愋停谖覀僣ase中代表Foo.class
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
  this(configuration, getSql(configuration, rootSqlNode), parameterType);
}

// sql 代表從statement中提取的原始未經(jīng)加工的SQL,帶有#{bar,jdbcType=VARCHAR}等信息
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> clazz = parameterType == null ? Object.class : parameterType;
  sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  // ParameterMapping處理器
  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  // 解析器,解析 #{}
  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  // 重點(diǎn)
  String sql = parser.parse(originalSql);
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

來到org.apache.ibatis.parsing.GenericTokenParser#parse,該方法根據(jù)傳入的原始sql,解析里邊#{}所代表的內(nèi)容,在我們的case中,結(jié)果是bar,jdbcType=VARCHAR,將結(jié)果保存在expression變量中,調(diào)用ParameterMappingTokenHandler#handleToken進(jìn)行處理。每一個(gè)#{}代表了原始SQL中的?,因此handleToken方法的返回值就是?,使用過JDBC編程的同學(xué)應(yīng)該也明白?代表的含義---->從此處我們也證實(shí)了,#{}的方式屏蔽了SQL注入的風(fēng)險(xiǎn),與原生JDBC編程中使用?的預(yù)防SQL注入的方式是一樣的

// org.apache.ibatis.parsing.GenericTokenParser#parse

public String parse(String text) {
  // ...(省略)
  builder.append(handler.handleToken(expression.toString()));
  // ...(省略)
}

// org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken

public String handleToken(String content) {
  parameterMappings.add(buildParameterMapping(content));
  return "?";
}

buildParameterMapping方法根據(jù)傳入的expression,解析出javaType、jdbcType、typeHandler等屬性,構(gòu)建并填充ParameterMapping對(duì)象

private ParameterMapping buildParameterMapping(String content) {
  // ...(省略)
  // propertyType = Bar.class
  ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
  Class<?> javaType = propertyType;
  String typeHandlerAlias = null;
  for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
    String name = entry.getKey();
    String value = entry.getValue();
    if ("javaType".equals(name)) {
      javaType = resolveClass(value);
      builder.javaType(javaType);
    } else if ("jdbcType".equals(name)) {
      builder.jdbcType(resolveJdbcType(value));
    } else if ("mode".equals(name)) {
      builder.mode(resolveParameterMode(value));
    } else if ("numericScale".equals(name)) {
      builder.numericScale(Integer.valueOf(value));
    } else if ("resultMap".equals(name)) {
      builder.resultMapId(value);
    } else if ("typeHandler".equals(name)) {
      typeHandlerAlias = value;
    } else if // ...(省略)
  }
  return builder.build();
}

build方法做了兩件事,一是再次解析typeHandler,二是校驗(yàn)typeHandler是否為空,如果為空,則拋出異常。為什么需要再次解析?是因?yàn)橛锌赡茉?{}中未明確指定使用哪個(gè)typeHandler,即parameterMapping.typeHandler == null,這時(shí)候Mybatis會(huì)智能去匹配,當(dāng)然,有時(shí)候也不是那么智能,匹配的結(jié)果跟我們預(yù)期的不太一樣,這時(shí)候手動(dòng)指定會(huì)更合適

// org.apache.ibatis.mapping.ParameterMapping.Builder#build

public ParameterMapping build() {
  resolveTypeHandler();
  validate();
  return parameterMapping;
}

private void resolveTypeHandler() {
  // 再次解析typeHandler
  if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
    Configuration configuration = parameterMapping.configuration;
    TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    // 根據(jù)javaType、jdbcType去typeHandlerRegistry中找typeHandler
    parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
  }
}

private void validate() {
  // javaType為ResultSet類型,這種使用姿勢(shì)較少,可以跳過
  if (ResultSet.class.equals(parameterMapping.javaType)) {
    if (parameterMapping.resultMapId == null) { 
      throw new IllegalStateException("Missing resultmap in property '"  
          + parameterMapping.property + "'.  " 
          + "Parameters of type java.sql.ResultSet require a resultmap.");
    }            
  } else {
    // 再次解析后還空,拋出異常
    if (parameterMapping.typeHandler == null) { 
      throw new IllegalStateException("Type handler was null on parameter mapping for property '"
        + parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType ("
        + parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination.");
    }
  }
}

在我們的case中,并未明確指定typeHandler,因此resolveTypeHandler中,滿足parameterMapping.typeHandler == null的條件,調(diào)用typeHandlerRegistry.getTypeHandler方法進(jìn)行智能匹配

先根據(jù)javaType調(diào)用getJdbcHandlerMap方法拿到jdbcHandlerMap,而
getJdbcHandlerMap其實(shí)只是根據(jù)javaTypeTYPE_HANDLER_MAP取,從前文中我們知道,TYPE_HANDLER_MAP中存在這么一條entry <Bar.class, <null, BarTypeHandler>>,因此jdbcHandlerMap<null, BarTypeHandler>。

再根據(jù)jdbcTypejdbcHandlerMap中找typeHandler。此處經(jīng)過兩次查找:第一次以jdbcType(VARCHAR)為key,第二次以null為key。由于我們注冊(cè)的BarTypeHandler并沒有明確指定jdbcType,前文也提及到,不明確指定,就意味著不限制,就會(huì)將<null, BarTypeHandler>注冊(cè)到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;
}

經(jīng)過上述分析,我們對(duì)于一個(gè)<insert> statement,拿到了對(duì)應(yīng)的SqlSource,里面包含著解析后的SQL(如:insert into foo (bar, create_time) values (?, ?))以及ParameterMapping集合等信息,之所以是集合,是因?yàn)橐粋€(gè)statement里可能包含多個(gè)#{},而每一個(gè)#{}都對(duì)應(yīng)著一個(gè)ParameterMapping

接下來,我們看執(zhí)行insert方法的時(shí)候,發(fā)生了什么事情

// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());

  // 拿出啟動(dòng)過程過程構(gòu)建的ParameterMapping
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
          // ...(省略)
        value = metaObject.getValue(propertyName);
        }
        // 從parameterMapping中取出typeHandler與jdbcType
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        
        // 忽略try catch
        // 調(diào)用typeHandler的setParameter方法,完成JavaType到數(shù)據(jù)庫字段的轉(zhuǎn)化
        typeHandler.setParameter(ps, i + 1, value, jdbcType);
      }
    }
  }
}

// org.apache.ibatis.type.BaseTypeHandler#setParameter

public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
  // ...(省略)
  setNonNullParameter(ps, i, parameter, jdbcType);

}

最終,代碼走到我們自定義的BarTypeHandler,在這,我們將parameter對(duì)象 json化,并調(diào)用ps.setString方法,最終轉(zhuǎn)換成VARCHAR保存起來

public abstract class AbstractObjectTypeHandler<T> extends BaseTypeHandler<T> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JsonUtil.toJson(parameter));
    }

    // ...(省略)
}

總結(jié)

  1. 本文一開始提出在表中存儲(chǔ)json串的需求,并展示了手動(dòng)將對(duì)象與json互轉(zhuǎn)的原始方式,隨后給出了Mybatis優(yōu)雅存取json字段的解決方案 - TypeHandler
  2. 接著,從TypeHandler的注冊(cè)過程開始介紹,分析了12個(gè)register方法之間錯(cuò)綜復(fù)雜的關(guān)系,最終得出注冊(cè)過程就是構(gòu)建三個(gè)Map的過程,核心是TYPE_HANDLER_MAP,它維護(hù)著<JavaType, <JdbcType, TypeHandler>>的映射關(guān)系,在構(gòu)造ParameterMappingResultMapping時(shí)使用到
  3. 然后,詳細(xì)闡述了在應(yīng)用啟動(dòng)過程中,Mybatis如何根據(jù)Mapper.xmlTYPE_HANDLER_MAP構(gòu)造ParameterMapping
  4. 最后,簡(jiǎn)述了當(dāng)一個(gè)<insert>方法被調(diào)用時(shí),typeHandler如何工作

本文力求圍繞核心主題,緊著一條主脈落進(jìn)行講解,為避免被過多的分支干擾,省略了不少旁枝末節(jié),其中還包含一些比較重要的特性,因此下一篇,將分析typeHandler結(jié)合MappedTypes、MappedJdbcTypes注解的使用方式

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容