Mybatis通用Mapper應(yīng)用


通用Mapper

日期:2019-06-25

目錄:


概述

通用Mapper就是為了解決單表增刪改查,基于Mybatis的插件殖蚕。開發(fā)人員不需要編寫SQL霍转,不需要在DAO中增加方法窖梁,只要寫好實體類瘤袖,就能支持相應(yīng)的增刪改查方法挪凑。

集成(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 #領(lǐng)域?qū)ο髵呙杪窂?   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中玖喘,是否判斷字符串類型!=''澎办,少數(shù)方法會用到
   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的基礎(chǔ)和自定義接口窖逗,比如com.suixingpay.udip.common.mapper.BaseMapper等址否。

應(yīng)用案例

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. 實體字段值映射到數(shù)據(jù)庫字段,采用駝峰字段映射滑负;
  2. 主鍵字段使用@id注解在张;
  3. 非數(shù)據(jù)庫字段使用@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. 枚舉類要實現(xiàn)接口EnumType啄骇,該接口valueOfType用反射來獲取實例痴鳄;
  3. 該類主要就是對PreparedStatementResultSet設(shè)值和獲取值,從數(shù)據(jù)庫到j(luò)ava有個類型映射問題缸夹;
  4. 該類型是在SqlSessionFactoryBean類中痪寻,創(chuàng)建
    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時創(chuàng)建返回值的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. 已經(jīng)存在的基礎(chǔ)類型映射在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);
        //反射方式設(shè)置 java8 中的日期類型
        for (String time : JAVA8_DATE_TIME) {
            registerSimpleTypeSilence(time);
        }
    }    

3. Mapper使用

  • AddresMapper實現(xiàn)
@org.apache.ibatis.annotations.Mapper
public interface AddresMapper extends BaseMapper<Addres> {
}

  • 說明
  1. 繼承BaseMapper橡类,就實現(xiàn)了該類CRUD及復(fù)雜查詢相關(guān)操作;

4. 擴展自己Mapper

@RegisterMapper
public interface ResultMapper<T> {
    @SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
    List<T> selectByExample2Result(Object example);
}
  • 說明
  1. 使用注解@RegisterMapper芽唇,在創(chuàng)建SqlSessionFactory時顾画,會自動注入該類取劫;
  2. 該類不能被MapperScan掃描到,主要是因為需要獲取到范型中實體類型研侣;

4.1 實現(xiàn)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")進行自動關(guān)聯(lián)查詢;

4.2 自動關(guān)聯(lián)Mapper實現(xiàn)

@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")庶诡,實現(xiàn)自動關(guān)聯(lián)功能惦银;
  2. 可以根據(jù)需要寫復(fù)雜SQL@Select("select * from person u where u.id =#{id}")來實現(xiàn)特殊需求;

5. Weekend動態(tài)查詢使用

    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實現(xiàn)通用性的包裝,通過反射和Lambda表達式末誓,實現(xiàn)聲明式接口扯俱;
  2. 不需要寫表的字段,而是使用Lambda表達式喇澡,簡潔友好蘸吓;
  3. 生成的動態(tài)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實現(xiàn)原理

1. Mybatis架構(gòu)圖

[圖片上傳失敗...(image-11a4ec-1561497072794)]

  • 說明
  1. SqlSession 作為MyBatis工作的主要頂層API撩幽,表示和數(shù)據(jù)庫交互的會話库继,完成必要數(shù)據(jù)庫增刪改查功能
  2. Executor MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心窜醉,負責SQL語句的生成和查詢緩存的維護
  3. StatementHandler 封裝了JDBC Statement操作宪萄,負責對JDBC statement 的操作,如設(shè)置參數(shù)榨惰、將Statement結(jié)果集轉(zhuǎn)換成List集合拜英。
  4. ParameterHandler 負責對用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement 所需要的參數(shù),
  5. ResultSetHandler 負責將JDBC返回的ResultSet結(jié)果集對象轉(zhuǎn)換成List類型的集合琅催;
  6. TypeHandler 負責java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換
  7. MappedStatement MappedStatement維護了一條<select|update|delete|insert>節(jié)點的封裝居凶,
  8. SqlSource
    負責根據(jù)用戶傳遞的parameterObject揩局,動態(tài)地生成SQL語句匾寝,將信息封裝到BoundSql對象中,并返回
    BoundSql 表示動態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息 Configuration
    MyBatis所有的配置信息都維持在Configuration對象之中廉丽。

2. Mapper 原理

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

    /**
     * 根據(jù)實體中的屬性值進行查詢缠黍,查詢條件使用等號
     *
     * @param record
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    List<T> select(T record);

}
  • 該接口是使用java范型獲取到實體類弄兜,通過實體類與數(shù)據(jù)庫的映射,獲取到相關(guān)字段瓷式;也就是說SQL是從實體上動態(tài)生成替饿,而不再是讀取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)据过、結(jié)果映射颊埃、緩存配置、SqlSource蝶俱、參數(shù)對象等信息班利;
  1. Mybatis的在掃描mapper注入mapper時,會解析mapper榨呆,并根據(jù)該注解@SelectProvider罗标,會生成ProviderSqlSource類,而該類會創(chuàng)建StaticSqlSource來執(zhí)行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替換掉靜態(tài)的StaticSqlSource竿拆,而改成可以支持動態(tài)的Sql類宙拉;
    通過MappedStatement類獲取到接口和方法,并通反射調(diào)用該方法生成動態(tài)SQL丙笋;用反射替把ProviderSqlSource換成動態(tài)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()獲取并循環(huán)替換御板;
  1. Spring的情況下锥忿,以繼承的方式重寫了MapperScannerConfigurer 和
    MapperFactoryBean,在掃描配置Mapper時Spring 調(diào)用 checkDaoConfig
    的時候?qū)?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();
            //設(shè)置通用 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){
         ......
     }
    }
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末敬鬓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子笙各,更是在濱河造成了極大的恐慌钉答,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杈抢,死亡現(xiàn)場離奇詭異数尿,居然都是意外死亡,警方通過查閱死者的電腦和手機春感,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門砌创,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虏缸,“玉大人鲫懒,你說我怎么就攤上這事」粽蓿” “怎么了窥岩?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宰缤。 經(jīng)常有香客問我颂翼,道長晃洒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任朦乏,我火速辦了婚禮球及,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呻疹。我一直安慰自己吃引,他們只是感情好,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布刽锤。 她就那樣靜靜地躺著镊尺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪并思。 梳的紋絲不亂的頭發(fā)上庐氮,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音宋彼,去河邊找鬼弄砍。 笑死,一個胖子當著我的面吹牛输涕,可吹牛的內(nèi)容都是我干的输枯。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼占贫,長吁一口氣:“原來是場噩夢啊……” “哼桃熄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起型奥,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤瞳收,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后厢汹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螟深,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年烫葬,在試婚紗的時候發(fā)現(xiàn)自己被綠了界弧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡搭综,死狀恐怖垢箕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兑巾,我是刑警寧澤条获,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站蒋歌,受9級特大地震影響帅掘,放射性物質(zhì)發(fā)生泄漏委煤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一修档、第九天 我趴在偏房一處隱蔽的房頂上張望碧绞。 院中可真熱鬧,春花似錦吱窝、人聲如沸头遭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽计维。三九已至,卻和暖如春撕予,著一層夾襖步出監(jiān)牢的瞬間鲫惶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工实抡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留欠母,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓吆寨,卻偏偏與公主長得像赏淌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子啄清,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

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