为什么一个@LoadBalanced注解就可以实现RestTemplate的负载均衡?

  • SpringCloud 版本Hoxton.SR1
  • SpringBoot 版本2.2.1.RELEASE
  • 本文适用于对SpringBoot有一定基础的人,主要讲解RestTemplate的工作过程。讲解方式:场景驱动
  • 关键词 :RestTemplate 配置使用演示及代码示例、负载均衡过程源码分析
  • 上一篇 SpringCloud 服务注册与发现 源码分析(二)分析了 Eureka Client端和负载均衡客户端的相关组件及装配过程(本篇的铺垫,因为实现复负载均衡的一些底层组件都是这里实现创建的,建议两篇结合着看),本篇将会介绍RestTemplate的工作过程以及负载均衡的实现。

1. RestTemplate

  • 此组件是Spring中用来做远程调用而封装的一个调用模板:包括但不限于 GET、POST、PUT等简单易用的操作方法
1.1 使用RestTemplate实现负载均衡的先决条件

准备工作:一个Eureka消费者,两个Eureka提供者,两个Eureka服务器做服务端集群

1.1.1 Maven中引入如下依赖,版本自适应( 此依赖包中默认包含RestTemplate核心模块调用类 )
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
1.1.2 创建RestTemplate模板调用类,创建的方式多种多样,常用的就是基于@Configuration + @Bean的方式
图1 配置负载均衡客户端RestTemplate
图3 服务器2
  • 到此前期准备工作已经完成,我们可以发现两个提供者provider1和provider2均已注册到服务端了,由于作者本机演示,所以Port设置的不一样而IP地址是一样的,正常线上应用都是IP不一样而Port一样,道理都是一样的。下面我们看一下浏览器访问消费者提供的Web接口,会如何负载到服务端provider1和provider2
1.2 如何使用RestTemplate ?
1.2.1 前面我们也说了,RestTemplate中提供了若干个调用方法,使用者可以根据自己的需求选择性调用,此处作者写了个简单的Web接口,接口中调用RestTemplate的getForObject方法,需要注意的是:要想实现客户端的负载均衡,调用的URL中要使用应用名spring.application.name对应的值。如图:
图4 comsumer中提供的接口
  • 下面我们访问消费端的uri为/loadBalance的接口地址

    图5 请求消费端提供的接口

    第一次请求,控制台输出
    图6 第一次控制台输出日志

    第二次请求,控制台输出
    图7 第二次控制台输出日志

    ......

  • 我们可以发现,一次provider1然后一次provider2,轮询的请求这两个服务提供者

  • 然后再访问 消费端的uri为/noLoadBalance的接口地址

    图8 访问/noLoadBalance的接口
    并且后端消费者服务直接报错:
    图9 comsumer应用后端

1.2.2 而图1(作者暂时未找到在简书中如何实现页内跳转,此处还请读者向上翻到图1,若是有读者知道烦请评论区留言哦~)中的配置,我们观察两个RestTemplate模板类的唯一不同之处就是第一个创建过程多了一个@LoadBalanced注解,此时我们可以得出结论就是带有@LoadBalanced注解的RestTemplate具有负载均衡的能力,得出这个结论之后实际开发上已经够用了,但是为了做到 知根知底,我们将剖析负载均衡背后的原理!

代码下载地址:https://github.com/LiujunjieALiling/spring-cloud-netflix.git

