MyBatis 源碼分析(九):集成 Spring

mybatis-springMyBatis 的一個子項目恰力,用于幫助開發(fā)者將 MyBatis 無縫集成到 Spring 中凭疮。它允許 MyBatis 參與到 Spring 的事務(wù)管理中歧譬,創(chuàng)建映射器 mapperSqlSession 并注入到 Spring bean 中燕雁。

SqlSessionFactoryBean

MyBatis 的基礎(chǔ)用法中岖妄,是通過 SqlSessionFactoryBuilder 來創(chuàng)建 SqlSessionFactory宫仗,最終獲得執(zhí)行接口 SqlSession 的蜒犯,而在 mybatis-spring 中组橄,則使用 SqlSessionFactoryBean 來創(chuàng)建。其使用方式如下:

  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    // 設(shè)置配置文件路徑
    bean.setConfigLocation(new ClassPathResource("config/mybatis-config.xml"));
    // 別名轉(zhuǎn)化類所在的包
    bean.setTypeAliasesPackage("com.wch.domain");
    // 設(shè)置數(shù)據(jù)源
    bean.setDataSource(dataSource);
    // 設(shè)置 mapper 文件路徑
    bean.setMapperLocations(new ClassPathResource("mapper/*.xml"));
    // 獲取 SqlSessionFactory 對象
    return bean.getObject();
  }

SqlSessionFactoryBean 實現(xiàn)了 FactoryBean 接口罚随,因此可以通過其 getObject 方法獲取 SqlSessionFactory 對象玉工。

  @Override
  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }

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

  protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      // 使用已配置的全局配置對象和附加屬性
      targetConfiguration = this.configuration;
      if (targetConfiguration.getVariables() == null) {
        targetConfiguration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        targetConfiguration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      // 使用配置文件路徑加載全局配置
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
      // 新建全局配置對象
      LOGGER.debug(
        () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      targetConfiguration = new Configuration();
      Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }

    // 設(shè)置對象創(chuàng)建工廠、對象包裝工廠淘菩、虛擬文件系統(tǒng)實現(xiàn)
    Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
    Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
    Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

    // 以包的維度注冊別名轉(zhuǎn)換器
    if (hasLength(this.typeAliasesPackage)) {
      // 掃描之類包下的符合條件的類對象
      scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
        // 過濾匿名類
        .filter(clazz -> !clazz.isAnonymousClass())
        // 過濾接口
        .filter(clazz -> !clazz.isInterface())
        // 過濾成員類
        .filter(clazz -> !clazz.isMemberClass()).
        // 注冊別名轉(zhuǎn)換器
        forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
    }

    // 以類的維度注冊別名轉(zhuǎn)換器
    if (!isEmpty(this.typeAliases)) {
      Stream.of(this.typeAliases).forEach(typeAlias -> {
        // 注冊類對象到別名轉(zhuǎn)換器
        targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
        LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
      });
    }

    // 設(shè)置插件
    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    // 以包的維度注冊類型轉(zhuǎn)換器
    if (hasLength(this.typeHandlersPackage)) {
      // 掃描指定包下 TypeHandler 的子類
      scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().
        // 過濾匿名類
        filter(clazz -> !clazz.isAnonymousClass())
        // 過濾接口
        .filter(clazz -> !clazz.isInterface())
        // 過濾抽象類
        .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
        // 注冊類對象到類型轉(zhuǎn)換器
        .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    // 以類的維度注冊類型轉(zhuǎn)換器
    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        // 注冊類對象到類型轉(zhuǎn)換器
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }

    // 注冊腳本語言驅(qū)動
    if (!isEmpty(this.scriptingLanguageDrivers)) {
      Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
        targetConfiguration.getLanguageRegistry().register(languageDriver);
        LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
      });
    }
    Optional.ofNullable(this.defaultScriptingLanguageDriver)
      .ifPresent(targetConfiguration::setDefaultScriptingLanguage);

    // 配置數(shù)據(jù)庫產(chǎn)品識別轉(zhuǎn)換器
    if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
      try {
        targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

    // 設(shè)置緩存配置
    Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

    // 如果設(shè)置了配置文件路徑遵班,則解析并加載到全局配置中
    if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();
        LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    // 設(shè)置數(shù)據(jù)源環(huán)境
    targetConfiguration.setEnvironment(new Environment(this.environment,
      this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
      this.dataSource));

    // 解析 xml statement 文件
    if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }

    // 創(chuàng)建 sql 會話工廠
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }

