mybatis中mapper代理的生成过程

目录

  1. mybatis中mapper代理的生成过程
  2. 与Spring集成时mapper代理的生成过程
  3. 与SpringBoot集成时mapper代理的生成过程

mybatis中mapper代理的生成过程

构建代理类工厂

从入口点开始一步一步看,首先SqlSessionFactoryBuilder类中build()方法加载配置文件

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      // ...省略
    }
  }

将配置文件读取为XMLConfigBuilder对象,并调用parse()方法来解析文件,进到parse()

  public Configuration parse() {
     // ...省略
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

可以看到具体的解析过程是在parseConfiguration方法中进行的。

  private void parseConfiguration(XNode root) {
    try {
       // ...省略
      //解析mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

这里重点看一下最后解析mapper的方法mapperElement(root.evalNode("mappers")),进到方法里,


  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //   package 形式加载 ,加载package下的所有class文件
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            // 通过Mapper.xml 加载
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // 通过Mapper.xml 加载
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // 通过单个class文件加载
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

整个mapperElement()方法就是加载mapper的过程了,可以看到加载mapper
有两种形式:通过class文件和通过xml文件。
构建mapper代理的过程也就是从这开始的,那就一步一步分析。
看一下通过XML文件加载的过程,mybatis将mapper相关的配置读取为一个XMLMapperBuilder对象,并通过parse()方法进行解析,进到这个方法中


  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 加载xml文件
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 加载mapper class文件
      bindMapperForNamespace();
    }
   // ...省略
  }

parse()方法做了主要做了两件事,加载xml文件和加载class文件。
看一下加载xml的过程

  private void configurationElement(XNode context) {
    try {
      // 获取xml文件的namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 保存获取xml文件的namespace
      builderAssistant.setCurrentNamespace(namespace);
       // ...省略
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

本文是分析mapper代理的生成过程,所以加载xml的具体细节就不详细分析了,这里注意的是读取xml文件中namespace标签的值,并将值设置到builderAssistant对象中
现在回过头来看一下加载class文件的过程。进到bindMapperForNamespace()方法中去

  private void bindMapperForNamespace() {
    // 获取xml文件中设置的namespace值
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 加载类
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // 添加到configuration中
          configuration.addMapper(boundType);
        }
      }
    }
  }

bindMapperForNamespace()通过xml文件中设置的namespace值加载对应的mapper接口,最后通过configuration.addMapper()添加到configuration中。

还记不记得刚才提到的加载mapper
有两种形式:通过class文件和通过xml文件。通过class文件的方式直接调用configuration.addMapper()将mapper接口加载到了configuration 中了。

Configuration是mybatis的全局配置类,所有的mybatis相关的信息都保存在Configuration中。
继续进到ConfigurationaddMapper方法中

  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

Configuration把对应的mapper接口添加到mapperRegistry中,再进到mapperRegistry.addMapper()方法中

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        // ...省略
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
         // ...省略
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

该方法首先判断是否是接口,如果是接口则将mapper接口添加到knownMappers中。
看一下knownMappers的定义

  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

knownMappers是一个HashMap,它保存的是所有的mapper接口和对应的mapper代理工厂。

到现在为止,mapper已经加载完了,但是并没有生成mapper的代理对象,只是生成了对应的代理工厂。

生成并使用代理对象

mybatis并没有在加载mapper接口的时候生成代理对象,而是在调用的时候生成的。
首先从入口开始

sqlSession.getMapper(XXX.class)

sqlSession默认是DefaultSqlSession。进到DefaultSqlSessiongetMapper()方法中

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

继续到ConfigurationgetMapper

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

继续到mapperRegistry.getMapper()

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // ...省略
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

knownMappers中获取到对应mapper接口的代理工厂类MapperProxyFactory,然后通过MapperProxyFactory获取真正的代理对象。
进到MapperProxyFactorynewInstance()方法中

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
  
   protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

首先生成了MapperProxy类,再通过Proxy生成真正的代理类。
看一下MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {
  //  ...省略
}

MapperProxy实现了InvocationHandler接口,mapper接口的具体处理逻辑也就是在这类中处理。

到此为止,代理对象才真正的生成。

与Spring集成时mapper代理的生成过程

mybatis与Spring集成时需要用到mybatis-spring的jar。

Spring注册mapper代理类

既然是与Spring集成,那么就要配置一下,将mybatis交给Spring管理。
spring的xml文件配置

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="driverClassName"/>
        <property name="url" value="url"/>
        <property name="username" value="username"/>
        <property name="password" value="password"/>
    </bean>

    <!--sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!--绑定mybatis配置文件-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--注册Mapper.xm映射器-->
        <property name="mapperLocations" value="classpath:cn/ycl/mapper/*.xml"/>
    </bean>

    <!--注册所有mapper-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--basePackage 属性是映射器接口文件的包路径。-->
        <!--你可以使用分号或逗号 作为分隔符设置多于一个的包路径-->
        <property name="basePackage" value="cn/ycl/mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

将mybatis交给Spring只需要配置3个bean就可以了
1、 数据库相关的dataSource
2、 mybatis的sqlSessionFactory
3、 将mapper委托给Spring的工具类MapperScannerConfigurer
生成mapper代理的过程主要在MapperScannerConfigurer里,看一下MapperScannerConfigurer的定义

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    // ...省略
}

关键点在MapperScannerConfigurer 实现了BeanDefinitionRegistryPostProcessorBeanDefinitionRegistryPostProcessor是Spring留的扩展点,可以往Spring中注册自定义的bean。

MapperScannerConfigurer中实现了BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry()方法,mapper的注册就是在该方法中注册的

  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // ...省略
    
    // 实例化ClassPathMapperScanner,并对scanner相关属性进行配置
    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.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    // 注册扫描规则
    scanner.registerFilters();
    // 扫描并注册所有的mapper
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

