MyBatis源码解析 - 类型转换模块

MyBatis源码解析 - 类型转换模块

前言

JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换成JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换成Java类型。MyBatis 使用类型处理器完成上述两种转换,如图所示。

在MyBatis中使用JdbeType这个枚举类型代表JIDBC中的数据类型,该枚举类型中定义了TYPECODE字段,记录了JDBC类型在javasql.Types中相应的常量编码,并通过一个静态集合codelookup(HashMap<nteger,JdbeTypec>类型)维护了常量编码与JdbcType之间的对应关系。

TypeHandler

TypeHandler是类型处理器接口,MyBatis中所有的类型转换器都继承了TypeHandler接口,在TypeHandler接口中定义了如下四个方法,这四个方法分为两类:setParameter()方法负责将数据由JdbeType类型转换成Java类型;getResult()方法及其重载负责将数据由Java类型转换成JdbcType类型。

public interface TypeHandler<T> {

  //在通过PreparedStatement为SQL语句绑定参数时,会将数据由JdbcType类型转换成Java类型
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  //从 ResultSet 中获取数据时会调用此方法,会将数据由Java类型转换成JdbcType类型
  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

为方便用户自定义TypeHandler实现,MyBatis 提供了BaseTypeHandler 这个抽象类,它实现了TypeHandler接口,并继承了TypeReference抽象类,其继承结构如图所示。

<img src="http://qiniu-cdn.janker.top/oneblog/20200117165004531.jpg" style="zoom:33%;" />

BaseTypeHandler中实现TypeHandler.setParameter()方法和TypeHandler.getResult()方法, 具体实现如下所示。需要注意的是,这两个方法对于非空数据的处理都交给了子类实现。

public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
  if (parameter == null) {
    if (jdbcType == null) {
      //参数 类型 异常
      throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
    }
    try {
      //绑定参数为null的处理
      ps.setNull(i, jdbcType.TYPE_CODE);
    } catch (SQLException e) {
      throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
            + "Cause: " + e, e);
    }
  } else {
    try {
      //绑定非空参数 该方法为抽象方法 由子类实现
      setNonNullParameter(ps, i, parameter, jdbcType);
    } catch (Exception e) {
      throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different configuration property. "
            + "Cause: " + e, e);
    }
  }
}

public T getResult(ResultSet rs, String columnName) throws SQLException {
  try {
    //抽象方法 由多个重载 由子类实现
    return getNullableResult(rs, columnName);
  } catch (Exception e) {
    throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
  }
}

BaseTypeHandler的实现类比较多,如图所示,但大多是直接调用PreparedStatementResultSetCallableStatement的对应方法,实现比较简单。

<img src="/Users/janker/Library/Application Support/typora-user-images/image-20200118165213276.png" style="zoom:50%;" />

这里以IntegerTypeHandler为例简单介绍,其他的实现类请读者参考相关源码:

public class IntegerTypeHandler extends BaseTypeHandler<Integer> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    //调用PreparedStatement.setInt()实现参数绑定
    ps.setInt(i, parameter);
  }

  @Override
  public Integer getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    //调用PreparedStatement.getInt()获取指定列值
    int result = rs.getInt(columnName);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Integer getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    //调用ResultSet.getInt()获取指定列值
    int result = rs.getInt(columnIndex);
    return result == 0 && rs.wasNull() ? null : result;
  }

  @Override
  public Integer getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    //调用CallableStatement.getInt()获取指定列值
    int result = cs.getInt(columnIndex);
    return result == 0 && cs.wasNull() ? null : result;
  }
}

一般情况下, TypeHandler用于完成单个参数以及单个列值的类型转换,如果存在多列值转换成一个Java对象的需求,应该优先考虑使用在映射文件中定义合适的映射规则(<resultMap>节点)完成映射。

TypeHandlerRegistry

介绍完TypeHandler接口及其功能后,我们来看一下MyBatis如何管理众多的TypeHandler接口实现,如何知道何时使用哪个TypeHandler接口完成转换。这些都是由TypehandlerRegistry完成的,在MyBatis初始化过程中,会为所有已知的TypeHandler创建对象,并实现注册到TypeHandlerRegistry中,由TypeHandlerRegistry负责挂你这些TypeHandler对象。

