Spring MyBatis 事務分析 -- MyBatis 整合過程

上一篇我們分析了 Spring 的 Transaction 處理過程臭胜,這里我們一起看下拙友,MyBatis 是如何跟 Spring 整合剑勾,提供完整的事務處理方案床估。

想要把 MyBatis 跟 Spring 整合弯囊,都需要這樣一個 Jar 包:mybatis-spring-x.x.x.jar,這個 Jar 包可以說是 MyBatis 與 Spring 的通信橋梁胶果,將兩個不相關(guān)的框架可以整合到一起匾嘱,提供完整的 ORM 功能。

在 Spring 配置文件中需要配置如下兩個 Bean:

<!-- mybatis配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dynamicDataSource" />
    <property name="configLocation" value="classpath:mybatis.xml"></property>
    <!-- mybatis配置文件 -->
    <property name="mapperLocations" value="classpath:com/blackbread/dao/mapper/*.xml" />
</bean>

<!--mapper scanning -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.blackbread.dao" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

SqlSessionFactoryBean

首先讓我們來看 SqlSessionFactoryBean 類早抠,在這個類初始化的時候霎烙,需要注入 DataSource,而且是需要跟初始化 TransactionManager 時候注入的 DataSource 為同一個蕊连。

SqlSessionFactoryBean 類實現(xiàn)了 InitializingBean 接口悬垃,所以初始化后會執(zhí)行 afterPropertiesSet() 方法,在 afterPropertiesSet() 方法中會執(zhí)行 buildSqlSessionFactory() 方法生成一個 SqlSessionFactory 對象甘苍,讓我們看下 buildSqlSessionFactory() 方法尝蠕。

SqlSessionFactoryBean#buildSqlSessionFactory()

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        Configuration configuration;
        XMLConfigBuilder xmlConfigBuilder = null;
        // 初始化一個configuration
        if (this.configLocation != null) {
            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            configuration = new Configuration();
            configuration.setVariables(this.configurationProperties);
        }
        if (this.objectFactory != null) {
            configuration.setObjectFactory(this.objectFactory);
        }
        if (this.objectWrapperFactory != null) {
            configuration.setObjectWrapperFactory(this.objectWrapperFactory);
        }
        if (hasLength(this.typeAliasesPackage)) {
            String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
                                                                   ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeAliasPackageArray) {
                configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                                                                     typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
            }
        }
        // 設(shè)置別名
        if (!isEmpty(this.typeAliases)) {
            for (Class<?> typeAlias : this.typeAliases) {
                configuration.getTypeAliasRegistry().registerAlias(typeAlias);
            }
        }
        // 裝入插件,mybatis的插件都是以攔截器的形式
        // 比如比如分頁插件载庭,這里是載入 spring 中注入的
        if (!isEmpty(this.plugins)) {
            for (Interceptor plugin : this.plugins) {
                configuration.addInterceptor(plugin);
            }
        }
        if (hasLength(this.typeHandlersPackage)) {
            String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
                                                                      ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeHandlersPackageArray) {
                configuration.getTypeHandlerRegistry().register(packageToScan);
            }
        }

        if (!isEmpty(this.typeHandlers)) {
            for (TypeHandler<?> typeHandler : this.typeHandlers) {
                configuration.getTypeHandlerRegistry().register(typeHandler);
            }
        }
        // 這里將解析mybatis.xml文件看彼,載入所有配置,插件昧捷、setting等
        if (xmlConfigBuilder != null) {
            try {
                xmlConfigBuilder.parse();
            } catch (Exception ex) {
                throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
            } finally {
                ErrorContext.instance().reset();
            }
        }
        // 這個很重要闲昭,這里定義了用的transactionFactory為SpringManagedTransactionFactory
        // 這個在獲取 connection 等地方都有用到罐寨,是mybatis跟spring的主要鏈接類
        if (this.transactionFactory == null) {
            this.transactionFactory = new SpringManagedTransactionFactory();
        }
        // 新建一個Environment對象靡挥,并將新建的transactionFactory放入其中
        Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource);
        configuration.setEnvironment(environment);
        if (this.databaseIdProvider != null) {
            try {
                configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
            } catch (SQLException e) {
                throw new NestedIOException("Failed getting a databaseId", e);
            }
        }
        if (!isEmpty(this.mapperLocations)) {
            for (Resource mapperLocation : this.mapperLocations) {
                if (mapperLocation == null) {
                    continue;
                }
                try {
                    // 這里主要是解析配置的sql mapper配置文件
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                                                                             configuration, mapperLocation.toString(), configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }
            }
        } else {
        }
    }
    return this.sqlSessionFactoryBuilder.build(configuration);
}