buildSqlSessionFactory 方法會分別對配置文件、別名轉(zhuǎn)換類潮改、mapper 文件等進行解析狭郑,逐步配置全局配置對象,并最終調(diào)用 SqlSessionFactoryBuilder 創(chuàng)建 SqlSessionFactory 對象汇在。

SqlSessionTemplate

在前章分析 MyBatis 接口層時說到 SqlSessionManager 通過 JDK 動態(tài)代理為每個線程創(chuàng)建不同的 SqlSession 來解決 DefaultSqlSession 的線程不安全問題翰萨。mybatis-spring 的實現(xiàn)與 SqlSessionManager 大致相同,但是其提供了更好的方式與 Spring 事務(wù)集成糕殉。

SqlSessionTemplate 實現(xiàn)了 SqlSession 接口亩鬼,但是都是委托給成員對象 sqlSessionProxy 來實現(xiàn)的。sqlSessionProxy 在構(gòu)造方法中使用 JDK 動態(tài)代理初始化為代理類阿蝶。

  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());
  }

sqlSessionProxy 的代理邏輯如下雳锋。

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 獲取 sqlSession
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 執(zhí)行原始調(diào)用
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // 如果事務(wù)沒有交給外部事務(wù)管理器管理,進行提交
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // 異常為 PersistenceException羡洁,使用配置的 exceptionTranslator 來包裝異常
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          // 關(guān)閉 sql session
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

獲取 SqlSession

在執(zhí)行原始調(diào)用前會先根據(jù) SqlSessionUtils#getSqlSession 方法獲取 SqlSession玷过,如果通過事務(wù)同步管理器 TransactionSynchronizationManager 獲取不到 SqlSession,就會使用 SqlSessionFactory 新建一個 SqlSession,并嘗試將獲取的 SqlSession 注冊到 TransactionSynchronizationManager 中辛蚊。

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    // SqlSessionFactory 和 ExecutorType 參數(shù)不可為 null
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    // 嘗試從事務(wù)同步管理器中獲取 SqlSessionHolder
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    // 獲取 SqlSession
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    // 新建 SqlSession
    LOGGER.debug(() -> "Creating a new SqlSession");
    session = sessionFactory.openSession(executorType);

    // 將新建的 SqlSession 注冊到事務(wù)同步管理器中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

事務(wù)同步管理器

每次獲取 SqlSession 時是新建還是從事務(wù)同步管理器中獲取決于事務(wù)同步管理器是否開啟粤蝎。事務(wù)同步管理器用于維護當前線程的同步資源,如判斷當前線程是否已經(jīng)開啟了一個事務(wù)就需要查詢事務(wù)同步管理器袋马,以便后續(xù)根據(jù)事務(wù)傳播方式?jīng)Q定是新開啟一個事務(wù)或加入當前事務(wù)诽里。Spring 支持使用注解開啟事務(wù)或編程式事務(wù)。

注解開啟事務(wù)

Spring 工程中可以通過添加 EnableTransactionManagement 注解來開啟 Spring 事務(wù)管理飞蛹。EnableTransactionManagement 注解的參數(shù) mode = AdviceMode.PROXY 默認指定了加載代理事務(wù)管理器配置 ProxyTransactionManagementConfiguration,在此配置中其默認地對使用 Transactional 注解的方法進行 AOP 代理灸眼。在代理邏輯中卧檐,會調(diào)用 AbstractPlatformTransactionManager#getTransaction 方法獲取當前線程對應(yīng)的事務(wù),根據(jù)當前線程是否有活躍事務(wù)焰宣、事務(wù)傳播屬性等來配置事務(wù)霉囚。如果是新創(chuàng)建事務(wù),就會調(diào)用 TransactionSynchronizationManager#initSynchronization 方法來初始化當前線程在事務(wù)同步管理器中的資源匕积。

編程式事務(wù)

