Spring详解8.Spring DAO

一年又一年,字节跳动 Lark(飞书) 研发团队又双叒叕开始招新生啦!
【内推码】:GTPUVBA
【内推链接】:https://job.toutiao.com/s/JRupWVj
【招生对象】:20年9月后~21年8月前 毕业的同学
【报名时间】:6.16-7.16(提前批简历投递只有一个月抓住机会哦!)
【画重点】:提前批和正式秋招不矛盾!面试成功,提前锁定Offer;若有失利,额外获得一次面试机会,正式秋招开启后还可再次投递。

点击进入我的博客

更多章节

Spring详解1.概述
Spring详解2.理解IoC容器
Spring详解3.Bean的装配
Spring详解4.容器内幕
Spring详解5.AOP
Spring详解6.基于AspectJ的AOP
Spring详解7.Spring MVC
Spring详解8.Spring DAO

1 Spring DAO的概念

什么是DAO

DAO(Data Access Object)是用于访问数据的对象,虽然在大多数情况下将数存在数据库中,但这并不是唯一的选择,也可以将数据存储到文件中或LDAP中。DAO不但屏蔽了数据存储的最终介质的不同,也屏蔽了具体的实现技术的不同。提供DAO层的抽象可以带来一些好处:可以很容易地构造模拟对象,方便单元测试的开展;在使用切面时会有更多的选择,既可以使用JDK动态代理,又可以使用 CGLib动态代理。

Spring DAO的内容
  • Spring对多个持久化技术提供了集成支持,包括 Hibernate、 MyBatis、JPA、JDO;
  • Spring提供一个简化JDBC API操作的Spring JDBC框架。
  • Spring面向DAO制定了一个通用的异常体系,屏蔽具体持久化技术的异常,使业务层和具体的持久化技术实现解耦。
  • Spring提供了模板类简化各种持久化技术的使用。

2 Spring统一的异常体系

数据访问异常DataAccessException

Spring在org.springframework.dao包中提供了一套完备优雅的DAO异常体系,这些异常都继承于 DataAccessException,而DataAccessException本身又继承于NestedRuntimeException,NestedRuntime Exception异常以嵌套的方式封装了源异常。因此,虽然不同持久化技术的特定异常被转换到 Spring的DAO异常体系中,但原始的异常信息并不会丢失;只要用户愿意,就可以方便地通过getCause()方法获取原始的异常信息。

异常体系
Spring DAO 异常体系第一层次的异常类

Spring以分类手法建立了异常分类目录,上图列出了那些位于Spring DAO异常体系第一层次的异常类,每个异常类下可能拥有众多的子异常类。

异常 说明
CleanupFailureDataAccessException DAO操作成功执行,但在释放数据资源时发生异常,如关闭 Connection时发生异常等
ConcurrencyFailureException 表示在进行并发数据操作时发生异常,如乐观锁无法获取、悲观锁无法获取、死锁引发的失败等
DataAccessResourceFailureException 访问数据资源时失败,如无法获取数据连接、无法获取Hibernate的会话等
DataRetrievalFailureException 获取数据失败,如找不到对应主键的数据、使用了错误的列索引等
DataSourceLookupFailure Exception 无法从JNDI中查找到数据源
DatalntegrityViolationException 当数据操作违反了数据一致性限制时抛出的异常,如插入重复的主键、引用不存在的外键等
InvalidDataAccessApiUsageException 不正确地调用某一持久化技术时抛出的异常,如在 Spring JDBC中查询对象,在调用前必须进行编译操作,如果忘记这项操作则会产生该异常。这种异常不是由底层数据资源产生的,而是由不正确地使用持久化技术产生的
InvalidDataAccessResourceUsageException 在访问数据源时使用了不正确的方法所抛出的异常,如SQL语句错误将抛出该异常
PermissionDeniedDataAccessException 数据访问时由于权限不足引发的异常,如仅拥有只读权限的用户试图进行数据更改操作时将抛出该异常
UncategorizedAccessException 其他未分类的异常都归到该异常中
JDBC异常转换器
  • 传统的JDBC API在发生几乎所有的数据操作问题时都会抛出相同的SQLException,它将异常的细节性信息封装在异常属性中。SQLException拥有两个代表异常具体原因的属性:错误码和SQL状态码。
  • Spring根据错误码和SQL状态码信息将SQLException译成 Spring DAO的异常体系所对应的异常。org.springframework.jdbc.support.SQLExceptionTranslator接口的两个实现类SQLErrorCodeTranslator和SQLStateSQLExceptionTranslator分别负责处理SQLException中错误码和SQL状态码的翻译工作。