1.2.3 在此,我们先回答上一篇中的问题1: 为什么只会注入带有@LoadBalanced注解的RestTemplate实例,而普通的RestTemplate不会注入进去?
  • 由于两个RestTemplate得不同之处仅仅是一个@LoadBalanced得区别,我们就从此注解下手:
    图10 LoadBalanced注解
    从注释上可以看出用来标记RestTemplate得bean( 使用LoadBalancerClient来配置),然后全局搜了一下并没有发现有哪个地方会处理此@LoadBalanced注解,此时心中有点产生疑惑(难道这个注解也不按套路来吗?为什么没有地方来解析或者拦截呢?)。然后全局查看此工程中使用到此注解得地方仅有三处:
    图11 三处引用到@LoadBalanced得地方

    第①处:LoadBalancerAutoConfiguration类(上一篇已经分析过)
    第②处:AsyncLoadBalancerAutoConfiguration类(上一篇也分析过)
    第③处:MyRestTemplateConfiguration类(当前项目中用来配置RestTemplate得配置类)我们也可以看出来@LoadBalanced注解可以修饰在方法上、属性上、参数上而且还被@Qualifier修饰。从目前得突破口来看只有此注解比较可疑。那我们就顺着@Qualifier注解得解析看起:
    QualifierAnnotationAutowireCandidateResolver得类关系图
    ,对Spring注解解析相关类比较熟悉得就可以快速找到解析@Qualifier得解析器,而此类创建时机是当调用org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry, java.lang.Object)方法( 此方法得调用在启动类解析时,不清楚得可以看一下作者得SpringBoot启动过程源码分析系列文章 )创建ContextAnnotationAutowireCandidateResolver( 此类继承了QualifierAnnotationAutowireCandidateResolver )解析器的时候:
    图12 AnnotationConfigUtils#registerAnnotationConfigProcessors方法
    ,并且QualifierAnnotationAutowireCandidateResolver 得无参构造器中会将javax.inject.Qualifierorg.springframework.beans.factory.annotation.Qualifier注解都缓存起来以供属性注入时使用:
    图13 无参构造器
  • 当属性注入得时候( 生命周期其中一个关键点)调用 org.springframework.beans.factory.support.DefaultListableBeanFactory#isAutowireCandidate(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, org.springframework.beans.factory.config.DependencyDescriptor, org.springframework.beans.factory.support.AutowireCandidateResolver)方法时,由于上下文中得解析器是上文说得ContextAnnotationAutowireCandidateResolver,而isAutowireCandidate继承自父类,所以我们直接从父类QualifierAnnotationAutowireCandidateResolverisAutowireCandidate方法看起(对SpringBoot启动过程与Spring容器解析过程不了解得可移步作者得SpringBoot启动解析系列文章,此处不再深入):
    @Override
    public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
        boolean match = super.isAutowireCandidate(bdHolder, descriptor);
        if (match) {
            // 校验依赖得属性中得注解与当前bean定义中是否匹配(很关键得步骤)
            match = checkQualifiers(bdHolder, descriptor.getAnnotations());
            if (match) {
                MethodParameter methodParam = descriptor.getMethodParameter();
                if (methodParam != null) {
                    Method method = methodParam.getMethod();
                    if (method == null || void.class == method.getReturnType()) {
                        match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
                    }
                }
            }
        }
        return match;
    }

    /**
     * Match the given qualifier annotations against the candidate bean definition.
     */
    protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
        if (ObjectUtils.isEmpty(annotationsToSearch)) {
            return true;
        }
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();
        for (Annotation annotation : annotationsToSearch) {
            Class<? extends Annotation> type = annotation.annotationType();
            boolean checkMeta = true;
            boolean fallbackToMeta = false;
            if (isQualifier(type)) {
                if (!checkQualifier(bdHolder, annotation, typeConverter)) {
                    fallbackToMeta = true;
                }
                else {
                    checkMeta = false;
                }
            }
            if (checkMeta) {
                boolean foundMeta = false;
                for (Annotation metaAnn : type.getAnnotations()) {
                    Class<? extends Annotation> metaType = metaAnn.annotationType();
                    if (isQualifier(metaType)) {
                        foundMeta = true;
                        // Only accept fallback match if @Qualifier annotation has a value...
                        // Otherwise it is just a marker for a custom qualifier annotation.
                        if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
                                !checkQualifier(bdHolder, metaAnn, typeConverter)) {
                            return false;
                        }
                    }
                }
                if (fallbackToMeta && !foundMeta) {
                    return false;
                }
            }
        }
        return true;
    }
    /**
     * Match the given qualifier annotation against the candidate bean definition.
     */
    protected boolean checkQualifier(
            BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {

        Class<? extends Annotation> type = annotation.annotationType();
        RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();
        // 此处我们没有对bean定义指定解析器,所以获取得为空
        AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
        if (qualifier == null) {
            // 同理,也为空
            qualifier = bd.getQualifier(ClassUtils.getShortName(type));
        }
        if (qualifier == null) {
            //  正常情况下创建得bean定义也没有设置此属性,所以返回也为null
            Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
            // Then, check annotation on factory method, if applicable
            if (targetAnnotation == null) {
                // 此处针对于@Bean 方式创建得bean会设置一个FactoryMethod属性,此属性保存得就是配置类中得创建方法,
                // 此处根据type(@Qualifier)获取到得目标注解就是@LoadBalanced
                targetAnnotation = getFactoryMethodAnnotation(bd, type);
            }
            if (targetAnnotation == null) {
                RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
                if (dbd != null) {
                    targetAnnotation = getFactoryMethodAnnotation(dbd, type);
                }
            }
            if (targetAnnotation == null) {
                // Look for matching annotation on the target class
                if (getBeanFactory() != null) {
                    try {
                        Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
                        if (beanType != null) {
                            targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
                        }
                    }
                    catch (NoSuchBeanDefinitionException ex) {
                        // Not the usual case - simply forget about the type check...
                    }
                }
                if (targetAnnotation == null && bd.hasBeanClass()) {
                    targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type);
                }
            }
            //  此处获取得targetAnnotation注解为@LoadBalanced,并且annotation也是,则会直接返回匹配成功,注入属性
            if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
                return true;
            }
        }

        Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
        if (attributes.isEmpty() && qualifier == null) {
            // If no attributes, the qualifier must be present
            return false;
        }
        for (Map.Entry<String, Object> entry : attributes.entrySet()) {
            String attributeName = entry.getKey();
            Object expectedValue = entry.getValue();
            Object actualValue = null;
            // Check qualifier first
            if (qualifier != null) {
                actualValue = qualifier.getAttribute(attributeName);
            }
            if (actualValue == null) {
                // Fall back on bean definition attribute
                actualValue = bd.getAttribute(attributeName);
            }
            if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
                    expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)) {
                // Fall back on bean name (or alias) match
                continue;
            }
            if (actualValue == null && qualifier != null) {
                // Fall back on default, but only if the qualifier is present
                actualValue = AnnotationUtils.getDefaultValue(annotation, attributeName);
            }
            if (actualValue != null) {
                actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass());
            }
            if (!expectedValue.equals(actualValue)) {
                return false;
            }
        }
        return true;
    }

