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

起因

在業(yè)務(wù)開(kāi)發(fā)過(guò)程中类垫,會(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è)問(wèn)題

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

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

解決方案

  1. 定義一個(gè)抽象類(lèi),繼承于org.apache.ibatis.type.BaseTypeHandler二打,用作對(duì)象類(lèi)型的換轉(zhuǎn)基類(lèi)县忌;之后但凡想varchar(longvarchar)對(duì)象互轉(zhuǎn),繼承此基類(lèi)即可
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)類(lèi)继效,繼承上述步驟1中定義的AbstractObjectTypeHandler症杏,泛型中填上要轉(zhuǎn)換的Java類(lèi)型Bar
public class BarTypeHandler extends AbstractObjectTypeHandler<Bar> {}
  1. 刪除FooString bar,并將Bar barObj 改成Bar bar瑞信,讓Foo的字段名跟數(shù)據(jù)庫(kù)字段名一一對(duì)應(yīng)
@Data
public class Foo {
    private Long id;
    private Bar bar;
    private Date createTime;
}
  1. 配置類(lèi)型處理器掃包路徑
  • 如果使用mybatis-spring-boot-starter厉颤,可以在application.properties里配置mybatis.typeHandlersPackage={BarTypeHandler所在包路徑}
  • 如果只使用mybatis-spring凡简,可以構(gòu)造一個(gè)SqlSessionFactoryBean對(duì)象逼友,并調(diào)用其setTypeHandlersPackage方法設(shè)置類(lèi)型處理器掃包路徑
  • 使用其它Mybatis擴(kuò)展組件的精肃,例如mybatis-plus,同理配置typeHandlersPackage屬性即可

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

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

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

原理分析

如果只是于使用而言,按照步驟1234走即可振定,而且4只需要走一次必怜。但是,我們顯然不能止步于此后频,知其然梳庆,知其所以然,才能用的安心卑惜,用的放心膏执,用的順手

接下來(lái)會(huì)以mybatis-spring 1.3.2mybatis 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的核心配置類(lèi)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)) { //配置的類(lèi)型處理器所在包
  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嵌施、TypeHandlerRegistryTypeAliasRegistry莽鸭、LanguageDriverRegistry吗伤,其中TypeHandlerRegistry用于TypeHandler的注冊(cè)與管理,也是本文的主角

TypeHandlerRegistry的構(gòu)造函數(shù)中硫眨,默認(rèn)注冊(cè)了幾十個(gè)類(lèi)型轉(zhuǎn)化器足淆,它們的存在,正是Mybatis非常便于使用的原因之一:幫助各種Java類(lèi)型與JdbcType互轉(zhuǎn)礁阁,比如java.util.DateJdbcType.TIMESTAMP互相轉(zhuǎn)化巧号,java.lang.StringJdbcType.VARCHARJdbcType.LONGVARCHAR互相轉(zhuǎn)化姥闭,而JdbcType默認(rèn)又與數(shù)據(jù)庫(kù)類(lèi)型有對(duì)應(yīng)關(guān)系丹鸿,為了便于理解,可以簡(jiǎn)單記為Java類(lèi)型與數(shù)據(jù)庫(kù)字段類(lèi)型的轉(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 register门怪,B register調(diào)C register,如果不擼清他們之間的關(guān)系锅纺,容易混亂:我是誰(shuí)掷空,我在哪,我在干什么

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

1個(gè)參數(shù)
  • register(String packageName)
    • 掃描packageName包下的TypeHandler類(lèi),如果非匿名內(nèi)部類(lèi)嚼锄、非接口减拭、非抽象類(lèi),就調(diào)用register(typeHandlerClass)進(jìn)行注冊(cè)
  • register(Class<?> typeHandlerClass)
    • 如果typeHandlerClass上有MappedTypes注解区丑,且注解里配置了映射的類(lèi)型拧粪,就調(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注解可霎,且注解里配置了映射的類(lèi)型,就調(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并沒(méi)有直接使用到旷余,內(nèi)部是將javaTypeClassName绢记、typeHandlerClassName分別轉(zhuǎn)成Class類(lèi)型,并調(diào)用register(javaTypeClass, typeHandlerClass)進(jìn)行注冊(cè)
  • register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler)
    • Mybatis并沒(méi)有直接使用到正卧,內(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的日期類(lèi)型處理(Since Mybatis 3.4.5)签孔,如this.register(Instant.class, InstantTypeHandler.class)。不過(guò)需要吐槽的一點(diǎn)是窘行,由于開(kāi)發(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è),而前文已說(shuō)過(guò)烛亦,與spring結(jié)合后诈泼,該文件已經(jīng)被拋棄,故不用太關(guān)注
  • register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)
    • 內(nèi)部將type強(qiáng)轉(zhuǎn)為Type類(lèi)型后煤禽,直接調(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)行解讀选脊,確保邏輯無(wú)誤杭抠,但容易讓人云里霧里,不便于理解恳啥,因此有必要在此基礎(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è)類(lèi)型保存到Map維護(hù)起來(lái)
  2. 12個(gè)register方法邢疙,目的都是為了尋找JavaType棍弄、JdbcType、TypeHandler及他們之間的關(guān)系疟游,最終維護(hù)在3個(gè)Map中:JDBC_TYPE_HANDLER_MAP呼畸、TYPE_HANDLER_MAPALL_TYPE_HANDLERS_MAP
  3. javaType颁虐、javaTypeClass 描述的是待轉(zhuǎn)換java的類(lèi)型蛮原,在例子中就是Bar.class;JdbcType是一個(gè)枚舉類(lèi)型,代表Jdbc類(lèi)型另绩,典型的取值有JdbcType.VARCHAR儒陨、JdbcType.BIGINTtypeHandler笋籽、BarTypeHandler分別代表類(lèi)型轉(zhuǎn)換器實(shí)例及其Class實(shí)例蹦漠,在例子中就是BarTypeHandler、BarTypeHandler.class
  4. MappedTypes车海、MappedJdbcTypes是兩個(gè)注解笛园,作用于TypeHandler上,用于指示侍芝、限定其所能支持的JavaType以及JdbcType

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

接著留量,回到buildSqlSessionFactory掃包處接著往下看窄赋,找到符合條件的類(lèi)型處理器并調(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是否為空來(lái)判斷使用哪個(gè)typeHandlerClass的構(gòu)造函數(shù)來(lái)構(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開(kāi)始纸淮,支持自動(dòng)發(fā)現(xiàn)mapped type的特性,這兒的mapped type指的是前文中提到的JavaType亚享。Mybatis 3.1.0新增了一個(gè)抽象類(lèi)TypeReference咽块,它是BaseTypeHandler的抽象基類(lèi),該類(lèi)只有一個(gè)能力欺税,就是使用"標(biāo)準(zhǔn)姿勢(shì)"提取泛型具體類(lèi)侈沪,即提取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,而我們沒(méi)有配置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);
  }
}

終于來(lái)到最后維護(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è)的荆责,接下來(lái)分析它是如何與mapper.xml關(guān)聯(lián)起來(lái)的

注: 由于接下來(lái)基本與mapper.xml相關(guān)滥比,如無(wú)特殊說(shuō)明亚脆,將用xml來(lái)指代mapper.xml做院,而不是mybatis-config.xml

繼續(xù)回到buildSqlSessionFactory方法,往下看濒持,mapperLocations的類(lèi)型是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)行更深層次的解析。任意打開(kāi)一個(gè)xml文件官套,在DOCTYPE聲明后緊跟著的第一行即是mapper元素酒奶,它可能長(zhǎng)<mapper namespace="com.example.demo.mapper.FooMapper" >這樣,該元素很常見(jiàn)奶赔,只是容易讓人忽視

// 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另伍、cacheresultMap绞旅、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录别、jdbcTypetypeHandler。我們可以手動(dòng)指定ParameterMappingResultMappingtypeHandler蝌数,若未明確指定,Mybatis會(huì)在應(yīng)用啟動(dòng)解析xml文件過(guò)程中嚷量,為其智能匹配上合適的值镇饮,若匹配不到,會(huì)拋出異常No typehandler found for property ...耗绿。這也暗示著一個(gè)事實(shí):MyBatis依托于無(wú)論內(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各類(lèi)屬性值狼速,比如resultTypeparameterType卦停、timeout向胡、flushCache等,為了突出重點(diǎn)惊完,把其余的省略僵芹。

SqlSouce: 代表從XML或者注解中解析出來(lái)的SQL語(yǔ)句的封裝

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

接下來(lái)以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)⒌念?lèi)型,在我們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中提取的原始未經(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());
}