比較長的一段代碼,其實這里做的工作就是解析配置文件生成 Configuration 對象而已鸯绿。在 XmlMapperBuilder#parse() 方法中跋破,這里將解析 Sql Mapper 文件中的映射關(guān)系生成 MappedStatement 對象,并執(zhí)行 Configuration#addMappedStatement() 方法瓶蝴,將其放入到 Configuration 對象中毒返,有興趣的同學可以仔細看下。

這里最需要注意的一塊就是 this.transactionFactory = new SpringManagedTransactionFactory()舷手。SpringManagedTransactionFactory 就是 MyBatis 跟 Spring 的鏈接拧簸。

MapperScannerConfigurer

接著我們看一下 MapperScannerConfigurer 對象的初始化過程,這個對象實現(xiàn)了BeanDefinitionRegistryPostProcessor 接口男窟,所以看 postProcessBeanDefinitionRegistry() 方法盆赤,在這個方法中初始化一個對象 ClassPathMapperScanner,并執(zhí)行 scan() --> doScan() 方法歉眷。

ClassPathMapperScanner#doScan()

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    for (BeanDefinitionHolder holder : beanDefinitions) {
        GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
        definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
        // 實際就是將掃描到的接口包裝成MapperFactoryBean的實現(xiàn)類
        definition.setBeanClass(MapperFactoryBean.class);
        definition.getPropertyValues().add("addToConfig", this.addToConfig);
        boolean explicitFactoryUsed = false;
        // 注入sqlSessionFactory對象牺六,這個也很重要
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
            definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
            explicitFactoryUsed = true;
        } else if (this.sqlSessionFactory != null) {
            definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
            explicitFactoryUsed = true;
        }
        if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
            if (explicitFactoryUsed) {
                logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
            }
            definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
            explicitFactoryUsed = true;
        } else if (this.sqlSessionTemplate != null) {
            if (explicitFactoryUsed) {
                logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
            }
            definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
            explicitFactoryUsed = true;
        }

        if (!explicitFactoryUsed) {
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
    }
    return beanDefinitions;
}

這段代碼其實主要就是根據(jù) basePackage 屬性的配置,掃描相應的接口類汗捡,并且注冊到 Spring 中淑际,并且定義此對象的 FactoryBean 為:MapperFactoryBean ,而 MapperFactoryBean 將返回如下對象。

  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

  public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }

最終其實就是生成 Handler 為 MapperProxy春缕,接口為 mapperInterface 屬性指定(業(yè)務的 Mapper 接口)的代理類盗胀。同時添加屬性:sqlSessionFactory,這個操作很重要锄贼,在后面有核心應用读整。

MapperFactoryBean

這里讓我們看下 MapperFactoryBean 類,這個類繼承自 SqlSessionDaoSupport 在 SqlSessionDaoSupport 中有如下方法:

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
        this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
}

也就是上面調(diào)用的添加 sqlSessionFactory 屬性的 set 操作咱娶,在這個方法中初始化了 SqlSession 對象米间,用的是 SqlSessionTemplate 實現(xiàn)類。接下來讓我們看下 SqlSessionTemplate 的初始化過程:

SqlSessionTemplate

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

SqlSessionTemplate 其實實現(xiàn)了 SqlSession 接口的膘侮,在初始化的時候?qū)⑸梢粋€接口為 SqlSession屈糊,名為 sqlSessionProxy 代理對象,可以看到 SqlSessionTemplate 里面的所有與數(shù)據(jù)庫相關(guān)的操作都是通過sqlSessionProxy 這個代理對象實現(xiàn)的琼了。

接著看下 sqlSessionProxy 代理對象的實際 handler:

SqlSessionInterceptor