上述代码中判断逻辑是 isAutowireCandidate() -> checkQualifiers() -> checkQualifier(),首先获取候选bean得所有注解,然后循环调用checkQualifier方法判断这些注解是否符合要求,而判断是否符合要求得核心代码是在checkQualifier方法中通过上述代码分析得知:通过@Qualifier注解只会过滤出带有@LoadBalanced修饰得RestTemplate,不带有此注解得RestTemplate不会注入到属性集合中。换句话说就是:只要你注入得属性(容器bean)被带有@Qualifier得自定义注解修饰(即使不叫@LoadBalanced),那么就可以将容器bean注入到属性中。此处就不演示了,代码都在GitHub上可以下载

1.2.4 解决完第一个问题之后,接下来我们就开始分析RestTemplate怎么就可以实现均衡的调用服务提供者呢? 开始我们今天另一个主题( 手撕RestTemplate负载均衡过程源码,上一篇中的问题2
2.1 RestTemplate实现负载均衡的底层实现
RestTemplate类关系图
  • 还是老套路,我们使用场景驱动的方式从前到后梳理调用链。首先,我们顺着入口方法getForObject
    图14 RestTemplate的getForObject重载之一
    我们可以看到最终会执行execute方法,基本上所有的直接使用方法调用流程都是 getXXX/postXXX/putXXX 等方法 -> execute(可省略) -> doExecute(真正处理调用过程的实现),所以我们看一下doExecute方法的实现,大致分为以下几部分:
    图15 RestTemplate的doExecute方法

    ① 处createRequest(url, method)方法:通过注释可以得知是通过ClientHttpRequestFactory工厂来创建ClientHttpRequest实例的
    图16 HttpAccessor的createRequest方法
    首先调用获取getRequestFactory方法获取请求工厂实现,而此方法被子类InterceptingHttpAccessor覆写
    图17 InterceptingHttpAccessor中的getRequestFactory方法

    我们通过注释可以得知会覆写父类HtppAccessor的getRequestFactory方法来创建InterceptingClientHttpRequestFactory类型的请求工厂。首先获取当前对象的interceptors列表(当前只有一个RetryLoadBalancerInterceptor,拦截器的设置时机已经上篇文章说明了),所以我们此处的拦截器不为空,将会创建一个InterceptingClientHttpRequestFactory 类型的请求工厂,并且创建InterceptingClientHttpRequestFactory的时候同时会包装一个SimpleClientHttpRequestFactory类型的工厂(调用super.getRequestFactory()在父类HttpAccessor中创建):
    InterceptingClientHttpRequestFactory 的类关系图
    然后调用请求工厂InterceptingClientHttpRequestFactory 的createRequest方法:通过类关系图我们可以得知此工厂继承自AbstractClientHttpRequestFactoryWrapper,所以会调用父类AbstractClientHttpRequestFactoryWrapper的创建请求方法:
    图18 AbstractClientHttpRequestFactoryWrapper的createRequest方法
    ,然后调用重载的模板方法,最终调用子类的实现创建InterceptingClientHttpRequest类型的请求:
    图19 InterceptingClientHttpRequestFactory的createRequest方法

② 处requestCallback.doWithRequest(request)方法:判断若是requestCallback不为空,则调用其doWithRequest方法对请求进行处理,一般情况下我们调用get/post..方法时不会指定这个requestCallback参数,所以会使用默认的AcceptHeaderRequestCallback实例,而此实例的作用就是像请求头中添加支持的MediaType类型。

InterceptingClientHttpRequest 的类关系图

③ 处request.execute()方法:调用InterceptingClientHttpRequest 的execute方法,此execute方法继承自AbstractClientHttpRequest抽象类:

图20 AbstractClientHttpRequest中的方法
方法中首先会去断言请求没有被执行,然后调用模板方法 executeInternal(this.headers),此模板方法得实现是在抽象类AbstractBufferingClientHttpRequest中:
图21 抽象类AbstractBufferingClientHttpRequest的方法实现
首先获取字节缓冲输出流中的数据,然后调用内部模板方法 executeInternal(headers, bytes),方法实现就是在创建的请求实例InterceptingClientHttpRequest 中:
图22 InterceptingClientHttpRequest中的方法
创建 InterceptingRequestExecution请求拦截执行器,紧接着调用执行器得execute方法。很显然,由于我们有拦截器(RetryLoadBalancerInterceptor),所以会执行拦截器得interceptor方法,如图:
图23 RetryLoadBalancerInterceptor得intercept方法

①首先获取原始请求URL,然后获取URL中得主机名(此处是provider
②根据负载均衡重试工厂获取重试策略:此处得重试工厂是RibbonLoadBalancedRetryFactory(上一篇文章说过),创建得策略是RibbonLoadBalancedRetryPolicy
③然后再创建重试调用模板类RetryTemplate,此模板类中会设置一些重试监听器、重试策略等,最重要得一点是此模板类似于RestTemplate(由于是重试得模板调用类,所以会多一些重试策略(此处设置得是InterceptorRetryPolicy)、重试机制、回调监听等等,看到这里我们发现刚开始执行RestTemplate得逻辑已经切换到RetryTemplate了
④然后调用RetryTemplate得execute方法,再调用org.springframework.retry.support.RetryTemplate#doExecute,此方法中主要步骤是首先通过 重试策略InterceptorRetryPolicy获取重试上下文(LoadBalancedRetryContext,获取得同时会通过InterceptorRetryPolicy调用org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy#canRetry方法选择服务实例,此处创建得服务实例是 RibbonServer 类型,具体是通过org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#getServer(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)方法,调用com.netflix.loadbalancer.BaseLoadBalancer#chooseServer方法,此选择方法是负载均衡得核心实现,调用默认规则RoundRobinRule得com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)方法选择实例,默认得轮询规则可以通过set方法修改。然后将实例设置到上下文中),接着执行回调逻辑(函数式编程,看起来不是很直观,就是context ->{} 里面得逻辑
注:总的来说逻辑之前得桥梁就是LoadBalancedRetryContext,会向此重试上下文中设置一系列得参数供负载均衡得时候使用
⑤由于上面已经获取到服务实例( RibbonServer),此处不会执行
⑥调用RibbonLoadBalancerClient得execute方法,方法如下:
图24 RibbonLoadBalancerClient得execute方法
方法中首先获取RibbonServer实例,然后根据服务Id从SpringClientFactory中获取服务均衡上下文,创建RibbonStatsRecorder用于对调用返回结果做记录(记录服务状态ServerStats),此处得返回值是通过LoadBalancerRequest函数接口调用得方式(apply方法)根据服务实例RibbonServer获取得。函数是this.requestFactory.createRequest(request, body, execution)
图25 LoadBalancerRequestFactory得createRequest方法
根据服务实例创建ServiceRequestWrapper请求,通过LoadBalancerRequestTransformer允许用户对HttpRequest做修改,最后请求执行器InterceptingRequestExecution得execute方法(我们发现又回到图22中得execute方法,类似于递归处理拦截器得拦截方法。此处责任链模式得设计通过一个迭代器来实现很值得借鉴与学习。但是由于我们只有一个拦截器,所以此处得this.iterator.hasNext()为false,会进入else逻辑

图22中已经截图,此处大致说一下里面得逻辑:首先获取ServiceRequestWrapper得请求方法,然后通过成员变量ClientHttpRequestFactory得实现SimpleClientHttpRequestFactory调用createRequest方法创建HTTP请求(此处得createRequest方法要玩真的了😂,前面很多类都有这个方法,我们可以把它想象成在做负载均衡得一些关键性步骤),创建得实例是SimpleBufferingClientHttpRequest,然后完善请求头参数,如果请求body不为空,将body写入到SimpleBufferingClientHttpRequest委派请求得输出流中,最后调用此请求实例得execute方法,此执行方法得execute调用链 :org.springframework.http.client.AbstractClientHttpRequest#execute -> org.springframework.http.client.AbstractBufferingClientHttpRequest#executeInternal(org.springframework.http.HttpHeaders) -> org.springframework.http.client.SimpleBufferingClientHttpRequest#executeInternal(HttpHeaders headers, byte[] bytes),此处创建得是原生得java.net.HttpURLConnection,到此负载均衡得调用过程基本分析完毕。由于使用了大量得函数式编程,请求拦截得过程与代码得编写理解起来还是有一定难度得,不过其中有很多地方得逻辑稍显繁琐,让会让阅读起来有一定得理解成本。下面我们来做个简单得总结:

1. 调用RestTemplate得execute方法,由于有拦截器,所以会创建InterceptingClientHttpRequestFactory,请求工厂中会保存设置得拦截器集合。然后调用工厂得createRequest方法创建InterceptingClientHttpRequest(可以理解为负载均衡就是通过拦截器来实现得,所以后面创建得类基本上都会带有Interceptor字样),会保存一份原始得请求工厂(SimpleClientHttpRequestFactory)、拦截器列表、请求方法、请求URI。
2. 调用InterceptingClientHttpRequest得execute方法,此处涉及到大量得模板设计,最终会调用InterceptingRequestExecution得execute方法。此execute方法中会通过迭代器得方式实现拦截器得责任链调用,直到执行最后一个拦截器(此处得拦截器可以作为扩展钩子)。执行得过程中会创建重试策略、将RestTemplate得执行逻辑切换到执行RetryTemplate得逻辑,获取服务实例:获取实例得默认规则是轮询,通过com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)方法选取实例(此处就是负载均衡得默认核心实现,当然开发者可以自己实现ILoadBalancer接口实现自己得负载均衡算法,作者没有对负载均衡得选择做大量篇幅介绍,感兴趣得读者可以自己看看
3. 选择完要调用得服务之后,通过JDK原生得java.net.HttpURLConnection处理HTTP请求,得到响应。

  • 介于篇幅原因,本文着重解决了 上篇提到问题1与问题2,然后对负载均衡得调用链做了一个全面得分析,但是对负载均衡得实例选择那块得策略与逻辑处理并没有大幅度展开,感兴趣得读者可以根据文章作者提到得一些关键点步骤与总结再进行一次梳理,这样会加深印象(并没有要求代码得每一处都记住,也是不可能得,主要记住整个逻辑,然后记住这些调用链中用到得类与一些设计巧妙得地方,然后转为己用)。
  1. ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码获得的知识,难免会有疏忽!
  2. 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
  3. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,585评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,283评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,760评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,461评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,280评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,268评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,656评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,322评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,629评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,691评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,445评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,299评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,694评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,982评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,244评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,642评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,829评论 2 335