其他持久化技术的异常转换器

由于各种框架级的持久化技术都拥有一个语义明确的异常体系,所以将这些异常转换为Spring DAO的体系相对轻松一些。在org.springframework.orm包中,分别为Spring所支持的ORM持久化技术定义了一个子包,在这些子包中提供相应ORM技术的整合类。

ORM持久化技术 异常转换器
Hibernate org.springframework.orm.hibernate.Session.FactoryUtils
JPA org.springframework.orm.jpa.EntityManagerFactoryUtils
JDO org.springframework.orm.jdo.PersistenceManagerFactoryUtils
MyBatis MyBatis抛出的异常是和JDBC相同的SQLException异常,所以直接采用和JDBC相同的异常转换器

3 统一数据访问模板

访问数据库的流程

以JDBC为例,访问数据库的操作大致按照以下流程进行:准备资源、启动事务、在事务中执行具体的数据访问操作、提交/回滚事务、关闭资源及处理异常。而其中除了在事务中执行具体的数据访问操作是业务相关的,其他代码都是几乎固定不变的。

Spring DAO的模板

Spring将这个相同的数据访问流程固化到模板类中,并将数据访问中固定和变化的部分分开,同时保证模板类是线程安全的,以便多个数据访问线程共享同一个模板实例。固定的部分在模板类中已经准备好,而变化的部分通过回调接口开放出来,用于定义具体数据访问和结果返回的操作。这样,只要编写好回调接口,并调用模板类进行数据访问,就可以得到预想的结果。


Spring DAO的模板和回调
不同持久化技术的模板类

Spring为各种支持的持久化技术都提供了简化操作的模板和回调,在回调中编写具体的数据操作逻辑,使用模板执行数据操作。

ORM持久化技术 模板类
JDBC org.springframework.jdbc.core.JdbcTemplate
Hibernate org.springframework.orm.hibernate.HibernateTemplate
JPA org.springframework.orm.JpaTemplate
JDO org.springframework.orm.jdo.JdoTemplate

如果直接使用模板类,则一般需要在DAO中定义一个模板对象并提供数据资源。Spring为每种持久化技术都提供了支持类,支持类中已经完成了这样的功能。这样,只需扩展这些支持类,就可以直接编写实际的数据访问逻辑,因此更加方便。这些支持类都继承于dao.support.DaoSupport类,DaoSupport类实现了InitializingBean接口,在afterPropertiesSet()接口方法中检査模板对象和数据源是否被正确设置,否则将抛出异常。

ORM持久化技术 支持类
JDBC org.springframework.jdbc.core.JdbcDaoSupport
Hibernate org.springframework.orm.hibernate.HibernateDaoSupport
JPA org.springframework.orm.jpa.JpaDaoSupport
JDO org.springframework.orm.jdo.JdoSupport

4 数据源

不管采用何种持久化技术,都必须拥有数据连接。在 Spring中,数据连接是通过数据源获得的。在Spring中,不但可以通过JNDI获取应用服务器的数据源,也可以直接在Spring容器中配置数据源,还可以通过代码的方式创建一个数据源,以便进行无容器依赖的单元测试。

配置DBCP数据源
@Bean(destroyMethod = "close")
    public BasicDataSource basicDataSource() {
        BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        basicDataSource.setUrl("jdbc:mysql://localhost:3306/test");
        basicDataSource.setUsername("root");
        basicDataSource.setPassword("password");
        return basicDataSource;
    }
  • BasicDataSource提供了close方法关闭数据源,所以必须设定destroyMethod为close,以便 Spring容器关闭时,数据源能够正常关闭。
  • 假设数据库是 MySQL,如果数据源配置不当,则将可能发生经典的8小时问题——原因是MySQL在默认情况下如果发现一个连接的空闲时间超过8小时,则将会在数据库端自动关闭这个连接。而数据源并不知道这个连接已经被数据库关闭了,当它将这个无用的连接返回给某个DAO时,DAO就会报无法获取 Connection的异常。