private class SqlSessionInterceptor implements InvocationHandler {
    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 {
        // 實際 SQL 的執(zhí)行過程
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          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) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

invoke() 方法中首先需要獲取一個 SqlSession 對象:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    if (holder != null && holder.isSynchronizedWithTransaction()) {
        if (holder.getExecutorType() != executorType) {
            throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
        }
        holder.requested();
        return holder.getSqlSession();
    }
    SqlSession session = sessionFactory.openSession(executorType);
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        Environment environment = sessionFactory.getConfiguration().getEnvironment();
        if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
            holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
            TransactionSynchronizationManager.bindResource(sessionFactory, holder);
            TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
            holder.setSynchronizedWithTransaction(true);
            holder.requested();
        } else {
            if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
            } else {
                throw new TransientDataAccessResourceException(
                    "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
            }
        }
    } else {
    }
    return session;
}

這里將首先去判斷下 SqlSessionHolder 是否已經(jīng)存在逻锐,如果不存在將會初始化一個新的,我們這里只分析第一次調(diào)用過程雕薪,也就是將會執(zhí)行到 SessionFactory#openSession() 方法昧诱,這個方法里面接著會調(diào)用 openSessionFromDataSource() 方法。

SessionFactory#openSession()

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
        return new DefaultSqlSession(configuration, executor);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

這里與 Spring 事務管理關(guān)聯(lián)起來的核心代碼所袁,讓我們仔細分析下盏档,首先這里將通過 getTransactionFactoryFromEnvironment() 方法獲取 TransactionFactory。這個操作將得到我們之前初始化時候注入的 SpringManagedTransactionFactory 對象燥爷。然后將執(zhí)行 TransactionFactory#newTransaction() 方法蜈亩,初始化 MyBatis 的 Transaction。

再下面將通過 Configuration.newExecutor() 生成一個 Executor前翎。由于在之前指定了 execType 為Simple稚配,所以在這里將生成一個 SimpleExecutor: executor = new SimpleExecutor(this, transaction),并將剛初始化的 Transaction 加入屬性港华。

實際語句執(zhí)行

到這里SqlSession的初始化也就完成了道川,接下來就是看下實際方法的執(zhí)行了,也就是 Object result = method.invoke(sqlSession, args)立宜。

以一個 update() 方法執(zhí)行來舉例:

public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

首先將從 Configuration 中根據(jù)操作的 statement 獲取映射內(nèi)容 MappedStatement冒萄。getMappedStatement() 方法中其實就是從 Map 對象中根據(jù) key 取出之前緩存的數(shù)據(jù)。

Executor#update()

接著將執(zhí)行 Executor#update() 方法赘理,也就是實際的數(shù)據(jù)庫操作了宦言,記得之前初始化的 Executor 么,這里就是那個 SimpleExecutor商模。

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) throw new ExecutorException("Executor was closed.");
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

主要是看 prepareStatement() 方法奠旺,看到 Connection 的獲取了吧蜘澜?

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
  }

然后接著看 getConnection() 方法:

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog);
    } else {
      return connection;
    }
  }

到這里終于看到了真正的 Connection 獲取過程:Transaction#getConnection(),也就是通過之前注入的Transaction 來獲取 Connection响疚,而這個 Transaction 也就是 SpringManagedTransaction鄙信,他其實調(diào)用了 openConnection() 方法。

 private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
  }

openConnection() 方法中主要通過調(diào)用 DataSourceUtils#getConnection() 方法來獲取一個Connection忿晕。繼續(xù)看 DataSourceUtils#getConnection() 方法装诡,實際調(diào)用的又是 doGetConnection() 方法。

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    //從TransactionSynchronizationManager中獲取ConnectionHolder
    // 這個對象也就是之前我們上一次分析 Spring Transaction 的時候
    // 持有ConnectionHolder的對象了
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    // 由于在前面的切面中已經(jīng)開啟事務
    // 并且初始化了ConnectionHolder所以這里將直接返回ConnectionHolder中的connection
    if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            conHolder.setConnection(dataSource.getConnection());
        }
        return conHolder.getConnection();
    }
    Connection con = dataSource.getConnection();
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        ConnectionHolder holderToUse = conHolder;
        if (holderToUse == null) {
            holderToUse = new ConnectionHolder(con);
        }
        else {
            holderToUse.setConnection(con);
        }
        holderToUse.requested();
        TransactionSynchronizationManager.registerSynchronization(
            new ConnectionSynchronization(holderToUse, dataSource));
        holderToUse.setSynchronizedWithTransaction(true);
        if (holderToUse != conHolder) {
            TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
        }
    }
    return con;
}

