Mybatis通用Mapper应用


通用Mapper

日期:2019-06-25

目录:


概述

通用Mapper就是为了解决单表增删改查,基于Mybatis的插件。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。

集成(spring-boot)

1. 引入依赖包

  • 引用通用Mapper

     <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-spring-boot-starter</artifactId>
        <version>2.1.2</version>
     </dependency>
        
    

2. 配置yml


 mybatis:
   type-aliases-package: com.suixingpay.udip.manager.core #领域对象扫描路径
   mapper-locations: classpath:mapper/*.xml
   type-handlers-package: com.suixingpay.udip.manager.core.handler #字段为枚举类型的Handler
 mapper:
   mappers:
     - com.suixingpay.udip.common.mapper.BaseMapper #mapper的父接口
   not-empty: true  #insert和update中,是否判断字符串类型!='',少数方法会用到
   identity: MYSQL
   enum-as-simple-type: true  # 允许bean 接受 enum 类型
    

3. MapperScan

  • mapper扫描路径

    @MapperScan(basePackages = "com.suixingpay.udip.manager.core")
    
  • 说明

  1. mapperScan为tk.mybatis.spring.annotation.MapperScan而不是org.mybatis.spring.annotation.MapperScan
  2. 不能扫描到mapper的基础和自定义接口,比如com.suixingpay.udip.common.mapper.BaseMapper等。

应用案例

1. 实体类

  • Person实体类

    
    @Data
    public class Person implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        private Integer age;
    
        @ColumnType(typeHandler = BaseHandler.class)
        private StatusEnum status;
    
        @ColumnType(typeHandler = BaseHandler.class)
        private AuthEnum role;
    
        private Long countryId;
    
        @Transient
        private Country country;
    
        @Transient
        private List<PersonAddress> personAddresses;
    
    }
    
    
  • 说明

  1. 实体字段值映射到数据库字段,采用驼峰字段映射;
  2. 主键字段使用@id注解;
  3. 非数据库字段使用@Transient标注;
  4. 枚举类型使用@ColumnType 注解标注;并指明Hanler处理器;

2. 字段类型处理器

  • 通用枚举类型BaseHandler
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = {StatusEnum.class, AuthEnum.class})
public class BaseHandler extends BaseTypeHandler<EnumType> {

    private Class<EnumType> types;

    public BaseHandler(Class<EnumType> types) {
        this.types = types;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, EnumType parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getValue());
    }

    @Override
    public EnumType getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int id = rs.getInt(columnName);
        if (rs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }

    @Override
    public EnumType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int id = rs.getInt(columnIndex);
        if (rs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }

   private EnumType getEnumType(int id) {
        try {
            Method valueOfType = Arrays.stream(types.getDeclaredMethods())
                    .filter(m -> m.getName().equals("valueOfType"))
                    .findFirst()
                    .orElse(null);
            return (EnumType) ReflectionUtils.invokeMethod(valueOfType, types.getEnumConstants()[0], id);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert to " + types.getName() + " by ordinal value.", ex);
        }
    }

    @Override
    public EnumType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int id = cs.getInt(columnIndex);
        if (cs.wasNull()) {
            return null;
        } else {
            return getEnumType(id);
        }
    }
}

  • 说明
  1. class使用注解@MappedJdbcTypes
    @MappedTypes,并继承BaseTypeHandler<EnumType;
  2. 枚举类要实现接口EnumType,该接口valueOfType用反射来获取实例;
  3. 该类主要就是对PreparedStatementResultSet设值和获取值,从数据库到java有个类型映射问题;
  4. 该类型是在SqlSessionFactoryBean类中,创建
    SqlSessionFactory时注册字段映射类型;
    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
     ......
        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 + "'");
            }
          }
        }
    
     ...... 
    }
    
  1. 该handler会在DefaultResultSetHandlerle类中处理ResultMap时创建返回值的java对象时使用:
public class DefaultResultSetHandler implements ResultSetHandler{
.......
  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {
        final Class<?> resultType = resultMap.getType();
        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
        final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
        if (hasTypeHandlerForResultObject(rsw, resultType)) {
            return  createPrimitiveResultObject(rsw, resultMap, columnPrefix); 
        } else if (!constructorMappings.isEmpty()) {
            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
            return objectFactory.create(resultType);
        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
            return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
        }
        throw new ExecutorException("Do not know how to create an instance of " + resultType);
    }  
.......
} 
  
  1. 已经存在的基础类型映射在SimpleTypeUtil中;
static {
        SIMPLE_TYPE_SET.add(byte[].class);
        SIMPLE_TYPE_SET.add(String.class);
        SIMPLE_TYPE_SET.add(Byte.class);
        SIMPLE_TYPE_SET.add(Short.class);
        SIMPLE_TYPE_SET.add(Character.class);
        SIMPLE_TYPE_SET.add(Integer.class);
        SIMPLE_TYPE_SET.add(Long.class);
        SIMPLE_TYPE_SET.add(Float.class);
        SIMPLE_TYPE_SET.add(Double.class);
        SIMPLE_TYPE_SET.add(Boolean.class);
        SIMPLE_TYPE_SET.add(Date.class);
        SIMPLE_TYPE_SET.add(Timestamp.class);
        SIMPLE_TYPE_SET.add(Class.class);
        SIMPLE_TYPE_SET.add(BigInteger.class);
        SIMPLE_TYPE_SET.add(BigDecimal.class);
        //反射方式设置 java8 中的日期类型
        for (String time : JAVA8_DATE_TIME) {
            registerSimpleTypeSilence(time);
        }
    }    

3. Mapper使用

  • AddresMapper实现
@org.apache.ibatis.annotations.Mapper
public interface AddresMapper extends BaseMapper<Addres> {
}

  • 说明
  1. 继承BaseMapper,就实现了该类CRUD及复杂查询相关操作;

4. 扩展自己Mapper

@RegisterMapper
public interface ResultMapper<T> {
    @SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
    List<T> selectByExample2Result(Object example);
}
  • 说明
  1. 使用注解@RegisterMapper,在创建SqlSessionFactory时,会自动注入该类;
  2. 该类不能被MapperScan扫描到,主要是因为需要获取到范型中实体类型;

4.1 实现SelectResultProvider类

public class SelectResultProvider extends MapperTemplate {
    public SelectResultProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }

    public String selectByExample2Result(MappedStatement ms) {
        Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder("SELECT ");
        if (isCheckExampleEntityClass()) {
            sql.append(SqlHelper.exampleCheck(entityClass));
        }
        sql.append("<if test=\"distinct\">distinct</if>");
        //支持查询指定列
        sql.append(SqlHelper.exampleSelectColumns(entityClass));
        sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
        sql.append(SqlHelper.exampleWhereClause());
        sql.append(SqlHelper.exampleOrderBy(entityClass));
        sql.append(SqlHelper.exampleForUpdate());
        return sql.toString();
    }
}
  • 说明
  1. 继承MapperTemplate,拼装SQL;
  2. 去掉返回类型setResultType(ms, entityClass),而是采用ResultMap("id")进行自动关联查询;

4.2 自动关联Mapper实现

@org.apache.ibatis.annotations.Mapper
public interface PersonMapper extends Mapper<Person>{

    @Select("select  * from person u where u.id = #{id}")
    @Results(id = "personResultMap",
            value = {
                    @Result(id = true, property = "id", column = "id"),
                    @Result(property = "countryId", column = "country_id"),
                    @Result(property = "country",
                            column = "country_id",
                            one = @One(select = "mybatis.example.domain.country.CountryMapper.selectByPrimaryKey", fetchType = FetchType.EAGER))
                    ,
                    @Result(property = "personAddresses",
                            column = "id",
                            many = @Many(select = "mybatis.example.domain.addres.PersonAddressMapper.selectByUserId", fetchType = FetchType.EAGER))
            }
    )
    Person getPersonById(@Param("id") Long id);


    @ResultMap("personResultMap")
    @SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
    List<Person> selectByExample2Result(Object example);


}

  • 说明
  1. 在方法selectByExample2Result上增加注解@ResultMap("personResultMap"),实现自动关联功能;
  2. 可以根据需要写复杂SQL@Select("select * from person u where u.id =#{id}")来实现特殊需求;

5. Weekend动态查询使用

    public void selectByExample2Result() {
        Weekend<Person> of = Weekend.of(Person.class);
        of.weekendCriteria()
                .andGreaterThan(Person::getAge, 1)
                .andLike(Person::getName, "%ndy%");
        List<Person> list = personMapper.selectByExample2Result(of);
        Assert.isTrue(!list.isEmpty(), "list is null");
    }
    
    public void weekendSqls() {
        Example example = Example.builder(Person.class)
                .select(FiledHelper.fieldName(Person::getId),
                        FiledHelper.fieldName(Person::getName),
                        FiledHelper.fieldName(Person::getCountryId))
                .where(WeekendSqls.<Person>custom()
                        .andLike(Person::getName, "%d%"))
                .orWhere(WeekendSqls.<Person>custom()
                        .andGreaterThan(Person::getCountryId, 1)
                        .andLessThanOrEqualTo(Person::getCountryId, 100))
                .build();
        List<Person> list = personMapper.selectByExample(example);
        Assert.isTrue(list.size() > 0, "list is null");
    }   
    
  • 说明
  1. Weekend和WeekendSqls实现通用性的包装,通过反射和Lambda表达式,实现声明式接口;
  2. 不需要写表的字段,而是使用Lambda表达式,简洁友好;
  3. 生成的动态Sql如下;
    SELECT <if test="distinct">distinct</if>
    <choose>
        <when test="@tk.mybatis.mapper.util.OGNL@hasSelectColumns(_parameter)">
            <foreach collection="_parameter.selectColumns" item="selectColumn" separator=",">${selectColumn}</foreach>
        </when>
        <otherwise>id,name,age,status,role,country_id</otherwise>
    </choose> FROM person 
    <if test="_parameter != null">
        <where>${@tk.mybatis.mapper.util.OGNL@andNotLogicDelete(_parameter)} <trim prefix="(" prefixOverrides="and |or " suffix=")">
          <foreach collection="oredCriteria" item="criteria">
            <if test="criteria.valid">
              ${@tk.mybatis.mapper.util.OGNL@andOr(criteria)}      <trim prefix="(" prefixOverrides="and |or " suffix=")">
                <foreach collection="criteria.criteria" item="criterion">
                  <choose>
                    <when test="criterion.noValue">
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
                    </when>
                    <when test="criterion.singleValue">
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value}
                    </when>
                    <when test="criterion.betweenValue">
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                    </when>
                    <when test="criterion.listValue">
                      ${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
                      <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
                        #{listItem}
                      </foreach>
                    </when>
                  </choose>
                </foreach>
              </trim>
            </if>
          </foreach>
         </trim>
        </where>
    </if>
    <if test="orderByClause != null">order by ${orderByClause}</if>
    <if test="@tk.mybatis.mapper.util.OGNL@hasForUpdate(_parameter)">FOR UPDATE</if>

通用Mapper实现原理

1. Mybatis架构图

[图片上传失败...(image-11a4ec-1561497072794)]

  • 说明
  1. SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
  2. Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  3. StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  4. ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
  5. ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  6. TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
  7. MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装,
  8. SqlSource
    负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
    BoundSql 表示动态生成的SQL语句以及相应的参数信息 Configuration
    MyBatis所有的配置信息都维持在Configuration对象之中。

2. Mapper 原理

  1. 通用Mapper提供的接口如下:
@RegisterMapper
public interface SelectMapper<T> {

    /**
     * 根据实体中的属性值进行查询,查询条件使用等号
     *
     * @param record
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    List<T> select(T record);

}
  • 该接口是使用java范型获取到实体类,通过实体类与数据库的映射,获取到相关字段;也就是说SQL是从实体上动态生成,而不再是读取xml;
    如下是通过反射获取到实体类的代码;

public abstract class MapperTemplate{
    /**
     * 获取返回值类型 - 实体类型
     */
    public Class<?> getEntityClass(MappedStatement ms) {
        String msId = ms.getId();
        if (entityClassMap.containsKey(msId)) {
            return entityClassMap.get(msId);
        } else {
            Class<?> mapperClass = getMapperClass(msId);
            Type[] types = mapperClass.getGenericInterfaces();
            for (Type type : types) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType t = (ParameterizedType) type;
                    if (t.getRawType() == this.mapperClass || this.mapperClass.isAssignableFrom((Class<?>) t.getRawType())) {
                        Class<?> returnType = (Class<?>) t.getActualTypeArguments()[0];
                        //获取该类型后,第一次对该类型进行初始化
                        EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());
                        entityClassMap.put(msId, returnType);
                        return returnType;
                    }
                }
            }
        }
        throw new MapperException("无法获取 " + msId + " 方法的泛型信息!");
    }
    
}
  1. Mybatis中每个方法都会包装成MappedStatement实例,这个对象是对jdbc的statement包装;
    这个对象包含id(namespace+id)、结果映射、缓存配置、SqlSource、参数对象等信息;
  1. Mybatis的在扫描mapper注入mapper时,会解析mapper,并根据该注解@SelectProvider,会生成ProviderSqlSource类,而该类会创建StaticSqlSource来执行SQL;