配置C3P0数据源

C3P0是一个开放源码的JDBC数据源实现项目,实现了JDBC3和JDBC2扩展规范说明的Connection和Statement池。ComboPooledDataSource也提供了一个用于关闭数据源的close方法,这样就可以保证 Spring容器关闭时数据源能够被成功释放。C3P0拥有比DBCP更丰富的配置属性,通过这些属性,可以对数据源进行各种有效的控制。

配置Druid数据源

Druid首先是阿里巴巴开源的一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser。Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。

    @Bean
    public DruidDataSource druidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
使用配置文件放置属性
@PropertySource(value = "classpath:database.properties")
public class DatabaseConfig {
    @Value("${jdbc.driverClassName}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
}
配置JNDI

如果应用配置在高性能的应用服务器上,则可能更希望使用应用服务器本身提供的数据源。应用服务器的数据源使用JNDI开放调用者使用,Spring为此专门提供了引用JDI数据源的JndiobjectFactoryBean类,通过jndiName指定引用的JNDI数据源名称。

Spring数据源

Spring本身也提供了一个简单的数据源实现类DriverManagerDataSource,它位于org. springframework.jdbc.datasource包中。这个类实现了Javax.sql.DataSource接口,但它并没有提供池化连接的机制;每次调用 getConnectionO方法获取新连接时,只是简单地创建一个新的连接。因此,这个数据源类比较适合在单元测试或简单的独立应用中使用,因为它不需要额外的依赖类。

5 Spring JDBC

Spring JDBC是Spring所提供的持久层技术。它的主要目的是降低使用 JDBC API的门槛,以一种更直接、更简洁的方式使用 JDBC API。在 Spring JDBC里,仅需做那些与业务相关的DML操作的事,而将资源获取、Statement创建、资源释放及异常处理等繁杂而乏味的工作交给Spring JDBC。

5.1 使用Spring JDBC

简单的例子
    private void func() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(); // (1)
        jdbcTemplate.setDataSource(dataSource);

        String sql = "INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')";
        jdbcTemplate.execute(sql);
    }

上述示例中,特意将数据源创建和模板实例创建的代码都列在这个例子中,但在实际应用中,用户一般不会在DAO中做这些事情。由于JdbcTemplate是线程安全的,因而所有的DAO都可以共享同一个 JdbcTemplate实例,这样(1)的代码就可以从DAO中移除了,转而在 Spring配置文件中统一定义即可。

在DAO中使用JdbcTemplate
@Configuration
public class DatabaseConfig {
    @Bean
    public JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

@Repository
public class MyTestDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void func() {
        jdbcTemplate.execute("INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')");
    }
}
JdbcTemplate的属性

JdbcTemplate拥有几个可用于控制底层JDBC API的属性

  • queryTimeout:设置 JdbcTemplate所创建的Statement查询数据时的最大超时时间。默认为0,表示使用底层JDBC驱动程序的默认设置。
  • fetchSize:设置底层的ResultSet每次从数据库返回的行数。该属性对程序的性能影响很大,如果设置过大,因为一次性载入的数据都放到内存中,所以内存的消耗很大:反之,如果设置过小,从数据库读取的次数将增大,也会影响性能。默认值为0,表示使用底层JDBC驱动程序的默认设置。
  • maxRows:设置底层的Resultset从数据库返回的最大行数。默认值为0,表示使用底层JDBC驱动程序的默认设置。
  • ignoreWarnings:是否忽略SQL的警告信息。默认为tue,即所有的警告信息都被记录到日志中,如果设置为fale,则JdbcTemplate将抛出SQLWarningException。

5.2 数据操作

更改数据update

