适配器模式-文件服务

前言

适配器模式是把一个类的接口变换成客户端所期待的另一中接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
用电器做例子,笔记本电脑的插头一般都是三相的,即除了阳极、阴极外,还有一个地极。而有些地方的电源插座却只有两极,没有地极。电源插座与笔记本电脑的电源插头不匹配使得笔记本电脑无法使用。这时候一个三相到两相的转换器(适配器)就能解决此问题,而这正像是本模式所做的事情(这个例子是我抄来的)。

适配器模式的类型

关于适配器模式的类型这个小节太过枯燥无味,概念定义生搬硬套太过牵强令人难以理解,虽然基本是我复制粘贴的过来的但是仍一度让我恼火到不想继续写下去。建议大家跳过这一部分直接看下面具体的业务场景。

适配器模式有类的适配器和对象的适配器两种不同的形式。

  • 类适配器
    image.png

类的适配器模式把适配的类的API转换成目标类的API,假设有一个接口A和一个具体的类B,B不是A的实现类,现在客户端希望在一个A的是实现类C中使用B的功能,最简单的方法就是使C实现A的同时又继承B。这其中涉及到了三个角色

  • 目标角色(target):就是所期待得到的目标接口A

  • 源角色(Adapee):现在要适配的类B

  • 适配器角色:
    适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以 是接口,而必 须是具体类,在本例中C就是适配器

  • 对象适配器
    与类的适配器模式一样,对象的适配器模式把被适配的类的API转换成为目标类的API,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。

    image.png

使用适配器模式实现文件服务

最近一个月都在做公司的项目迁移,把集团的项目(包括服务器)迁移到上海这边,我们原有的框架下有一套自研的文件服务实现,是将文件内容byte数组及元数据存放在mongo中,这个自研的文件服务有些年头了而且引入了这个文件服务之后在项目启动时会自动加载mongo连接(无论是否实际使用),重度依赖基础环境,所以趁着这次项目迁移准备完全移除自研的文件服务使用第三方文件服务实现,最终选用了阿里云oss作为第三方文件服务实现,以下就用oss表示新的文件服务。
因为要考虑到历史文件的迁移,文件服务迁移主要分成了两个阶段:
一、文件双写:
增量数据(新上传的文件)双写,同时上传到自研文件服务和oss,同一个文件在自研文件服务和oss中的文件id是一致的,读取文件仍然从自研文件服务中读取(因为存量数据尚未同步至oss中)。
二、存量数据同步:
因为有很多文件id是作为历史数据(存量数据)保存在mysql中的各个业务表中的(比如营业执照、合同等),为了确保切换到oss后历史文件能够正确显示,最简单的方法就是将自研文件服务中的存量数据同步到oss中并且确保文件id一致,这样同步完成之后就能够无缝切换到oss。

在文件双写阶段要实现的目标是 使用新的文件服务依赖替代原有的自研框架的文件服务依赖,对外只暴露新的文件服务接口定义的接口和类(接口和类除了包路径与自研服务不同之外,接口名类名和参数名都完全相同,这样各个依赖方只需要在导包处把包路径替换即可无需改动代码逻辑),所以这个时候就需要我们将自研的文件服务和新的文件服务做一个适配。
以下是代码部分:
自研文件服务:

package com.cube.dp.adapter.fs.custom;

import com.cube.dp.adapter.fs.custom.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:32
 * <p>
 * 自研文件服务接口
 */
public interface IFileOperationService {


    /**
     * 上传文件
     *
     * @param dto 参数
     * @return 文件id
     */
    String upload(FileUploadDto dto);

    /**
     * 流式上传文件
     *
     * @param streamDto 参数
     * @return 文件id
     */
    String upload4Stream(FileUploadForStreamDto streamDto);

    /**
     * 下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadDto download(String fileId);

    /**
     * 流式下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadForStreamDto download4Stream(String fileId);
}

自研文件服务实现类

package com.cube.dp.adapter.fs.custom;

import com.cube.dp.adapter.fs.custom.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadDto;
import com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:48
 * <p>
 * 自研文件服务实现
 */
public class CustomFileOperationServiceImpl implements IFileOperationService {

    @Override
    public String upload(FileUploadDto dto) {
        System.out.println("自研文件服务上传文件...");
        return null;
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        System.out.println("自研文件服务流式上传文件...");
        return null;
    }