public class MapperRegistry{
   public <T> void addMapper(Class<T> type) {
       if (type.isInterface()) {
         if (hasMapper(type)) {
           throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
         }
         boolean loadCompleted = false;
         try {
           knownMappers.put(type, new MapperProxyFactory<T>(type));
           MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
           parser.parse();
           loadCompleted = true;
         } finally {
           if (!loadCompleted) {
             knownMappers.remove(type);
           }
         }
       }
     }
}
     
  • 在MapperAnnotationBuilder类中生成ProviderSqlSource;
public class MapperAnnotationBuilder{ 
    void parseStatement(Method method) { 
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method); SqlSource
        sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass,languageDriver);
     ...... 
    }
    
    private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
          Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
          Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
          if (sqlAnnotationType != null) {
            if (sqlProviderAnnotationType != null) {
              throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
            }
            Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
            final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
            return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
          } else if (sqlProviderAnnotationType != null) {
            Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
            
            //FIXME 生成ProviderSqlSource类;
            return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
          }
          return null;
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
      }
} 
  1. 通用Mapper就是通过ProviderSqlSource生成MappedStatement替换掉静态的StaticSqlSource,而改成可以支持动态的Sql类;
    通过MappedStatement类获取到接口和方法,并通反射调用该方法生成动态SQL;用反射替把ProviderSqlSource换成动态Sql;
    代码如下:
    public SqlSource createSqlSource(MappedStatement ms, String xmlSql) {
        return languageDriver.createSqlSource(ms.getConfiguration(), "<script>\n\t" + xmlSql + "</script>", null);
    }
    
    protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) {
        MetaObject msObject = MetaObjectUtil.forObject(ms);
        msObject.setValue("sqlSource", sqlSource);
    }    
    
  1. 通用Mapper何时替换ProviderSqlSource

    1. 初始化
      SqlSessionFactory时注册mapper后,通过
      configuration.getMappedStatements()获取并循环替换;
  1. Spring的情况下,以继承的方式重写了MapperScannerConfigurer 和
    MapperFactoryBean,在扫描配置Mapper时Spring 调用 checkDaoConfig
    的时候对 SqlSource 进行替换。

    public class MapperScannerConfigurer{
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
            if (this.processPropertyPlaceHolders) {
                processPropertyPlaceHolders();
            }
            ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
            scanner.setAddToConfig(this.addToConfig);
            scanner.setAnnotationClass(this.annotationClass);
            scanner.setMarkerInterface(this.markerInterface);
            scanner.setSqlSessionFactory(this.sqlSessionFactory);
            scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
            scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
            scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
            scanner.setResourceLoader(this.applicationContext);
            scanner.setBeanNameGenerator(this.nameGenerator);
            scanner.registerFilters();
            //设置通用 Mapper
            scanner.setMapperHelper(this.mapperHelper);
            scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
        }
    }
    
    public class ClassPathMapperScanner{
     private MapperFactoryBean<?> mapperFactoryBean = new MapperFactoryBean<Object>();
     private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions){
         ......
     }
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,326评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,228评论 1 304
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,979评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,489评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,894评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,900评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,075评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,803评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,565评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,778评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,255评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,582评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,254评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,151评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,952评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,035评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,839评论 2 277

推荐阅读更多精彩内容