JdbcTemplate在内部是通过PreparedStatement执行SQL语句的,可以使用绑定了参数的SQL语句。

    private void func() {
        String value = "ZZX";
        // (1) 直接执行SQL
        jdbcTemplate.update("UPDATE tb_name SET name = 'ZZX' WHERE id = '1'");
        // (2) 绑定参数的SQL
        jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 2});
        // (3) 指定参数的参数类型
        jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 3}, new int[]{Types.VARCHAR, Types.INTEGER});
    }
带回调的update

需要指出的是,在实际使用中,应优先考虑使用不带回调接口的 JdbcTemplate方法。首先,回调使代码显得臃肿;其次,回调并不能带来额外的好处。当使用简洁版的方法时,JdbcTemplate会在内部自动创建这些回调实例。以下是带回调带update方法:

  • int update(String sql, PreparedStatementSetter pss):PreparedStatementSetter是一个回调接口,它定义了一个void set values( PreparedStatement ps)接口方法。 JabcTemplate使用SQL语句创建出 PreparedStatement实例后,将调用该回调接口执行绑定参数的操作。PreparedStatement绑定参数时,参数索引从1开始而非从0开始。
  • int update(PreparedStatementCreator psc)PreparedStatementCreator也是一个回调接口,它负责创建一个PreparedStatement实例。该回调接口定义了一个PreparedStatement create PreparedStatement( Connection con)方法。
  • protected int update(PreparedStatementCreator psc, PreparedStatementSetter pss):联合使用PreparedStatementCreator和 PreparedStatementSetter回调。
获取自增主键

在JDBC3.0规范中,当新增记录时,允许将数据库自动产生的主键值绑定到Statement或PreparedStatement中。在使用 Statement时,可以通过以下方法绑定主键值:int executeUpdate(String sql, int autoGeneratedKeys);也可以通过Connection创建绑定自增主键值的PreparedStatement,如下:Preparedstatement prepareStatement(String sql, int autoGeneratedKeys)。Spring利用这一技术,提供了一个可以返回新增记录对应主键值的方法:int update(Preparedstatementcreator psc, KeyHolder generatedKey Holder)

private void func() {
        String sql = "INSERT INTO tb_name (name) VALUES (?)";
        // 创建一个主键持有对象
        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update((Connection con) ->  {
            PreparedStatement statement = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            statement.setString(1, "ZZX");
            return statement;
            }, keyHolder);
        // 获取主键
        System.out.println(keyHolder.getKey().intValue());
    }
批量修改数据

如果需要一次性插入或更新多条记录,最好的选择是使用JdbcTemplate批量数据更改的方
法。JdbcTemplate的两个批量数据操作方法:

  • int[] batchUpdate(String[] sql):多条SQL语句组成一个数组(这些SQL语句不带参数),该方法以批量方式执行这些SQL语句。 Spring在内部使用JDBC提供的批量更新API完成操作。
  • int[] batchUpdate(String sql, BatchPreparedStatementSetter pss):使用该方法对于同
    一结构的带参SQL语句多次进行数据更新操作。BatchPreparedStatementSetter是一次性批量提交数据的,getSize()是整批的大小。
private void func() {
        String sql = "INSERT INTO tb_name (name) VALUES (?)";
        String[] values = {"A", "B", "C"};

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, values[i]);
            }

            @Override
            public int getBatchSize() {
                return values.length;
            }
        });
    }
查询数据——使用RowCallbackHandler

当结果集中没有数据时,此时并不会抛出异常,而是此时RowCallbackHandler回调接口中定义的处理逻辑没有得到调用。

    private void func() {
        String sql = "SELECT * FROM tb_name WHERE id = 1";

        User user = new User();
        jdbcTemplate.query(sql, (ResultSet rs) -> {
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
        });
        System.out.println(user);
    }
批量查询数据——使用RowCallbackHandler
private void func() {
        String sql = "SELECT * FROM tb_name";

        List<User> users = new ArrayList<>();
        jdbcTemplate.query(sql, (ResultSet rs) -> {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
            users.add(user);
        });
        System.out.println(users);
    }
