Spring Cloud實(shí)戰(zhàn)小技巧(解決feign GET傳pojo逼泣、繼承接口方法參數(shù)注解等問(wèn)題)

源碼地址: https://github.com/charlesvhe/spring-cloud-practice/tree/refactor

spring cloud 實(shí)踐

項(xiàng)目結(jié)構(gòu)

config 配置中心

端口:8888,方便起見(jiàn)直接讀取配置文件碌宴,生產(chǎn)環(huán)境可以讀取git个曙。application-dev.properties為全局配置察郁。先啟動(dòng)配置中心伙菜,所有服務(wù)的配置(包括注冊(cè)中心的地址)均從配置中心讀取轩缤。

eureka 注冊(cè)中心

端口:8761,/metadata端點(diǎn)實(shí)現(xiàn)metadata信息配置贩绕。

zuul 網(wǎng)關(guān)

端口:8080典奉,演示解析token獲得label并放入header往后傳遞

core 框架核心包

核心jar包,所有微服務(wù)均引用該包丧叽,使用AutoConfig實(shí)現(xiàn)免配置,模擬生產(chǎn)環(huán)境下spring-cloud的使用公你。

starter spring cloud以及core框架依賴簡(jiǎn)化starter

provider 服務(wù)提供者

api 服務(wù)提供者SDK

強(qiáng)制服務(wù)消費(fèi)方只能通過(guò)服務(wù)提供者提供的SDK包(api項(xiàng)目)進(jìn)行調(diào)用踊淳,以便更加方便控制服務(wù)消費(fèi)方的行為:
提供可能的優(yōu)化,SDK直接讀取緩存(SDK只讀緩存陕靠,寫(xiě)緩存放還是在微服務(wù))迂尝。
統(tǒng)計(jì)有哪些服務(wù)消費(fèi)者,info端點(diǎn)顯示包依賴剪芥,通過(guò)注冊(cè)中心遍歷所有服務(wù)垄开。
方便dubbo平滑遷移,接口加feign注解税肪。

service 服務(wù)提供者微服務(wù)

端口:18090溉躲,服務(wù)提供者,無(wú)特殊邏輯益兄。

consumer 服務(wù)消費(fèi)者

端口:18090锻梳,調(diào)用服務(wù)提供者。

Restful API 設(shè)計(jì)規(guī)范

/版本/訪問(wèn)控制/域?qū)ο?br> /版本/訪問(wèn)控制/域?qū)ο?action/動(dòng)作

版本

版本為微服務(wù)級(jí)別净捅,也就是說(shuō)不存在一個(gè)API是v3版疑枯,其他API還只是v1版的問(wèn)題,要升級(jí)所有API版本一起升級(jí)蛔六,但是需要保證之前版本v1-v3還可以使用荆永。
原則上要兼容上一個(gè)版本 如果當(dāng)前是 /v3 則 /v2 要求可以正常使用 /v1 不做要求
如果無(wú)法兼容 需要通知所有服務(wù)消費(fèi)者 并約定版本火車(chē) 一起上線時(shí)間

小技巧

下面swagger注解就可以實(shí)現(xiàn)上述要求废亭,路徑中的{version}沒(méi)有限定,其實(shí)可以是任意內(nèi)容具钥,通過(guò)swagger文檔來(lái)進(jìn)行約定

// v1版api 即將廢棄
@ApiOperation("分頁(yè)查詢")
@RequestMapping(value = "/v1/pb/product", method = RequestMethod.GET)
@Deprecated
List<Product> selectAll(@RequestParam("offset") Integer offset, @RequestParam("limit") Integer limit);
 
//ProviderApiAutoConfig.CURRENT_VERSION="v2" 用來(lái)替換上面的v1版本
@ApiOperation("帶過(guò)濾條件和排序的分頁(yè)查詢")
@RequestMapping(value = "/{version}/pb/product", method = RequestMethod.GET)
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.CURRENT_VERSION, required = true)
Response<PageData<Product, Product>> selectAllGet(Page page);
  
// ProviderApiAutoConfig.COMPATIBLE_VERSION="v2,v1" swagger-ui上會(huì)顯示一個(gè)version的下拉框, 默認(rèn)v2
@ApiOperation(value = "帶過(guò)濾條件和排序的復(fù)雜分頁(yè)查詢")
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.COMPATIBLE_VERSION, required = true)
@RequestMapping(value = "/{version}/pb/product/action/search", method = RequestMethod.POST)
Response<PageData<Product, Product>> selectAll(@RequestBody Page page);