是不是感覺這段代碼很眼熟践盼?對鸦采,因為這里有我們上一篇里面非常熟悉的TransactionSynchronizationManager,在 Spring Transaction 中也是通過這個類中的 resources 屬性(ThreadLocal對象)對 ConnectionHolder 進行持有的咕幻。

在這里將獲取到 Spring 開啟事務時候持有的 ConnectionHolder 對象渔伯,自然獲取到的 Connection 對象也是 Srping 開啟事務時候創(chuàng)建的對象,這樣就保證了Spring Transaction 中控制的 Connection 跟在 MyBatis 中執(zhí)行 SQL 語句用的 Connection 為同一個 Connection肄程,也就可以通過之前 Spring 事務管理機制進行事務管理了锣吼。

后續(xù)的對數(shù)據(jù)的操作有興趣的可以自己讀一下,感覺 MyBatis 的源碼沒有 Spring 的那么清晰蓝厌,還是需要仔細分析下才能看的明白玄叠。

其他

注意事項

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    //從TransactionSynchronizationManager中獲取ConnectionHolder
    // 這個對象也就是之前我們上一次分析 Spring Transaction 的時候
    // 持有ConnectionHolder的對象了
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    // 由于在前面的切面中已經(jīng)開啟事務
    // 并且初始化了ConnectionHolder所以這里將直接返回ConnectionHolder中的connection
    if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            conHolder.setConnection(dataSource.getConnection());
        }
        return conHolder.getConnection();
    }
    Connection con = dataSource.getConnection();
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        ConnectionHolder holderToUse = conHolder;
        if (holderToUse == null) {
            holderToUse = new ConnectionHolder(con);
        }
        else {
            holderToUse.setConnection(con);
        }
        holderToUse.requested();
        TransactionSynchronizationManager.registerSynchronization(
            new ConnectionSynchronization(holderToUse, dataSource));
        holderToUse.setSynchronizedWithTransaction(true);
        if (holderToUse != conHolder) {
            TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
        }
    }
    return con;
}

我們再看下 DataSourceUtils#doGetConnection() 方法,如果 conHolder 為空拓提,也就是沒有開啟事務情況下读恃,將執(zhí)行 Connection con = dataSource.getConnection(),也就是說沒有開啟事務情況下崎苗,每次的數(shù)據(jù)庫操作都將從連接池拿一個新的 Connection狐粱,而不是第一次獲取后就與線程綁定。

這樣做可能是從系統(tǒng)并行度方面考慮的胆数,因為連接是比較稀缺的資源,在不開啟事務情況下互墓,應遵循隨用隨拿必尼、用完即還的原則。如果連接跟線程綁定篡撵,當線程執(zhí)行完數(shù)據(jù)庫操作判莉,又執(zhí)行了其他耗時操作,那么與其綁定的連接將無法得到復用育谬,大大降低了系統(tǒng)的并行度(受連接池中連接數(shù)量限制)券盅。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膛檀,隨后出現(xiàn)的幾起案子锰镀,更是在濱河造成了極大的恐慌娘侍,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泳炉,死亡現(xiàn)場離奇詭異憾筏,居然都是意外死亡,警方通過查閱死者的電腦和手機花鹅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門氧腰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刨肃,你說我怎么就攤上這事古拴。” “怎么了真友?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵斤富,是天一觀的道長。 經(jīng)常有香客問我锻狗,道長满力,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任轻纪,我火速辦了婚禮油额,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刻帚。我一直安慰自己潦嘶,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布崇众。 她就那樣靜靜地躺著掂僵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪顷歌。 梳的紋絲不亂的頭發(fā)上锰蓬,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音眯漩,去河邊找鬼芹扭。 笑死,一個胖子當著我的面吹牛赦抖,可吹牛的內(nèi)容都是我干的舱卡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼队萤,長吁一口氣:“原來是場噩夢啊……” “哼轮锥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起要尔,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤舍杜,失蹤者是張志新(化名)和其女友劉穎新娜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝴簇,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡杯活,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了熬词。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旁钧。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖互拾,靈堂內(nèi)的尸體忽然破棺而出歪今,到底是詐尸還是另有隱情,我是刑警寧澤颜矿,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布寄猩,位于F島的核電站骑疆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏箍铭。R本人自食惡果不足惜泊柬,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一兽赁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冷守,春花似錦、人聲如沸拍摇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至堪唐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淮菠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工合陵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留枢赔,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓踏拜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親低剔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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