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<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
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
  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<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");

  this.sqlSessionFactory = buildSqlSessionFactory();
}

而在buildSqlSessionFactory方法中,構造了Mybatis的核心配置類Configuration,並且進行了初始化。當Mybatis不結合Spring使用時,就需要自己構造Configuration對象,這個對應於mybatis-config.xml配置文件,具體使用規則可以參考官網 。當然,mybatis-spring幫我們搞定了配置Configuration的事,同時也拋棄了mybatis-config.xml原始的配置文件

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

Configuration configuration;

// ...(省略)

  configuration = new Configuration();

// ...(省略)

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

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

Configuration還中持有非常多的對象,比如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 register調C 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<T>) null, typeHandler)進行註冊
2個參數
  • register(String javaTypeClassName, String typeHandlerClassName)
    • Mybatis並沒有直接使用到,內部是將javaTypeClassNametypeHandlerClassName分別轉成Class類型,並調用register(javaTypeClass, typeHandlerClass)進行註冊
  • register(TypeReference javaTypeReference, TypeHandler<? extends T> handler)
    • Mybatis並沒有直接使用到,內部是從javaTypeReference獲取到rawType之後,調用register(javaType, typeHandler)進行註冊
  • register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
    • 調用getInstance生成TypeHandler實例後,調用register(javaTypeClass, typeHandler)進行註冊
    • 該方法在TypeHandlerRegistry構造函數中被大量調用,主要用於支持JSR310的日期類型處理(Since Mybatis 3.4.5),如this.register(Instant.class, InstantTypeHandler.class)。不過需要吐槽的一點是,由於開發者與之前不同,因此註冊的風格與之前不同,調用的API也不同,增加了學習成本
  • register(Type javaType, TypeHandler<? extends T> typeHandler)
    • 如果typeHandler的Class上有MappedJdbcTypes註解
      • 註解裏配置了JdbcType,調用register(javaType, handledJdbcType, typeHandler)進行註冊
      • 否則,若includeNullJdbcType = true,調用register(javaType, null, typeHandler)進行註冊
    • 否則,調用register(javaType, null, typeHandler)進行註冊
  • register(Class javaType, TypeHandler<? extends T> typeHandler)
    • 內部調用register(javaType, typeHandler)
    • 該方法在TypeHandlerRegistry構造函數中被大量調用,如register(Date.class, new DateTypeHandler())
  • register(JdbcType jdbcType, TypeHandler<?> handler)
    • <JdbcType, TypeHandler>的映射關係保存到JDBC_TYPE_HANDLER_MAP
    • 該方法在TypeHandlerRegistry構造函數中被大量調用,如register(JdbcType.INTEGER, new IntegerTypeHandler())