    @Override
    public FileDownloadDto download(String fileId) {
        System.out.println("自研文件服务下载文件...");
        return null;
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        System.out.println("自研文件服务流式下载文件...");
        return null;
    }
}

第三方文件服务:

package com.cube.dp.adapter.fs.third;

import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:32
 * <p>
 * 第三方文件服务接口
 */
public interface IThirdPartyFileOperationService {


    /**
     * 上传文件
     *
     * @param dto 参数
     * @return 文件id
     */
    String upload(FileUploadDto dto);

    /**
     * 流式上传文件
     *
     * @param streamDto 参数
     * @return 文件id
     */
    String upload4Stream(FileUploadForStreamDto streamDto);

    /**
     * 下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadDto download(String fileId);

    /**
     * 流式下载文件
     *
     * @param fileId 文件id
     * @return 结果
     */
    FileDownloadForStreamDto download4Stream(String fileId);
}

oss文件服务实现

package com.cube.dp.adapter.fs.third;

import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:48
 * <p>
 * 自研文件服务实现
 */
public class OssFileOperationServiceImpl implements IThirdPartyFileOperationService {

    @Override
    public String upload(FileUploadDto dto) {
        System.out.println("oss文件服务上传文件...");
        return null;
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        System.out.println("oss文件服务流式上传文件...");
        return null;
    }