訪問(wèn)控制

用于在網(wǎng)關(guān)統(tǒng)一進(jìn)行訪問(wèn)控制, 訪問(wèn)控制分為
pb - public 所有請(qǐng)求均可訪問(wèn)
pt - protected 需要進(jìn)行token認(rèn)證通過(guò)后方可訪問(wèn)
df - default 網(wǎng)關(guān)進(jìn)行token認(rèn)證 并且請(qǐng)求參數(shù)和返回結(jié)果進(jìn)行加解密
pv - private 無(wú)法通過(guò)網(wǎng)關(guān)訪問(wèn) 只能微服務(wù)內(nèi)部調(diào)用
其他 同pv

action

無(wú)法用名詞+請(qǐng)求方法表述的可以擴(kuò)展為 /域?qū)ο?action/動(dòng)詞 必須為POST方法
例如 POST /users/action/login

問(wèn)題描述

主要由于使用了API(SDK)為了偷懶豆村,以及Restful API路徑中的版本帶來(lái)的一系列問(wèn)題。

spring MVC 不支持繼承接口中方法參數(shù)上的注解(支持繼承類(lèi)氓拼、方法上的注解)

API中為了方便你画,使用feign替代RestTemplate手動(dòng)調(diào)用。帶來(lái)的問(wèn)題:springMVC注解想偷懶桃漾,只在feign接口寫(xiě)一遍坏匪,然后實(shí)現(xiàn)類(lèi)繼承此接口即可。
例如:
feign接口定義如下

@FeignClient(ProviderApiAutoConfig.PLACE_HOLD_SERVICE_NAME)
public interface ProductService {
    // 為了讓spring mvc能夠正確綁定變量
    public class Page extends PageRequest<Product> {
    }
    @RequestMapping(value = "/{version}/pt/product", method = RequestMethod.POST)
    Response<Product> insert(@RequestBody Product product);
}

service實(shí)現(xiàn)類(lèi)方法參數(shù)必須再寫(xiě)一次@RequestBody注解撬统,方法上的@RequestMapping注解可以省略

@RestController
public class ProductServiceImpl implements ProductService {
    @Override
    public Response<Product> insert(@RequestBody Product product) {
        product.setId(1L);
        return new Response(product);
    }
}

解決辦法适滓,@Configuration配置類(lèi)添加如下代碼,擴(kuò)展spring默認(rèn)的ArgumentResolvers

public static MethodParameter interfaceMethodParameter(MethodParameter parameter, Class annotationType) {
    if (!parameter.hasParameterAnnotation(annotationType)) {
        for (Class<?> itf : parameter.getDeclaringClass().getInterfaces()) {
            try {
                Method method = itf.getMethod(parameter.getMethod().getName(), parameter.getMethod().getParameterTypes());
                MethodParameter itfParameter = new MethodParameter(method, parameter.getParameterIndex());
                if (itfParameter.hasParameterAnnotation(annotationType)) {
                    return itfParameter;
                }
            } catch (NoSuchMethodException e) {
                continue;
            }
        }
    }
    return parameter;
}
    
@PostConstruct
public void modifyArgumentResolvers() {
    List<HandlerMethodArgumentResolver> list = new ArrayList<>(adapter.getArgumentResolvers());

    list.add(0, new PathVariableMethodArgumentResolver() {  // PathVariable 支持接口注解
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return super.supportsParameter(interfaceMethodParameter(parameter, PathVariable.class));
        }

        @Override
        protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
            return super.createNamedValueInfo(interfaceMethodParameter(parameter, PathVariable.class));
        }
    });

    list.add(0, new RequestHeaderMethodArgumentResolver(beanFactory) {  // RequestHeader 支持接口注解
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return super.supportsParameter(interfaceMethodParameter(parameter, RequestHeader.class));
        }

        @Override
        protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
            return super.createNamedValueInfo(interfaceMethodParameter(parameter, RequestHeader.class));
        }
    });

    list.add(0, new ServletCookieValueMethodArgumentResolver(beanFactory) {  // CookieValue 支持接口注解
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return super.supportsParameter(interfaceMethodParameter(parameter, CookieValue.class));
        }

        @Override
        protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
            return super.createNamedValueInfo(interfaceMethodParameter(parameter, CookieValue.class));
        }
    });

    list.add(0, new RequestResponseBodyMethodProcessor(adapter.getMessageConverters()) {    // RequestBody 支持接口注解
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return super.supportsParameter(interfaceMethodParameter(parameter, RequestBody.class));
        }

        @Override
        protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {    // 支持@Valid驗(yàn)證
            super.validateIfApplicable(binder, interfaceMethodParameter(methodParam, Valid.class));
        }
    });

    // 修改ArgumentResolvers, 支持接口注解
    adapter.setArgumentResolvers(list);
}