來(lái)到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方法的返回值就是?遭殉,使用過(guò)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痹愚、jdbcTypetypeHandler等屬性蛔糯,構(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類(lèi)型翅溺,這種使用姿勢(shì)較少脑漫,可以跳過(guò)
  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)過(guò)兩次查找:第一次以jdbcType(VARCHAR)為key昼浦,第二次以null為key馍资。由于我們注冊(cè)的BarTypeHandler并沒(méi)有明確指定jdbcType,前文也提及到关噪,不明確指定鸟蟹,就意味著不限制,就會(huì)將<null, BarTypeHandler>注冊(cè)到jdbcHandlerMap使兔,第一次通過(guò)通過(guò)jdbcHandlerMap.get(VARCHAR)拿不到建钥,第二次通過(guò)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)過(guò)上述分析,我們對(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

接下來(lái),我們看執(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)過(guò)程過(guò)程構(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ù)庫(kù)字段的轉(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保存起來(lái)

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市龄糊,隨后出現(xiàn)的幾起案子逆粹,更是在濱河造成了極大的恐慌,老刑警劉巖炫惩,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僻弹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡他嚷,警方通過(guò)查閱死者的電腦和手機(jī)蹋绽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)筋蓖,“玉大人卸耘,你說(shuō)我怎么就攤上這事≌晨В” “怎么了蚣抗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)瓮下。 經(jīng)常有香客問(wèn)我翰铡,道長(zhǎng),這世上最難降的妖魔是什么讽坏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任锭魔,我火速辦了婚禮,結(jié)果婚禮上路呜,老公的妹妹穿的比我還像新娘迷捧。我一直安慰自己,他們只是感情好拣宰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布党涕。 她就那樣靜靜地躺著,像睡著了一般巡社。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上手趣,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天晌该,我揣著相機(jī)與錄音肥荔,去河邊找鬼。 笑死朝群,一個(gè)胖子當(dāng)著我的面吹牛燕耿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姜胖,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼誉帅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了右莱?” 一聲冷哼從身側(cè)響起蚜锨,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慢蜓,沒(méi)想到半個(gè)月后亚再,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晨抡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年氛悬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耘柱。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡如捅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出调煎,到底是詐尸還是另有隱情镜遣,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布汛蝙,位于F島的核電站烈涮,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏窖剑。R本人自食惡果不足惜坚洽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望西土。 院中可真熱鬧讶舰,春花似錦、人聲如沸需了。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肋乍。三九已至鹅颊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間墓造,已是汗流浹背堪伍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工锚烦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帝雇。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓涮俄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親尸闸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子彻亲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容