    @Override
    public FileDownloadDto download(String fileId) {
        System.out.println("oss文件服务下载文件...");
        return null;
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        System.out.println("oss文件服务流式下载文件...");
        return null;
    }
}

看一下两者的目录结构:


image.png

这里保证了自研文件服务和第三方文件服务的类名、接口完全一致,原因上面已经说过了。
这里因为是为了方便演示放在同一个项目下所以两者的包路径差别不大,实际上两者的包路径可以说是没有任何相似之处。

再复述一下我们在双写阶段的目标:文件双写+对外暴露第三方文件服务定义的接口和方法
,提供如下适配器

package com.cube.dp.adapter;


import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 15:28
 * <p>
 * 文件操作适配器
 * 将自研文件服务与第三方文件服务适配
 */
public interface FileOperationAdaptee extends IThirdPartyFileOperationService {

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileUploadDto to(FileUploadDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileUploadDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileUploadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto to(FileUploadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileUploadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }


    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileDownloadDto to(FileDownloadDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileDownloadDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileDownloadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default FileDownloadDto to(com.cube.dp.adapter.fs.custom.dto.FileDownloadDto dto) {
        if (dto == null) {
            return null;
        }
        FileDownloadDto fileUploadDto = new FileDownloadDto();

        fileUploadDto.setFileName(dto.getFileName());
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto to(FileDownloadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto fileUploadDto = new com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

    /**
     * 转换
     *
     * @param dto 参数
     * @return 结果
     */
    default FileDownloadForStreamDto to(com.cube.dp.adapter.fs.custom.dto.FileDownloadForStreamDto dto) {
        if (dto == null) {
            return null;
        }
        FileDownloadForStreamDto fileUploadDto = new FileDownloadForStreamDto();

        fileUploadDto.setFileName(dto.getFileName());
        //输入流一般是不能重复读取的,实际上应该采用byte数组拷贝或者转换成可重复读去的输入流,本案例只是为了演示适配器模式的用法,不严格考虑具体的实现细节了
        fileUploadDto.setFileContent(dto.getFileContent());
        fileUploadDto.setContentType(dto.getContentType());
        fileUploadDto.setMetadata(dto.getMetadata());

        return fileUploadDto;
    }

}

该适配器首先继承了第三方文件服务的定义的接口IThirdPartyFileOperationService,提供了一些将新旧文件服务参数相互转换的默认方法。
再提供一个抽象的双写适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 16:52
 * <p>
 * 抽象文件双写适配器,文件上传时,会先将文件上传只自研文件服务实现,然后再将该文件上传至第三方文件服务实现,
 * 同一个文件上传后在自研文件服务和第三方文件服务内的文件id是一致的
 */
public abstract class AbstractFileDoubleWriteOperationAdaptor implements FileOperationAdaptee {

    private final IFileOperationService fileOperationService;

    private final IThirdPartyFileOperationService thirdPartyFileOperationService;

    public AbstractFileDoubleWriteOperationAdaptor(IFileOperationService fileOperationService,
                                                   IThirdPartyFileOperationService thirdPartyFileOperationService) {
        this.fileOperationService = fileOperationService;
        this.thirdPartyFileOperationService = thirdPartyFileOperationService;
    }

    @Override
    public String upload(FileUploadDto dto) {
        String fileId = fileOperationService.upload(to(dto));
        //使用上传至自研文件服务的文件id作为第三方文件服务上传文件时的key,这样就能够报证同一个文件在两个文件服务的文件id是一致的,这个逻辑这里就不写了
        return thirdPartyFileOperationService.upload(dto);
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        String fileId = fileOperationService.upload4Stream(to(streamDto));
        //使用上传至自研文件服务的文件id作为第三方文件服务上传文件时的key,这样就能够报证同一个文件在两个文件服务的文件id是一致的,这个逻辑这里就不写了
        return thirdPartyFileOperationService.upload4Stream(streamDto);
    }

    public IFileOperationService getFileOperationService() {
        return fileOperationService;
    }

    public IThirdPartyFileOperationService getThirdPartyFileOperationService() {
        return thirdPartyFileOperationService;
    }
}

再提供一个文件上传时双写且从自研文件服务中读取文件的适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 17:08
 * <p>
 * 上传时双写,读取时从自研文件服务中读取的适配器
 */
public class DoubleWriteAndReadCustomOperationAdaptor extends AbstractFileDoubleWriteOperationAdaptor {

    public DoubleWriteAndReadCustomOperationAdaptor(IFileOperationService fileOperationService,
                                                    IThirdPartyFileOperationService thirdPartyFileOperationService) {
        super(fileOperationService, thirdPartyFileOperationService);
    }

    @Override
    public FileDownloadDto download(String fileId) {
        return to(getFileOperationService().download(fileId));
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        return to(getFileOperationService().download4Stream(fileId));
    }
}

这里不提供抽象的文件双写适配器AbstractFileDoubleWriteOperationAdaptor直接在DoubleWriteAndReadCustomOperationAdaptor实现双写逻辑也是可以的(因为实际上只能从自研文件服务中读取,这样才能确保增量数据和存量数据都能读取成功)

再提供一个获取文件服务的工厂

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.CustomFileOperationServiceImpl;
import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.OssFileOperationServiceImpl;

/**
 * @author litb
 * @date 2022/5/21 17:12
 * <p>
 * 文件服务工厂
 */
public class FileOperationFactory {


    public static IThirdPartyFileOperationService getDefault() {
        return getInstance(EnumFileOperationType.DOUBLE_WRITE_AND_READ_FROM_CUSTOM);
    }

    /**
     * 获取对应的文件服务实例
     *
     * @param operationType 类型
     * @return 实例
     */
    public static IThirdPartyFileOperationService getInstance(EnumFileOperationType operationType) {
        switch (operationType) {
            case OSS:
                return new OssFileOperationServiceImpl();
            case CUSTOM:
                return new CustomFileOperationAdaptor(new CustomFileOperationServiceImpl());
            case DOUBLE_WRITE_AND_READ_FROM_CUSTOM:
                return new DoubleWriteAndReadCustomOperationAdaptor(new CustomFileOperationServiceImpl(),
                        new OssFileOperationServiceImpl());
            default:
                throw new IllegalArgumentException("storageType is not support");
        }
    }
}

各个依赖方可以通过getDefault方法获取默认的文件服务实例,在双写阶段提供的是上传时双写+从自研文件服务中读取的实现,在第二阶段存量文件同步完成后升级一下第三方文件服务的版本完全移除自研文件服务的依赖并默认提供第三方文件服务实例即可,各个依赖方引入新的版本重新发布一下项目即可,改动程度相对较小。

假设有以下客户端依赖了文件服务:
一、文件双写+存量数据同步阶段

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;

/**
 * @author litb
 * @date 2022/5/21 17:16
 * <p>
 * 假装这是一个项目
 */
public class ProjectClient {

    public static void main(String[] args) {
        IThirdPartyFileOperationService operationService =
                FileOperationFactory.getInstance(EnumFileOperationType.DOUBLE_WRITE_AND_READ_FROM_CUSTOM);
        operationService.upload(new FileUploadDto());

        operationService.download("this is fileId");
    }
}

模拟下文件上传及下载,控制台输出如下:

自研文件服务上传文件...
oss文件服务上传文件...
自研文件服务下载文件...

可以看出,上传时是双写,读取时是从自研文件服务中读取。

二、存量数据同步完成之后
在存量数据同步完成之后,这个时候自研文件服务和oss中的存量数据和增量数据都是完全一致的,可以无缝切换到oss文件服务了,完全移除自研文件服务。

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.third.IThirdPartyFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;

/**
 * @author litb
 * @date 2022/5/21 17:16
 * <p>
 * 假装这是一个项目
 */
public class ProjectClient {

    public static void main(String[] args) {
        IThirdPartyFileOperationService operationService =
                //切换到oss实现,这一步在文件服务工程下完成即可,切换实现并完全移除自研文件服务后升级打包让各个依赖方重新引入
                FileOperationFactory.getInstance(EnumFileOperationType.OSS);
        operationService.upload(new FileUploadDto());

        operationService.download("this is fileId");
    }
}

模拟下文件上传和下载,控制台输出如下:

oss文件服务上传文件...
oss文件服务下载文件...

而且,考虑到不可控因素,还提供了基于第三方文件服务接口定义但是由自研文件服务提供实现的适配器

package com.cube.dp.adapter;

import com.cube.dp.adapter.fs.custom.IFileOperationService;
import com.cube.dp.adapter.fs.third.dto.FileDownloadDto;
import com.cube.dp.adapter.fs.third.dto.FileDownloadForStreamDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadDto;
import com.cube.dp.adapter.fs.third.dto.FileUploadForStreamDto;

/**
 * @author litb
 * @date 2022/5/21 16:23
 * <p>
 * 文件适配器,将第三方文件服务与自研文件服务适配
 */
public class CustomFileOperationAdaptor implements FileOperationAdaptee {

    private final IFileOperationService customFileOperationService;

    public CustomFileOperationAdaptor(IFileOperationService customFileOperationService) {
        this.customFileOperationService = customFileOperationService;
    }

    @Override
    public String upload(FileUploadDto dto) {
        return customFileOperationService.upload(to(dto));
    }

    @Override
    public String upload4Stream(FileUploadForStreamDto streamDto) {
        return customFileOperationService.upload4Stream(to(streamDto));
    }

    @Override
    public FileDownloadDto download(String fileId) {
        return to(customFileOperationService.download(fileId));
    }

    @Override
    public FileDownloadForStreamDto download4Stream(String fileId) {
        return to(customFileOperationService.download4Stream(fileId));
    }
}

假设要突然完全切换到自研文件服务,在第三方文件服务项目中切换到自研文件重新升级打包即可,各个项目重新引入新版本即可,无需回滚代码(主要是包路径调整)。

上述适配器模式的类图如下所示:


文件服务-适配器模式类图.png

表达能力欠缺一直是我的诸多缺点之一,不知道我上面一顿啰嗦有没有把我的想法表达清楚,参考代码也许更易理解一点,附上链接:适配器模式代码

总结

说一下为什么中途写到适配器模式类型的定义时让我觉得突然不想再写了,就像倚天屠龙记里张三丰教张无忌太极拳和太极剑一样,张无忌看了一遍之后虽然全部都忘记了但是最后却能够娴熟的运用出来,忘与不忘在我看来其中的差别其实就是无形有形的区别,有形只是生搬硬套,而无形却是融会贯通;设计模式难学的主要原因就是人为的界定了许多界限,当我在看完适配器类型的定义之后我把我上面写的代码往这个定义去套,尝试去解释这里面的一些概念,但是最终我发现连我自己都难以说服,所以我决定不再纠结这些具体的概念和定义,只是把我的想法表达出来就行了。

最后,说一些我个人最近的一些感悟,我写代码的时间不算长,最开始时写代码的时候,我觉得代码是难以控制的,不确定在什么地方会出问题,总是害怕出bug;不知道是上个月啥时候开始,我突然觉得我是能够控制住代码的,能够参照一部分源码写出一部分我觉得还不错的代码(起码看起来还不错),这应该还是处于有形的阶段;但是,我觉得后面至少应该还有一个阶段,当我写出一段代码后我会由衷的感叹:这段代码是这样不是因为我想要这样写,而是这段代码原本就应该是这个样子,就算是再让我写一千遍一万遍,它也应该就是这个样子。

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

推荐阅读更多精彩内容