swagger不支持繼承接口中方法參數(shù)上的注解(支持繼承類(lèi)恋追、方法上的注解)

沒(méi)有找到swagger自帶擴(kuò)展點(diǎn)能夠優(yōu)雅擴(kuò)展的方法凭迹,只好修改源碼了,下載springfox-spring-web 2.8.0 release源碼包苦囱。
添加pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-spring-web</artifactId>
    <version>2.8.0-charles</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <resource.delimiter>@</resource.delimiter>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <!--<version>2.0.0.RELEASE</version>-->
                <version>1.5.10.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.9.11</version>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
            <exclusions>
                <exclusion>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-spring-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</project>

添加ResolvedMethodParameterInterface繼承ResolvedMethodParameter

public class ResolvedMethodParameterInterface extends ResolvedMethodParameter {
    public ResolvedMethodParameterInterface(String paramName, MethodParameter methodParameter, ResolvedType parameterType) {
        this(methodParameter.getParameterIndex(),
                paramName,
                interfaceAnnotations(methodParameter),
                parameterType);
    }

    public ResolvedMethodParameterInterface(int parameterIndex, String defaultName, List<Annotation> annotations, ResolvedType parameterType) {
        super(parameterIndex, defaultName, annotations, parameterType);
    }

    public static List<Annotation> interfaceAnnotations(MethodParameter methodParameter) {
        List<Annotation> annotationList = new ArrayList<>();
        annotationList.addAll(Arrays.asList(methodParameter.getParameterAnnotations()));

        if (CollectionUtils.isEmpty(annotationList)) {
            for (Class<?> itf : methodParameter.getDeclaringClass().getInterfaces()) {
                try {
                    Method method = itf.getMethod(methodParameter.getMethod().getName(), methodParameter.getMethod().getParameterTypes());
                    MethodParameter itfParameter = new MethodParameter(method, methodParameter.getParameterIndex());
                    annotationList.addAll(Arrays.asList(itfParameter.getParameterAnnotations()));
                } catch (NoSuchMethodException e) {
                    continue;
                }
            }
        }

        return annotationList;
    }
}

修改HandlerMethodResolver類(lèi)line 181嗅绸,將ResolvedMethodParameter替換為ResolvedMethodParameterInterface,重新打包deploy撕彤,并在swagger相關(guān)依賴中強(qiáng)制指定修改后的版本鱼鸠。

<!-- swagger -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--擴(kuò)展swagger支持從接口獲得方法參數(shù)注解-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-spring-web</artifactId>
    <version>2.8.0-charles</version>
</dependency>

這樣就能夠順利生產(chǎn)swagger文檔啦。

feign不支持GET方法傳遞POJO

由于springMVC是支持GET方法直接綁定POJO的羹铅,只是feign實(shí)現(xiàn)并未覆蓋所有springMVC特效蚀狰,網(wǎng)上的很多變通方法都不是很好,要么是吧POJO拆散成一個(gè)一個(gè)單獨(dú)的屬性放在方法參數(shù)里职员,要么是把方法參數(shù)變成Map麻蹋,要么就是要違反HTTP協(xié)議,GET傳遞@RequestBody:
http://www.reibang.com/p/7ce46c0ebe9d
https://github.com/spring-cloud/spring-cloud-netflix/issues/1253
解決辦法焊切,使用feign攔截器:

public class CharlesRequestInterceptor implements RequestInterceptor {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        // feign 不支持 GET 方法傳 POJO, json body轉(zhuǎn)query
        if (template.method().equals("GET") && template.body() != null) {
            try {
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);

                Map<String, Collection<String>> queries = new HashMap<>();
                buildQuery(jsonNode, "", queries);
                template.queries(queries);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {   // 葉子節(jié)點(diǎn)
            if (jsonNode.isNull()) {
                return;
            }
            Collection<String> values = queries.get(path);
            if (null == values) {
                values = new ArrayList<>();
                queries.put(path, values);
            }
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()) {   // 數(shù)組節(jié)點(diǎn)
            Iterator<JsonNode> it = jsonNode.elements();
            while (it.hasNext()) {
                buildQuery(it.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
            while (it.hasNext()) {
                Map.Entry<String, JsonNode> entry = it.next();
                if (StringUtils.hasText(path)) {
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                } else {  // 根節(jié)點(diǎn)
                    buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
}

feign不支持路徑中的{version}

對(duì)于一個(gè)典型的Restful API定義如下:

@ApiOperation("帶過(guò)濾條件和排序的分頁(yè)查詢")
@RequestMapping(value = "/{version}/pb/product", method = RequestMethod.GET)
// 當(dāng)前版本新開(kāi)發(fā)api 隨微服務(wù)整體升級(jí) pt=protected 受保護(hù)的網(wǎng)關(guān)token驗(yàn)證合法可調(diào)用
@ApiImplicitParam(name = "version", paramType = "path", allowableValues = ProviderApiAutoConfig.CURRENT_VERSION, required = true)
Response<PageData<Product, Product>> selectAllGet(Page page);

我們并不關(guān)心路徑中的{version}扮授,因此方法參數(shù)中也沒(méi)有@PathVariable("version"),這個(gè)時(shí)候feign就傻了专肪,不知道路徑中的{version}應(yīng)該被替換成什么值糙箍。
解決辦法 使用自己的Contract替換SpringMvcContract,先將SpringMvcContract代碼復(fù)制過(guò)來(lái)牵祟,修改其中processAnnotationOnMethod方法的代碼深夯,從swagger注解中獲得{version}的值:

public class CharlesSpringMvcContract extends Contract.BaseContract
        implements ResourceLoaderAware {
    @Override
    protected void processAnnotationOnMethod(MethodMetadata data,
                                             Annotation methodAnnotation, Method method) {
        if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
                .annotationType().isAnnotationPresent(RequestMapping.class)) {
            return;
        }

        RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
        // HTTP Method
        RequestMethod[] methods = methodMapping.method();
        if (methods.length == 0) {
            methods = new RequestMethod[]{RequestMethod.GET};
        }
        checkOne(method, methods, "method");
        data.template().method(methods[0].name());

        // path
        checkAtMostOne(method, methodMapping.value(), "value");
        if (methodMapping.value().length > 0) {
            String pathValue = Util.emptyToNull(methodMapping.value()[0]);
            if (pathValue != null) {
                pathValue = resolve(pathValue);
                // Append path from @RequestMapping if value is present on method
                if (!pathValue.startsWith("/")
                        && !data.template().toString().endsWith("/")) {
                    pathValue = "/" + pathValue;
                }
                // 處理version
                if (pathValue.contains("/{version}/")) {
                    Set<ApiImplicitParam> apiImplicitParams = AnnotatedElementUtils.findAllMergedAnnotations(method, ApiImplicitParam.class);
                    for (ApiImplicitParam apiImplicitParam : apiImplicitParams) {
                        if ("version".equals(apiImplicitParam.name())) {
                            String version = apiImplicitParam.allowableValues().split(",")[0].trim();
                            pathValue = pathValue.replaceFirst("\\{version\\}", version);
                        }
                    }
                }
                data.template().append(pathValue);
            }
        }

        // produces
        parseProduces(data, method, methodMapping);

        // consumes
        parseConsumes(data, method, methodMapping);

        // headers
        parseHeaders(data, method, methodMapping);

        data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
    }
}

然后在自己的AutoConfig中聲明成spring的bean

@Configuration
@ConditionalOnClass(Feign.class)
public class FeignAutoConfig {
    @Bean
    public Contract charlesSpringMvcContract(ConversionService conversionService) {
        return new CharlesSpringMvcContract(Collections.emptyList(), conversionService);
    }

    @Bean
    public CharlesRequestInterceptor charlesRequestInterceptor(){
        return new CharlesRequestInterceptor();
    }
}

spring cloud以及feign小技巧

要求源碼對(duì)dev環(huán)境負(fù)責(zé)(application.properties里的配置均為dev環(huán)境地址)
配置中心dev環(huán)境全局配置添加:

# dev環(huán)境允許本地配置override配置中心配置
spring.cloud.config.overrideNone=true

在開(kāi)發(fā)過(guò)程中,dev環(huán)境會(huì)部署一套微服務(wù)到服務(wù)器上,本機(jī)開(kāi)發(fā)時(shí)為了不影響dev服務(wù)器咕晋,spring.application.name添加自己姓名為前綴雹拄,注冊(cè)到dev注冊(cè)中心也不影響dev服務(wù)器,配置中心指定名字掌呜,不再隨spring.application.name變化滓玖。
啟動(dòng)參數(shù)添加:

--spring.application.name=charles-framework-provider

bootstrap.properties配置模板

# 配置中心預(yù)留
spring.cloud.config.uri=https://config-mo.xxxx.com/config
# config.name 可以不指定 默認(rèn)為 spring.application.name
# 這里指定名字是為了兼容本機(jī)注冊(cè)到dev環(huán)境時(shí)通過(guò)修改spring.application.name=charles-framework-provider來(lái)區(qū)分微服務(wù)以免造成干擾
spring.cloud.config.name=framework-provider
spring.cloud.config.profile=${ENV:dev}
# 連不上配置中心不啟動(dòng)
spring.cloud.config.fail-fast=true

這樣本機(jī)開(kāi)發(fā)可以調(diào)用服務(wù)器上的微服務(wù),但是服務(wù)器上的微服務(wù)不會(huì)調(diào)用本機(jī)(服務(wù)名修改了)质蕉。
那么如果兩名研發(fā)需要互相調(diào)用聯(lián)調(diào)該如何處理呢势篡?
@FeignClient注解不要寫(xiě)死服務(wù)名,使用place holder模暗,類(lèi)似如下代碼:

@FeignClient(ProviderApiAutoConfig.PLACE_HOLD_SERVICE_NAME)
public interface ProductService {
}

ProviderApiAutoConfig的代碼如下:

@Configuration
// 自動(dòng)配置feign掃描包 使用方零配置 微服務(wù)本身不加載自己的API中的feign
@ConditionalOnExpression("#{!environment['spring.application.name'].endsWith('" + ProviderApiAutoConfig.SERVICE_NAME + "')}")
@EnableFeignClients(basePackages = "com.github.charlesvhe.springcloud.practice.provider")
public class ProviderApiAutoConfig {
    public static final String SERVICE_NAME = "provider";
    // FeignClient 用placeholder可以方便的進(jìn)行內(nèi)部調(diào)用 配置key為charles.service.服務(wù)名
    // 配置charles.service.framework-provider=charles-framework-provider來(lái)調(diào)用charles-framework-provider服務(wù)
    public static final String PLACE_HOLD_SERVICE_NAME = "${charles.service." + SERVICE_NAME + ":" + SERVICE_NAME + "}";

    public static final String CURRENT_VERSION = "v2";
    public static final String COMPATIBLE_VERSION = "v2,v1";

}

這樣在啟動(dòng)參數(shù)中指定哪個(gè)服務(wù)想調(diào)哪個(gè)人的實(shí)例都可以禁悠。同時(shí)處理了service依賴api時(shí),不會(huì)注入feign的api實(shí)例兑宇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末碍侦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子隶糕,更是在濱河造成了極大的恐慌瓷产,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枚驻,死亡現(xiàn)場(chǎng)離奇詭異濒旦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)再登,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)疤估,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人霎冯,你說(shuō)我怎么就攤上這事〕伲” “怎么了沈撞?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)雕什。 經(jīng)常有香客問(wèn)我缠俺,道長(zhǎng),這世上最難降的妖魔是什么贷岸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任壹士,我火速辦了婚禮,結(jié)果婚禮上偿警,老公的妹妹穿的比我還像新娘躏救。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布盒使。 她就那樣靜靜地躺著崩掘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪少办。 梳的紋絲不亂的頭發(fā)上苞慢,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音英妓,去河邊找鬼挽放。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蔓纠,可吹牛的內(nèi)容都是我干的辑畦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贺纲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼航闺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起猴誊,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤潦刃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后懈叹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體乖杠,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年澄成,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胧洒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡墨状,死狀恐怖卫漫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肾砂,我是刑警寧澤列赎,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站镐确,受9級(jí)特大地震影響包吝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜源葫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一诗越、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧息堂,春花似錦嚷狞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)褂乍。三九已至,卻和暖如春即硼,著一層夾襖步出監(jiān)牢的瞬間逃片,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工只酥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褥实,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓裂允,卻偏偏與公主長(zhǎng)得像损离,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绝编,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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