3個參數
  • register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
    • 調用getInstance生成TypeHandler實例後,調用register(javaTypeClass, jdbcType, typeHandler)進行註冊
    • 很少用到,只有在Mybatis解析``mybatis-config.xmltypeHandlers`元素時,可能會調用該方法進行註冊,而前文已說過,與spring結合後,該文件已經被拋棄,故不用太關注
  • register(Class type, JdbcType jdbcType, TypeHandler<? extends T> handler)
    • 內部將type強轉爲Type類型後,直接調用register((Type) javaType, jdbcType, handler)
  • register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
    • javaType非空,將<JavaType, <JdbcType, TypeHandler>>的映射關係保存到TYPE_HANDLER_MAP中,從中可以看出,對於一個javaType,可能存在多個typeHandler,用於跟不同的jdbcType進行轉換
    • <TypeHandlerClass, TypeHandler>的映射關係保存到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<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
  Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
  for (Class<?> type : handlerSet) {
    //Ignore inner classes and interfaces (including package-info.java) and abstract classes
    if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
      register(type);
    }
  }
}

邏輯會走到下邊部分,根據(null, typeHandlerClass)獲取TypeHandler實例,方法第一個入參爲javaTypeClass,而此處並不知道javaTypeClass是什麼,因此傳入的值null,而獲取實例的方法也很簡單,根據javaTypeClass是否爲空來判斷使用哪個typeHandlerClass的構造函數來構造例實。獲取實例之後調用register(typeHandler)

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


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

同樣忽略註解部分。從2012年發佈Mybatis 3.1.0開始,支持自動發現mapped type的特性,這兒的mapped type指的是前文中提到的JavaTypeMybatis 3.1.0新增了一個抽象類TypeReference,它是BaseTypeHandler的抽象基類,該類只有一個能力,就是使用"標準姿勢"提取泛型具體類,即提取JavaType,比如public class BarTypeHandler extends AbstractObjectTypeHandler<Bar>,提取的就是Bar.class

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

  private final Type rawType;

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

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

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

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

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

調用register(javaType, null, typeHandler),該方法第二個參數是JdbcType,而我們沒有配置MappedJdbcTypes註解,因此爲null,代表的是對JdbcType不做限制

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

終於來到最後維護Map的方法,根據源碼,很容易看出主要是維護ALL_TYPE_HANDLERS_MAP<typeHandlerClass, typeHandler>TYPE_HANDLER_MAP<javaType, jdbcType,typeHandler>

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

上面分析typeHandler是如何註冊的,接下來分析它是如何與mapper.xml關聯起來的

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

繼續回到buildSqlSessionFactory方法,往下看,mapperLocations的類型是Resource[],代表xml資源集合,遍歷每一個文件,並進行解析

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

使用XPath讀取mapper元素的值,並將結果傳入configurationElement進行更深層次的解析。任意打開一個xml文件,在DOCTYPE聲明後緊跟着的第一行即是mapper元素,它可能長<mapper namespace="com.example.demo.mapper.FooMapper" >這樣,該元素很常見,只是容易讓人忽視

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

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    // 解配`xml`文件中 mapper元素
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }
  // ...(省略)
}

configurationElement方法,主要是解析xml本身的所有元素,如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中#{}的封裝,如<insert>中的#{bar,jdbcType=VARCHAR}

public class ParameterMapping {

  private Configuration configuration;

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

  // ...(省略)
}

ResultMapping: 結果集的映射關係,是對xml<resultMap>中子元素的封裝,如<result column="bar" property="bar" jdbcType="VARCHAR" />

public class ResultMapping {

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

  // ...(省略)
}

二者有3個同名參數需要我們重點關注: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<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.

public void parseStatementNode() {
  // ...(省略)
  String parameterType = context.getStringAttribute("parameterType");
  // ...(省略)
  
  // Parse selectKey after includes and remove them.
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
}

接下來以insert方法爲例,方法簽名是int insert(Foo record);,對應的insert statement是

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

接着調用到langDriver.createSqlSource

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

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

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

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource = null;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
  	// 走這兒,parameterType代表入參的類型,在我們case中代表Foo.class
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

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

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

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

來到org.apache.ibatis.parsing.GenericTokenParser#parse,該方法根據傳入的原始sql,解析裏邊#{}所代表的內容,在我們的case中,結果是bar,jdbcType=VARCHAR,將結果保存在expression變量中,調用ParameterMappingTokenHandler#handleToken進行處理。每一個#{}代表了原始SQL中的?,因此handleToken方法的返回值就是?,使用過JDBC編程的同學應該也明白?代表的含義---->從此處我們也證實了,#{}的方式屏蔽了SQL注入的風險,與原生JDBC編程中使用?的預防SQL注入的方式是一樣的

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

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

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

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

buildParameterMapping方法根據傳入的expression,解析出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<String, String> entry : propertiesMap.entrySet()) {
    String name = entry.getKey();
    String value = entry.getValue();
    if ("javaType".equals(name)) {
      javaType = resolveClass(value);
      builder.javaType(javaType);
    } else if ("jdbcType".equals(name)) {
      builder.jdbcType(resolveJdbcType(value));
    } else if ("mode".equals(name)) {
      builder.mode(resolveParameterMode(value));
    } else if ("numericScale".equals(name)) {
      builder.numericScale(Integer.valueOf(value));
    } else if ("resultMap".equals(name)) {
      builder.resultMapId(value);
    } else if ("typeHandler".equals(name)) {
      typeHandlerAlias = value;
    } else if // ...(省略)
  }
  return builder.build();
}

build方法做了兩件事,一是再次解析typeHandler,二是校驗typeHandler是否爲空,如果爲空,則拋出異常。爲什麼需要再次解析?是因爲有可能在#{}中未明確指定使用哪個typeHandler,即parameterMapping.typeHandler == null,這時候Mybatis會智能去匹配,當然,有時候也不是那麼智能,匹配的結果跟我們預期的不太一樣,這時候手動指定會更合適

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

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

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

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

在我們的case中,並未明確指定typeHandler,因此resolveTypeHandler中,滿足parameterMapping.typeHandler == null的條件,調用typeHandlerRegistry.getTypeHandler方法進行智能匹配

先根據javaType調用getJdbcHandlerMap方法拿到jdbcHandlerMap,而
getJdbcHandlerMap其實只是根據javaTypeTYPE_HANDLER_MAP取,從前文中我們知道,TYPE_HANDLER_MAP中存在這麼一條entry <Bar.class, <null, BarTypeHandler>>,因此jdbcHandlerMap<null, BarTypeHandler>

再根據jdbcTypejdbcHandlerMap中找typeHandler。此處經過兩次查找:第一次以jdbcType(VARCHAR)爲key,第二次以null爲key。由於我們註冊的BarTypeHandler並沒有明確指定jdbcType,前文也提及到,不明確指定,就意味着不限制,就會將<null, BarTypeHandler>註冊到jdbcHandlerMap,第一次通過通過jdbcHandlerMap.get(VARCHAR)拿不到,第二次通過jdbcHandlerMap.get(null)就拿到了不受jdbcType限制的BarTypeHandler

// org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler

public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
  return getTypeHandler((Type) type, jdbcType);
}

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  if (ParamMap.class.equals(type)) {
    return null;
  }
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  TypeHandler<?> handler = null;
  if (jdbcHandlerMap != null) {
    handler = jdbcHandlerMap.get(jdbcType);
    if (handler == null) {
      handler = jdbcHandlerMap.get(null);
    }
    if (handler == null) {
      // #591
      handler = pickSoleHandler(jdbcHandlerMap);
    }
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}


private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
  if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
    return null;
  }
  if (jdbcHandlerMap == null && type instanceof Class) {
    Class<?> clazz = (Class<?>) type;
    if (clazz.isEnum()) {
      jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
      if (jdbcHandlerMap == null) {
        register(clazz, getInstance(clazz, defaultEnumTypeHandler));
        return TYPE_HANDLER_MAP.get(clazz);
      }
    } else {
      jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
    }
  }
  TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
  return jdbcHandlerMap;
}

經過上述分析,我們對於一個<insert> statement,拿到了對應的SqlSource,裏面包含着解析後的SQL(如:insert into foo (bar, create_time) values (?, ?))以及ParameterMapping集合等信息,之所以是集合,是因爲一個statement裏可能包含多個#{},而每一個#{}都對應着一個ParameterMapping

接下來,我們看執行insert方法的時候,發生了什麼事情

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

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

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

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

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

}

最終,代碼走到我們自定義的BarTypeHandler,在這,我們將parameter對象 json化,並調用ps.setString方法,最終轉換成VARCHAR保存起來

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

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

    // ...(省略)
}

總結

  1. 本文一開始提出在表中存儲json串的需求,並展示了手動將對象與json互轉的原始方式,隨後給出了Mybatis優雅存取json字段的解決方案 - TypeHandler
  2. 接着,從TypeHandler的註冊過程開始介紹,分析了12個register方法之間錯綜複雜的關係,最終得出註冊過程就是構建三個Map的過程,核心是TYPE_HANDLER_MAP,它維護着<JavaType, <JdbcType, TypeHandler>>的映射關係,在構造ParameterMappingResultMapping時使用到
  3. 然後,詳細闡述了在應用啓動過程中,Mybatis如何根據Mapper.xmlTYPE_HANDLER_MAP構造ParameterMapping
  4. 最後,簡述了當一個<insert>方法被調用時,typeHandler如何工作

本文力求圍繞核心主題,緊着一條主脈落進行講解,爲避免被過多的分支幹擾,省略了不少旁枝末節,其中還包含一些比較重要的特性,因此下一篇,將分析typeHandler結合MappedTypesMappedJdbcTypes註解的使用方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章