編程開啟事務(wù)的方式與注解式其實是一樣的盈罐,區(qū)別在于編程式需要手動開啟事務(wù),其最終也會為當前線程在事務(wù)同步管理器中初始化資源闪唆。

  // 手動開啟事務(wù)
  TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
  try {
    // invoke...
  } catch (Exception e) {
    transactionManager.rollback(txStatus);
    throw e;
  }
  transactionManager.commit(txStatus);

SqlSession 注冊

如果當前方法開啟了事務(wù)盅粪,那么創(chuàng)建的 SqlSession 就會被注冊到事務(wù)同步管理器中。SqlSession 會首先被包裝為 SqlSessionHolder悄蕾,其還包含了 SqlSession 對應(yīng)的執(zhí)行器類型票顾、異常處理器。

    // ...
    holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
    TransactionSynchronizationManager.bindResource(sessionFactory, holder);
    // ...

隨后 SqlSessionHolder 對象通過 TransactionSynchronizationManager#bindResource 方法綁定到事務(wù)同步管理器中帆调,其實現(xiàn)為將 SqlSessionFactorySqlSessionHolder 綁定到 ThreadLocal 中奠骄,從而完成了線程到 SqlSessionFactorySqlSession 的映射。

事務(wù)提交與回滾

如果事務(wù)是交給 Spring 事務(wù)管理器管理的番刊,那么Spring 會自動在執(zhí)行成功或異常后對當前事務(wù)進行提交或回滾含鳞。如果沒有配置 Spring 事務(wù)管理,那么將會調(diào)用 SqlSessioncommit 方法對事務(wù)進行提交芹务。

  if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    // 未被事務(wù)管理器管理蝉绷,設(shè)置提交
    sqlSession.commit(true);
  }

SqlSessionTemplate 是不允許用來顯式地提交或回滾的,其提交或回滾的方法實現(xiàn)為直接拋出 UnsupportedOperationException 異常锄禽。

關(guān)閉 SqlSession

在當前調(diào)用結(jié)束后 SqlSessionTemplate 會調(diào)動 closeSqlSession 方法來關(guān)閉 SqlSession潜必,如果事務(wù)同步管理器中存在當前線程綁定的 SqlSessionHolder,即當前調(diào)用被事務(wù)管理器管理沃但,則將 SqlSession 的持有釋放掉磁滚。如果沒被事務(wù)管理器管理,則會真實地關(guān)閉 SqlSession

  public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
    notNull(session, NO_SQL_SESSION_SPECIFIED);
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    if ((holder != null) && (holder.getSqlSession() == session)) {
      // 被事務(wù)管理器管理垂攘,釋放 SqlSession
      LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
      holder.released();
    } else {
      // 真實地關(guān)閉 SqlSesion
      LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
      session.close();
    }
  }

映射器

單個映射器

MyBatis 的基礎(chǔ)用法中维雇,MyBatis 配置文件支持使用 mappers 標簽的子元素 mapperpackage 來指定需要掃描的 mapper 接口。被掃描到的接口類將被注冊到 MapperRegistry 中晒他,通過 MapperRegistry#getMapper 方法可以獲得 Mapper 接口的代理類吱型。

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 生成 mapper 接口的代理類
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

通過代理類的方式可以使得 statementid 直接與接口方法的全限定名關(guān)聯(lián),消除了 mapper 接口實現(xiàn)類的樣板代碼陨仅。但是此種方式在每次獲取 mapper 代理類的時候都需要指定 sqlSession 對象津滞,而 mybatis-spring 中的 sqlSession 對象是 SqlSessionTemplate 代理創(chuàng)建的,為了適配代理邏輯灼伤,mybatis-spring 提供了 MapperFactoryBean 來創(chuàng)建代理類触徐。

  @Bean
  public UserMapper userMapper(SqlSessionFactory sqlSessionFactory) throws Exception {
    MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
    factoryBean.setSqlSessionFactory(sqlSessionFactory);
    return factoryBean.getObject();
  }

MapperFactoryBean 繼承了 SqlSessionDaoSupport,其會根據(jù)傳入的 SqlSessionFactory 來創(chuàng)建 SqlSessionTemplate狐赡,并使用 SqlSessionTemplate 來生成代理類撞鹉。

  @Override
  public T getObject() throws Exception {
    // 使用 SqlSessionTemplate 來創(chuàng)建代理類
    return getSqlSession().getMapper(this.mapperInterface);
  }

  public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
  }