TypeHandlerRegistry的核心字段的含义如下:

//记录JdbcType与TypeHandler之间的对应关系,其中JdbcType是一个枚举类型,它定义对应的JDBC类型
//该集合主要用于从结果集读取数据时,将数据从Jdbc类型转换成Java类型
private final Map<JdbcType, TypeHandler<?>>  jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
//记录了Java类型向指定JdbcType转换时,需要使用的TypeHandler对象。例如: Java 类型中的String可能
//转换成数据库的char、varchar等多种类型,所以存在一对多关系
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
//记录了全部TypeHandler类型以及该类型相应的TypeHandler对象
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

//空TypeHandler集合的标识
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

1. 注册TypeHandler对象

TypeHandlerRegistry.register()方法实现了注册TypeHandler对象的功能,该注册过程会向上述四个集合中添加TypeHandler对象。register()方法有多个重载,这些重载之间的调用关系如图所示。

<img src="http://qiniu-cdn.janker.top/oneblog/20200119114322840.jpg" style="zoom:33%;" />

从图中可以看出,多数register()方法最终会调用重载④完成注册功能,我们先来介绍该方法的实现,其三个参数分别指定了TypeHandler 能够处理的Java 类型、Jdbc 类型以及TypeHandler对象。

//register()方法的重载④的实现如下:
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
  if (javaType != null) { //检验是否明确指定了TypeHandler能够处理的Java类型
    //获取指定Java类型在TYPE_ HANDLER_ MAP集合中对应的TypeHandler集合
    Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
    if (map == null || map == NULL_TYPE_HANDLER_MAP) {
      //创建新的TypeHandler集合,并添加到TYPE HANDLER MAP中
      map = new HashMap<>();
      typeHandlerMap.put(javaType, map);
    }
    //将TypeHandler对象注册到typeHandlerMap集合中
    map.put(jdbcType, handler);
  }
  //向AllTypeHandler集合注册TypeHandler类型和对应的TypeHandler对象【以Class为key handler对象为value】
  allTypeHandlersMap.put(handler.getClass(), handler);
}

在①~③这个三个registr()方法重载中会尝试读取TypeHandler类中定义的@MappedTypes注解和@MappedJdbcTypes注解,@MappedTypes 注解用于指明该TypeHandler实现类能够处理的Java类型的集合,@MappedJdbcTypes注解用于指明该TypeHandler实现类能够处理的JDBC类型集合。register()方法的重载①~③的具体实现如下:

//register()方法的重载①的实现如下:
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
//获取 @MappedTypes 注解
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
  for (Class<?> javaTypeClass : mappedTypes.value()) {
    //经过强制类型转换 以及反射创建TypeHandler对象之后,交由重载③继续处理
    register(javaTypeClass, typeHandlerClass);
    mappedTypeFound = true;
  }
}
if (!mappedTypeFound) {
  //未@MappedTypes 注解,调用重载方法 ② 处理
  register(getInstance(null, typeHandlerClass));
}
}
//register()方法的重载②的实现如下:
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
//获取@MappedTypes注解 并根据MappedTypes注解指定的java类型进行注册,逻辑与重载①类似
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
  for (Class<?> handledType : mappedTypes.value()) {
    //交由重载③进行处理
    register(handledType, typeHandler);
    mappedTypeFound = true;
  }
}
//从3.1.0版本开始,可以根据TypeHandler类型自动查找对应的Java类型,这需要
//我们的TypeHandler实现类同时继承TypeReference这个抽象类
// @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);
}
}
//register()方法的重载③的实现如下:
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
//获取MappedJdbcTypes注解
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
  //根据指定类型进行注册 读取@MappedJdbcTypes value值 获取handlerJdbcType
  for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
    register(javaType, handledJdbcType, typeHandler);
  }
  if (mappedJdbcTypes.includeNullJdbcType()) {
    register(javaType, null, typeHandler);
  }
} else {
  //未指定MappedJdbcTypes注解 交由重载④继续处理
  register(javaType, null, typeHandler);
}
}

上述全部的register()方法重 载都是在向TYPE_ HANDLER_ _MAP集合和ALL_TYPE_HANDLERS MAP集合注册TypeHandler 对象,而重载⑤是向JDBC_ TYPE_ HANDLER_ MAP集合注册TypeHandler对象,其具体实现如下:

//register()方法重载⑤的实现如下:
public void register(JdbcType jdbcType, TypeHandler<?> handler) {
// 注册JdbcType类型对一个的TypeHandler
jdbcTypeHandlerMap.put(jdbcType, handler);
}

TypeHandlerRegistry除了提供注册单个TypeHandlerregister()重载,还可以扫描整个包下的TypeHandler接口实现类,并将完成这些TypeHandler实现类的注册。下面来看重载⑥的具体实现:

//register()方法重载⑥的实现,主要用来自动扫描指定包下的TypeHandler实现并完成注册
public void register(String packageName) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  //查找指定包下的TypeHandler接口实现类
  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);
    }
  }
}

最后来看TypeHandlerRegistry构造方法,会通过上述register()方法为很多基础类型注册对应的TypeHandler对象,简略代码如下:

public TypeHandlerRegistry(Configuration configuration) {
    //...这里重点看下String相关的TypeHandler对象的注册 其他类型的TypeHandler的注册类型(略)
    // stringTypeHandler 能够将数据从String 类型转换成null ( JdbcType ),所以向
    TYPE HANDLER MAP
    //集合注册该对象,并向ALL_ TYPE_ HANDLERS_ MAP集合注册StringTypeHandler
    register(String.class, new StringTypeHandler());
    
    register(JdbcType.NVARCHAR, new NStringTypeHandler());
    //向JDBC_TYPE_HANDLER_MAP集合注册NVARCHAR对应的NStringTypeHandler
  register(JdbcType.NVARCHAR, new NStringTypeHandler());
  // ...注册其他JdbcType类型对应的TypeHandler的过程类似(略)
}

2. 查找TypeHandler对象

介绍完注册TypeHandler对象的功能之后,再来介绍TypeHandlerRegistry提供的查找TypeHandler对象的功能。TypeHandlerRegistry.getTypeHandler()方法实现了从上述四个集合中获取对应TypeHandler对象的功能。TypeHandlerRegistry.getTypeHandler()方法有多个重载,这些重载之间的关系如图所示。

<img src="http://qiniu-cdn.janker.top/oneblog/20200119134802598.png" style="zoom:40%;" />

经过一系列类型转 换之后,TypeHandlerRegistry.getTypeHandler()方法的多个重载都会调用TypeHandlerRegistry.getTypeHandle(Type,JdbcType)这个重载方法,它会根据指定的Java类型和JdbcType类型查找相应的TypeHandler对象,具体实现如下:

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  if (ParamMap.class.equals(type)) {
    return null;
  }
  //查找或初始化Java类型对一个的TypeHandler对象
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  TypeHandler<?> handler = null;
  if (jdbcHandlerMap != null) {
    //根据JdbcType查找TypeHandler
    handler = jdbcHandlerMap.get(jdbcType);
    if (handler == null) {
      handler = jdbcHandlerMap.get(null);
    }
    if (handler == null) {
      // #591
      //如果JdbcType只注册了一个TypeHandler,使用此TypeHandler对象
      handler = pickSoleHandler(jdbcHandlerMap);
    }
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}

TypeHandlerRegitry.getJdbcHandlerMap()方法中,会检测TYPE_HANDLER_MAP集合中指定Java类型对应的TypeHandler集合是否已经初始化。如果未初始化,则尝试以该Java类型的、已初始化的父类对应的TypeHandler集合为初始集合;如不存在己初始化的父类,则将其对应的TypeHandler集合初始化为NULL_TYPE_HANDLER_MAP标识。getJdbcHandlerMap()方法具体实现如下:

private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
    //查找指定Java类型对应的TypeHandler集合
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type);
    if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) { //检测是否为空集合标识
      return null;
    }
    //初始化指定的Java类型的TypeHandler集合
    if (jdbcHandlerMap == null && type instanceof Class) {
      Class<?> clazz = (Class<?>) type;
      if (Enum.class.isAssignableFrom(clazz)) {
        Class<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz;
        jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass);
        if (jdbcHandlerMap == null) {
          //枚举类型的处理
          register(enumClass, getInstance(enumClass, defaultEnumTypeHandler));
          return typeHandlerMap.get(enumClass);
        }
      } else {
        //查找父类对应的TypeHandler集合,并作为初始化集合
        jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
      }
    }
    // 以NULL_TYPE_HANDLER_MAP作为TypeHandler集合
    typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
    return jdbcHandlerMap;
  }
  
  // getJdbcHandlerMapForSuperclass()实现
  private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForSuperclass(Class<?> clazz) {
    Class<?> superclass =  clazz.getSuperclass();
    if (superclass == null || Object.class.equals(superclass)) {
      return null;  //父类为Object或null则查找结束
    }
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(superclass);
    if (jdbcHandlerMap != null) {
      return jdbcHandlerMap;
    } else {
      //继续递归查找父类对应的TypeHandler集合
      return getJdbcHandlerMapForSuperclass(superclass);
    }
  }

TypeHandlerRegistry.getMappingTypeHandler()方法会根据指定的TypeHandler类型,直接从ALL_TYPE_HANDLERS_MAP集合中查找TypeHandler对象。TypeHandlerRegistry.getTypeHandler(JdbcType)方法会根据指定的JdbcType类型,从JDBC_TYPE_HANDLER_MAP集合中查找TypeHandler对象。这两个方法实现相对简单,代码就不贴出来了。
最后,除了MyBatis本身提供的TypeHandler实现,我们也可以添加自定义的TypeHandler
接口实现,添加方式是在mybatisconfig.xml配置文件中的<typeHandlers>节点下,添加相应的<typeHandler>节点配置,并指定自定义的TypeHandler接口实现类。在MyBatis初始化时会解析该节点,并将该TypeHandler类型的对象注册到TypeHandlerRegistry中,供MyBatis后续使用。在后面介绍MyBatis初始化时还会提到该配置。

TypeAliasRegistry

在编写SQL语句时,使用别名可以方便理解以及维护,例如表名或列名很长时,我们一般会为其设计易懂易维护的别名。MyBatis将SQL语句中别名的概念进行了延伸和扩展,MyBatis可以为一个类添加一一个别名,之后就可以通过别名引用该类。MyBatis通过TypeAliasRegistry类完成别名注册和管理的功能,TypeAliasRegistry的结构比较简单,它通过TYPE_ ALIASES字段(Map<String, Class<?>>类型)管理别名与Java类型之间的对应关系,通过TypeAliasRegistry.registerAlias()方法完成注册别名,该方法的具体实现如下:

public void registerAlias(String alias, Class<?> value) {
  if (alias == null) {  //校验别名是否为空,为空则抛出类型异常
    throw new TypeException("The parameter alias cannot be null");
  }
  // issue #748
  //将别名转换为小写
  String key = alias.toLowerCase(Locale.ENGLISH);
  //校验别名是否已经存在
  if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
    throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
  }
  //注册别名
  typeAliases.put(key, value);
}

TypeAliasRegistry的构造方法中,默认为Java的基本类型及其数组类型、基本类型的封装类及其数组类型、DateBigDecimalBigIntegerMapHashMapListArrayListCollectionIteratorResultSet等类型添加了别名,代码比较简单,请读者参考TypeAliasRegistry的源码进行学习。

TypeAliasRegistry中还有两个方法需要介绍一下,registerAliases(String,Class<?>)重载会扫描指定包下所有的类,并为指定类的子类添加别名; registerAlias(Class<?>)重 载中会尝试读取@Alias注解。这两个方法的实现如下:

public void registerAliases(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  //查找指定包下的superType类型类
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
  for (Class<?> type : typeSet) {
    // Ignore inner classes and interfaces (including package-info.java)
    // Skip also inner classes. See issue #6
    //过滤调内部类、接口、抽象类
    if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
      registerAlias(type);
    }
  }
}

public void registerAlias(Class<?> type) {
  String alias = type.getSimpleName();  //类型的简单名称(不包含报名)
  //读取Alias注解
  Alias aliasAnnotation = type.getAnnotation(Alias.class);
  if (aliasAnnotation != null) {
    alias = aliasAnnotation.value();
  }
  //检测次别名不存在后,会将其记录到TYPE_ALIASES集合中
  registerAlias(alias, type);
}

本文由 Janker 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
<img src="http://qiniu-cdn.janker.top/oneblog/20200311150833864.jpg" style="zoom:50%;" />

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容