查询数据——使用RowMapper<T>
private void func() {
        String sql = "SELECT * FROM tb_name";

        List<User> users = jdbcTemplate.query(sql, (ResultSet rs, int rowNum) -> {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
            return user;
        });

        System.out.println(users);
    }
  • 从功能上来说,RowCallbackHandler和RowMapper<T并没有太大的区别,它们都用于定义结果集行的读取逻辑,将 Resultset中的数据映射到对象或List中。
  • 通过JDBC查询返回一个Resultset结果集时,JDBC并不会一次性将所有匹配的数据都加载到JM中,而是只返回一批次的数据(由JDBC驱动程序决定),当通过ResultSet#next()游标滚动结果集超过数据范围时,JDBC再获取一批数据。这样以一种“批量化+串行化”的处理方式避免大结果集处理时JVM内存的过大开销。
  • 当处理大结果集时,如果使用RowMapper,那么采用的方式是将结果集中的所有数据都放到一个Lis<T>对象中,这样将会占用大量的JVM内存,甚至可能直接引发OutOfMemoryException异常。这时,可使用RowCallbackHandler接口,在processRow()接口方法内部一边获取数据一边完成处理,这样数据就不会在内存中堆积,可大大减少对JVM内存的占用。
  • 采用RowMapper的操作方式是先获取数据,然后再处理数据;而 RowCallbackHandler的操作方式是一边获取数据一边处理,处理完就丢弃之。因此,可以将 Row Mapper看作采用批量化数据处理策略,而 RowHandler则采用流化处理策略。
查询单值数据

如果查询的结果集仅有一个值,如SELECT COUNT(*) FROM tb_name,这时可以使用更简单的方式获取结果集的值。 JdbcTemplate为获取结果集中的单值数据提供了3组方法,分别用于获取int、long的单值,其他类型的单值则以 Object类型返回。

调用存储过程

JdbcTemplate提供了两个调用存储过程的接口方法,分别介绍如下。

  • <T> T execute(String callString, CallableStatementCallback<T> action):用户通过callString参数指定调用存储过程的SQL语句;第二个参数CallableStatementCallback<T>是一个回调接口,可以在接口的方法中进行输入参数绑定、输出参数注册及返回数据处理等操作。
  • <T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action):该接口方法使用 CallableStatementCreator创建CallableStatement。CallableStatementCreator负责创建 CallableStatement实例、绑定参数、注册输出参数等工作,CallableStatementCallback<T>负责处理存储过程的返回结果。 Spring提供了创建CallableStatementcreator的工厂类 CallableStatementCreatorFactory,通过该工厂类可以简化CallableStatementCreator的实例创建工作。

6 整合其他ORM框架

6.1 Spring ORM框架好处

  1. 方便基础设施的搭建:对于不同的ORM框架,始终可以采用相同的方式配置数据源;Spring还为不同的ORM框架提供了相应的FactoryBean,用以初始化ORM框架的基础设施,可以将它们当成普通的Bean对待。
  2. 异常封装:Spring能够转换各种ORM框架所抛出的异常,将ORM框架专有的或检査型异常
    转换为Spring DAO异常体系中的标准异常。
  3. 统一的事务管理:通过使用基于 Spring DAO模板的编程风格,甚至使用ORM框架原生的API,只要遵循 Spring所提出的很少的编程要求,就可以使用 Spring提供的事务管理功能。此外,JDBC代码能够在事务级别上与用户使用的ORM框架一起使用。这一功能对于诸如批量处理、LOB操作等并不适合单独采用ORM完成的地方尤其有用。
  4. 允许混合使用多个ORM框架:Spring在DAO异常、事务、资源等高级层次建立了抽象,因而可以让业务层对DAO具体实现的技术不敏感,可以在底层选用适合的实现方式,甚至可以混合使用多种ORM
  5. 方便单元测试:Spring容器使得替换不同的实现和配置变得非常简单,这些内容包括 HibernateSession Factory的位置、 JDBC DataSource、事务管理器及映射对象的实现等,这样就很容易隔离并测试不同的DAO类。

6.2 整合Hibernate

后续在学习Hibernate时会详细讲解

6.3 整合MyBatis

详见:MyBatis详解8.集成Spring

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