批量映射器

每次手動獲取單個映射器的效率是低下的,MyBatis 還提供了 MapperScan 注解用于批量掃描 mapper 接口并通過 MapperFactoryBean 創(chuàng)建代理類颖侄,注冊為 Spring bean鸟雏。

@Configuration
@MapperScan("org.mybatis.spring.sample.mapper")
public class AppConfig {
  // ...
}

MapperScan 注解解析后注冊 Spring bean 的邏輯是由 MapperScannerConfigurer 實現(xiàn)的,其實現(xiàn) 了 BeanDefinitionRegistryPostProcessor 接口的 postProcessBeanDefinitionRegistry 方法览祖。

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // ...

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    // ...
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

掃描邏輯由 ClassPathMapperScanner 提供孝鹊,其繼承了 ClassPathBeanDefinitionScanner 掃描指定包下的類并注冊為 BeanDefinitionHolder 的能力。

    public int scan(String... basePackages) {
        int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

    // 掃描指定包并注冊 bean definion
        doScan(basePackages);

        // Register annotation config processors, if necessary.
        if (this.includeAnnotationConfig) {
            AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
        }

        return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
    }

  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 掃描指定包已經(jīng)獲取的 bean 定義
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
          + "' package. Please check your configuration.");
    } else {
      // 增強 bean 配置
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      // ...
      // 設(shè)置 bean class 類型為 MapperFactoryBean
      definition.setBeanClass(this.mapperFactoryBeanClass);
            // ...
    }
  }

獲取到指定 bean 的定義后展蒂,重新設(shè)置 beanClassMapperFactoryBean惶室,因此在隨后的 bean 初始化中,這些被掃描的 mapper 接口可以創(chuàng)建代理類并被注冊到 Spring 容器中玄货。

映射器注冊完成后皇钞,就可以使用引用 Spring bean 的配置來使用 mapper 接口。

小結(jié)

mybatis-spring 提供了與 Spring 集成的更高層次的封裝松捉。

  • SqlSessionFactoryBean 遵循 Spring FactoryBean 的定義夹界,使得 SqlSessionFactory 注冊在 Spring 容器中。
  • SqlSessionTemplateSqlSession 另一種線程安全版本的實現(xiàn)隘世,并且能夠更好地與 Spring 事務(wù)管理集成可柿。
  • MapperFactoryBean 是生成 mapper 接口代理類的 SqlSessionTemplate 版本實現(xiàn)。
  • MapperScannerConfigurer 簡化了生成 mapper 接口代理的邏輯丙者,指定掃描的包即可將生成 mapper 接口代理類并注冊為 Spring bean复斥。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市械媒,隨后出現(xiàn)的幾起案子目锭,更是在濱河造成了極大的恐慌评汰,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痢虹,死亡現(xiàn)場離奇詭異被去,居然都是意外死亡,警方通過查閱死者的電腦和手機奖唯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門惨缆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丰捷,你說我怎么就攤上這事坯墨。” “怎么了病往?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵畅蹂,是天一觀的道長。 經(jīng)常有香客問我荣恐,道長,這世上最難降的妖魔是什么累贤? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任叠穆,我火速辦了婚禮,結(jié)果婚禮上臼膏,老公的妹妹穿的比我還像新娘硼被。我一直安慰自己,他們只是感情好渗磅,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布嚷硫。 她就那樣靜靜地躺著,像睡著了一般始鱼。 火紅的嫁衣襯著肌膚如雪仔掸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天医清,我揣著相機與錄音起暮,去河邊找鬼。 笑死会烙,一個胖子當著我的面吹牛负懦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柏腻,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼纸厉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了五嫂?” 一聲冷哼從身側(cè)響起颗品,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抛猫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蟆盹,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年闺金,在試婚紗的時候發(fā)現(xiàn)自己被綠了逾滥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡败匹,死狀恐怖寨昙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掀亩,我是刑警寧澤舔哪,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站槽棍,受9級特大地震影響捉蚤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜炼七,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一缆巧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豌拙,春花似錦陕悬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唯绍,卻和暖如春拼岳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背况芒。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工裂问, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像敞掘,于是被迫代替她去往敵國和親犀被。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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