postProcessBeanDefinitionRegistry()的主要逻辑是定义一个ClassPathMapperScanner对象,然后调用registerFilters()注册扫描规则,最后调用scan()方法。

在xml中定义MapperScannerConfigurerbean时可以设置一个annotationClass属性,值是一个注解类,调用registerFilters()时,registerFilters()会添加一个只扫描设置有annotationClass注解的类,这里没有设置,会扫描所有的接口。SpringBoot集成mybatis时会用到这个字段

看一下ClassPathMapperScanner类的定义

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    // ...省略
}

ClassPathMapperScanner继承了ClassPathBeanDefinitionScannerClassPathBeanDefinitionScanner是Spring中定义的,是一个从指定包内扫描所有bean定义的Spring工具。

看一下ClassPathMapperScannerscan()方法

  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
        // ...省略
    } else {
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

通过super.doScan(basePackages)已经扫描到了所有的mapper,继续processBeanDefinitions()方法

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    // 遍历扫描到的所有bean
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();
      boolean scopedProxy = false;
      if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
        definition = (AbstractBeanDefinition) Optional
            .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
            .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
                "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
        scopedProxy = true;
      }
      String beanClassName = definition.getBeanClassName();
      LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
          + "' mapperInterface");

 
      // 增加一个构造方法,接口类型作为构造函数的入参
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 

      // 将bean的类型转换成mapperFactoryBean
      definition.setBeanClass(this.mapperFactoryBeanClass);

      // 增加addToConfig属性
      definition.getPropertyValues().add("addToConfig", this.addToConfig);

  
      definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

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

      // 增加sqlSessionTemplate属性
      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) {
        LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }

      definition.setLazyInit(lazyInitialization);

      if (scopedProxy) {
        continue;
      }

      if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
        definition.setScope(defaultScope);
      }

      if (!definition.isSingleton()) {
        BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
        if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
          registry.removeBeanDefinition(proxyHolder.getBeanName());
        }
        registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
      }

    }
  }

这个方法比较长,但是并不复杂,主要逻辑为将扫描的bean的类型修改成MapperFactoryBean类型,并增加一个将接口类型作为入参的构造函数,也就是说Spring获取mapper时都是通过FactoryBean生成的。最后通过调用egistry.registerBeanDefinition() 方法注册到Spring中。

看一下mybatis提供的MapperFactoryBean的定义

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
}

MapperFactoryBean实现了FactoryBeanFactoryBean是一个Spring提供的一个能生产对象的工厂Bean

MapperFactoryBean同时继承了SqlSessionDaoSupportSqlSessionDaoSupport继承了DaoSupportDaoSupport实现了InitializingBeanInitializingBean的作用是在Spring初始化bean对象时会首先调用InitializingBeanafterPropertiesSet()方法。

DaoSupportafterPropertiesSet()中调用了checkDaoConfig()方法。

    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        this.checkDaoConfig();

        try {
            this.initDao();
        } catch (Exception var2) {
            throw new BeanInitializationException("Initialization of DAO failed", var2);
        }
    }

具体checkDaoConfig()方法的实现逻辑在MapperFactoryBean

 protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        // ..省略
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

OK,到这又回到mybatis了。在前面中说了configuration.addMapper()方法只是生成了对应的代理工厂。

以上整个过程,即把mapper注册为Spring的bean,又将mapper设置到mybatis中的configuration中,所以,在使用时既可以使用Spring自动注入那一套,又可以使用mybatis中通过sqlSession来获取mapper的代理对象

Spring生成代理对象

Spring中所有的mapper对应的bean是mapper对应的MapperFactoryBean,那么在获取mapper bean时是通过MapperFactoryBeangetObject()方法生成的

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

MapperFactoryBean先获取到sqlsession,再通过getMapper()获取到的代理对象。到这里就回到了mybatis生成代理对象的过程了。

与SpringBoot集成时mapper代理的生成过程

mybatis与Spring集成时需要用到mybatis-spring-boot-starter的jar,mybatis-spring-boot-starter依赖mybatis-spring-boot-autoconfigure这个jar,而mybatis-spring-boot-autoconfigure这个jar又依赖mybatis-spring这个jar,所以最终其实还是mybatis集成Spring那一套

根据SpringBoot自动加载的原理直接看mybatis-spring-boot-autoconfigurejar下META-INF/spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

SpringBoot会自动加载MybatisAutoConfiguration这个类,直接看这个类,MybatisAutoConfiguration定义了mybtis所需的各个bean。


    //生成SqlSessionFactory
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // ...省略
    }
    
    //生成SqlSessionTemplate
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
     // ...省略
    }
    
    //扫描mapper
     @Configuration
    @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
        public MapperScannerRegistrarNotFoundConfiguration() {
        }

        public void afterPropertiesSet() {
            MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
        }
    }
     //扫描mapper
     public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
        private BeanFactory beanFactory;

        public AutoConfiguredMapperScannerRegistrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            if (!AutoConfigurationPackages.has(this.beanFactory)) {
                MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
            } else {
                MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
                    packages.forEach((pkg) -> {
                        MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
                    });
                }
                //生成MapperScannerConfigurer 
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
                builder.addPropertyValue("processPropertyPlaceHolders", true);
                // 注册扫描规则
                builder.addPropertyValue("annotationClass", Mapper.class);
                builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
                BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
                Stream.of(beanWrapper.getPropertyDescriptors()).filter((x) -> {
                    return x.getName().equals("lazyInitialization");
                }).findAny().ifPresent((x) -> {
                    builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
                });
                registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
            }
        }

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

推荐阅读更多精彩内容