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

mybatis-springMyBatis 的一个子项目,用于帮助开发者将 MyBatis 无缝集成到 Spring 中。它允许 MyBatis 参与到 Spring 的事务管理中,创建映射器 mapperSqlSession 并注入到 Spring bean 中。

SqlSessionFactoryBean

MyBatis 的基础用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory,最终获得执行接口 SqlSession 的,而在 mybatis-spring 中,则使用 SqlSessionFactoryBean 来创建。其使用方式如下:

  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    // 设置配置文件路径
    bean.setConfigLocation(new ClassPathResource("config/mybatis-config.xml"));
    // 别名转化类所在的包
    bean.setTypeAliasesPackage("com.wch.domain");
    // 设置数据源
    bean.setDataSource(dataSource);
    // 设置 mapper 文件路径
    bean.setMapperLocations(new ClassPathResource("mapper/*.xml"));
    // 获取 SqlSessionFactory 对象
    return bean.getObject();
  }

SqlSessionFactoryBean 实现了 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);
    }

    // 设置对象创建工厂、对象包装工厂、虚拟文件系统实现
    Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
    Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
    Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

    // 以包的维度注册别名转换器
    if (hasLength(this.typeAliasesPackage)) {
      // 扫描之类包下的符合条件的类对象
      scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
        // 过滤匿名类
        .filter(clazz -> !clazz.isAnonymousClass())
        // 过滤接口
        .filter(clazz -> !clazz.isInterface())
        // 过滤成员类
        .filter(clazz -> !clazz.isMemberClass()).
        // 注册别名转换器
        forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
    }

    // 以类的维度注册别名转换器
    if (!isEmpty(this.typeAliases)) {
      Stream.of(this.typeAliases).forEach(typeAlias -> {
        // 注册类对象到别名转换器
        targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
        LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
      });
    }

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

    // 以包的维度注册类型转换器
    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()))
        // 注册类对象到类型转换器
        .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    // 以类的维度注册类型转换器
    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        // 注册类对象到类型转换器
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }

    // 注册脚本语言驱动
    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);

    // 配置数据库产品识别转换器
    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);
      }
    }

    // 设置缓存配置
    Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

    // 如果设置了配置文件路径,则解析并加载到全局配置中
    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();
      }
    }

    // 设置数据源环境
    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.");
    }

    // 创建 sql 会话工厂
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }

buildSqlSessionFactory 方法会分别对配置文件、别名转换类、mapper 文件等进行解析,逐步配置全局配置对象,并最终调用 SqlSessionFactoryBuilder 创建 SqlSessionFactory 对象。

SqlSessionTemplate

在前章分析 MyBatis 接口层时说到 SqlSessionManager 通过 JDK 动态代理为每个线程创建不同的 SqlSession 来解决 DefaultSqlSession 的线程不安全问题。mybatis-spring 的实现与 SqlSessionManager 大致相同,但是其提供了更好的方式与 Spring 事务集成。

SqlSessionTemplate 实现了 SqlSession 接口,但是都是委托给成员对象 sqlSessionProxy 来实现的。sqlSessionProxy 在构造方法中使用 JDK 动态代理初始化为代理类。

  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 {
        // 执行原始调用
        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) {
          // 异常为 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) {
          // 关闭 sql session
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

获取 SqlSession

在执行原始调用前会先根据 SqlSessionUtils#getSqlSession 方法获取 SqlSession,如果通过事务同步管理器 TransactionSynchronizationManager 获取不到 SqlSession,就会使用 SqlSessionFactory 新建一个 SqlSession,并尝试将获取的 SqlSession 注册到 TransactionSynchronizationManager 中。

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

    // SqlSessionFactory 和 ExecutorType 参数不可为 null
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    // 尝试从事务同步管理器中获取 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 注册到事务同步管理器中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

事务同步管理器

每次获取 SqlSession 时是新建还是从事务同步管理器中获取决于事务同步管理器是否开启。事务同步管理器用于维护当前线程的同步资源,如判断当前线程是否已经开启了一个事务就需要查询事务同步管理器,以便后续根据事务传播方式决定是新开启一个事务或加入当前事务。Spring 支持使用注解开启事务或编程式事务。

注解开启事务

Spring 工程中可以通过添加 EnableTransactionManagement 注解来开启 Spring 事务管理。EnableTransactionManagement 注解的参数 mode = AdviceMode.PROXY 默认指定了加载代理事务管理器配置 ProxyTransactionManagementConfiguration,在此配置中其默认地对使用 Transactional 注解的方法进行 AOP 代理。在代理逻辑中,会调用 AbstractPlatformTransactionManager#getTransaction 方法获取当前线程对应的事务,根据当前线程是否有活跃事务、事务传播属性等来配置事务。如果是新创建事务,就会调用 TransactionSynchronizationManager#initSynchronization 方法来初始化当前线程在事务同步管理器中的资源。

编程式事务

编程开启事务的方式与注解式其实是一样的,区别在于编程式需要手动开启事务,其最终也会为当前线程在事务同步管理器中初始化资源。

  // 手动开启事务
  TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
  try {
    // invoke...
  } catch (Exception e) {
    transactionManager.rollback(txStatus);
    throw e;
  }
  transactionManager.commit(txStatus);

SqlSession 注册

如果当前方法开启了事务,那么创建的 SqlSession 就会被注册到事务同步管理器中。SqlSession 会首先被包装为 SqlSessionHolder,其还包含了 SqlSession 对应的执行器类型、异常处理器。

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

随后 SqlSessionHolder 对象通过 TransactionSynchronizationManager#bindResource 方法绑定到事务同步管理器中,其实现为将 SqlSessionFactorySqlSessionHolder 绑定到 ThreadLocal 中,从而完成了线程到 SqlSessionFactorySqlSession 的映射。

事务提交与回滚

如果事务是交给 Spring 事务管理器管理的,那么Spring 会自动在执行成功或异常后对当前事务进行提交或回滚。如果没有配置 Spring 事务管理,那么将会调用 SqlSessioncommit 方法对事务进行提交。

  if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    // 未被事务管理器管理,设置提交
    sqlSession.commit(true);
  }

SqlSessionTemplate 是不允许用来显式地提交或回滚的,其提交或回滚的方法实现为直接抛出 UnsupportedOperationException 异常。

关闭 SqlSession

在当前调用结束后 SqlSessionTemplate 会调动 closeSqlSession 方法来关闭 SqlSession,如果事务同步管理器中存在当前线程绑定的 SqlSessionHolder,即当前调用被事务管理器管理,则将 SqlSession 的持有释放掉。如果没被事务管理器管理,则会真实地关闭 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)) {
      // 被事务管理器管理,释放 SqlSession
      LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
      holder.released();
    } else {
      // 真实地关闭 SqlSesion
      LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
      session.close();
    }
  }

映射器

单个映射器

MyBatis 的基础用法中,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 直接与接口方法的全限定名关联,消除了 mapper 接口实现类的样板代码。但是此种方式在每次获取 mapper 代理类的时候都需要指定 sqlSession 对象,而 mybatis-spring 中的 sqlSession 对象是 SqlSessionTemplate 代理创建的,为了适配代理逻辑,mybatis-spring 提供了 MapperFactoryBean 来创建代理类。

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

MapperFactoryBean 继承了 SqlSessionDaoSupport,其会根据传入的 SqlSessionFactory 来创建 SqlSessionTemplate,并使用 SqlSessionTemplate 来生成代理类。

  @Override
  public T getObject() throws Exception {
    // 使用 SqlSessionTemplate 来创建代理类
    return getSqlSession().getMapper(this.mapperInterface);
  }

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

批量映射器

每次手动获取单个映射器的效率是低下的,MyBatis 还提供了 MapperScan 注解用于批量扫描 mapper 接口并通过 MapperFactoryBean 创建代理类,注册为 Spring bean

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

MapperScan 注解解析后注册 Spring bean 的逻辑是由 MapperScannerConfigurer 实现的,其实现 了 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) {
    // 扫描指定包已经获取的 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();
      // ...
      // 设置 bean class 类型为 MapperFactoryBean
      definition.setBeanClass(this.mapperFactoryBeanClass);
            // ...
    }
  }

获取到指定 bean 的定义后,重新设置 beanClassMapperFactoryBean,因此在随后的 bean 初始化中,这些被扫描的 mapper 接口可以创建代理类并被注册到 Spring 容器中。

映射器注册完成后,就可以使用引用 Spring bean 的配置来使用 mapper 接口。

小结

mybatis-spring 提供了与 Spring 集成的更高层次的封装。

  • SqlSessionFactoryBean 遵循 Spring FactoryBean 的定义,使得 SqlSessionFactory 注册在 Spring 容器中。
  • SqlSessionTemplateSqlSession 另一种线程安全版本的实现,并且能够更好地与 Spring 事务管理集成。
  • MapperFactoryBean 是生成 mapper 接口代理类的 SqlSessionTemplate 版本实现。
  • MapperScannerConfigurer 简化了生成 mapper 接口代理的逻辑,指定扫描的包即可将生成 mapper 接口代理类并注册为 Spring bean
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容