字节码插桩技术,用于系统监控设计和实现

转载作者:小傅哥
本文会基于 AOP、字节码框架(ASM、Javassist、Byte-Buddy),分别实现不同的监控实现代码。整个工程结构如下:

MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└── pom.xml

简单介绍:aop、asm、bytebuddy、javassist,分别是四种不同的实现方案。test 是一个基于 SpringBoot 的简单测试工程。

cn-bugstack-middleware-test
@RestController
public class UserController {

    private Logger logger = LoggerFactory.getLogger(UserController.class);

    /**
     * 测试:http://localhost:8081/api/queryUserInfo?userId=aaa
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        logger.info("查询用户信息,userId:{}", userId);
        return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
    }

}

接下来的各类监控代码实现,都会以监控 UserController#queryUserInfo 的方法执行信息为主,看看各类技术都是怎么操作的

使用 AOP 做个切面监控

1. 工程结构
cn-bugstack-middleware-aop
└── src
    ├── main
    │   └── java
    │       ├── cn.bugstack.middleware.monitor
    │       │   ├── annotation
    │       │   │   └── DoMonitor.java
    │       │   ├── config
    │       │   │   └── MonitorAutoConfigure.java
    │       │   └── DoJoinPoint.java
    │       └── resources
    │           └── META-INF 
    │               └── spring.factories
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

基于 AOP 实现的监控系统,核心逻辑的以上工程并不复杂,其核心点在于对切面的理解和运用,以及一些配置项需要按照 SpringBoot 中的实现方式进行开发。

  • DoMonitor,是一个自定义注解。它作用就是在需要使用到的方法监控接口上,添加此注解并配置必要的信息。
  • MonitorAutoConfigure,配置下是可以对 SpringBoot yml 文件的使用,可以处理一些 Bean 的初始化操作。
  • DoJoinPoint,是整个中间件的核心部分,它负责对所有添加自定义注解的方法进行拦截和逻辑处理。
2. 定义监控注解

cn.bugstack.middleware.monitor.annotation.DoMonitor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {

   String key() default "";
   String desc() default "";

}
3. 定义切面拦截

cn.bugstack.middleware.monitor.DoJoinPoint

@Aspect
public class DoJoinPoint {

    @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
    public void aopPoint() {
    }

    @Around("aopPoint() && @annotation(doMonitor)")
    public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {
        long start = System.currentTimeMillis();
        Method method = getMethod(jp);
        try {
            return jp.proceed();
        } finally {
            System.out.println("监控 - Begin By AOP");
            System.out.println("监控索引:" + doMonitor.key());
            System.out.println("监控描述:" + doMonitor.desc());
            System.out.println("方法名称:" + method.getName());
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("监控 - End\r\n");
        }
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

}
  • 使用注解 @Aspect,定义切面类。这是一个非常常用的切面定义方式。
  • @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)"),定义切点。在 Pointcut 中提供了很多的切点寻找方式,有指定方法名称的、有范围筛选表达式的,也有我们现在通过自定义注解方式的。一般在中间件开发中,自定义注解方式使用的比较多,因为它可以更加灵活的运用到各个业务系统中。
  • @Around("aopPoint() && @annotation(doMonitor)"),可以理解为是对方法增强的织入动作,有了这个注解的效果就是在你调用已经加了自定义注解 @DoMonitor 的方法时,会先进入到此切点增强的方法。那么这个时候就你可以做一些对方法的操作动作了,比如我们要做一些方法监控和日志打印等。
  • 最后在 doRouter 方法体中获取把方法执行 jp.proceed(); 使用 try finally 包装起来,并打印相关的监控信息。这些监控信息的获取最后都是可以通过异步消息的方式发送给服务端,再由服务器进行处理监控数据和处理展示到监控页面。
4. 初始化切面类

cn.bugstack.middleware.monitor.config.MonitorAutoConfigure

@Configuration
public class MonitorAutoConfigure {

    @Bean
    @ConditionalOnMissingBean
    public DoJoinPoint point(){
        return new DoJoinPoint();
    }

}
  • @Configuration,可以算作是一个组件注解,在 SpringBoot 启动时可以进行加载创建出 Bean 文件。因为 @Configuration 注解有一个 @Component 注解
  • MonitorAutoConfigure 可以处理自定义在 yml 中的配置信息,也可以用于初始化 Bean 对象,比如在这里我们实例化了 DoJoinPoint 切面对象。
5. 运行测试
5.1 引入 POM 配置
<!-- 监控方式:AOP -->
<dependency>
    <groupId>cn.bugstack.middleware</groupId>
    <artifactId>cn-bugstack-middleware-aop</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
5.2 方法上配置监控注册
@DoMonitor(key = "cn.bugstack.middleware.UserController.queryUserInfo", desc = "查询用户信息")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    logger.info("查询用户信息,userId:{}", userId);
    return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
}

在通过 POM 引入自己的开发的组件后,就可以通过自定义的注解,拦截方法获取监控信息。

5.3 测试结果
2021-07-04 23:21:10.710  INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController     : 查询用户信息,userId:aaa
监控 - Begin By AOP
监控索引:cn.bugstack.middleware.UserController.queryUserInfo
监控描述:查询用户信息
方法名称:queryUserInfo
方法耗时:6ms
监控 - End

接下来我们开始介绍关于使用字节码插桩非入侵的方式进行系统监控,关于字节码插桩常用的有三个组件,包括:ASM、Javassit、Byte-Buddy,接下来我们分别介绍它们是如何使用的。

ASM

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
1. 先来个测试

cn.bugstack.middleware.monitor.test.ApiTest

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 添加方法;修饰符、方法名、描述符、签名、异常
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 执行指令;获取静态属性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加载常量 load constant
    methodVisitor.visitLdcInsn("Hello World ASM!");
    // 调用方法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 设置操作数栈的深度和局部变量的大小
    methodVisitor.visitMaxs(2, 1);
    // 方法结束
    methodVisitor.visitEnd();
    // 类完成
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();
}

以上这段代码就是基于 ASM 编写的 HelloWorld,整个过程包括:定义一个类的生成 ClassWriter、设定版本、修饰符、全类名、签名、父类、实现的接口,其实也就是那句;public class HelloWorld

监控设计工程结构
cn-bugstack-middleware-asm
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   ├── MethodInfo.java
    │   │       │   └── ProfilingFilter.java
    │   │       ├── probe
    │   │       │   ├── ProfilingAspect.java
    │   │       │   ├── ProfilingClassAdapter.java
    │   │       │   ├── ProfilingMethodVisitor.java
    │   │       │   └── ProfilingTransformer.java
    │   │       └── PreMain.java
    │   └── resources   
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

以上工程结构是使用 ASM 框架给系统方法做增强操作,也就是相当于通过框架完成硬编码写入方法前后的监控信息。不过这个过程转移到了 Java 程序启动时在 Javaagent#premain 进行处理。

  • MethodInfo 是方法的定义,主要是描述类名、方法名、描述、入参、出参信息。
  • ProfilingFilter 是监控的配置信息,主要是过滤一些不需要字节码增强操作的方法,比如main、hashCode、javax/等
  • ProfilingAspect、ProfilingClassAdapter、ProfilingMethodVisitor、ProfilingTransformer,这四个类主要是完成字节码插装操作和输出监控结果的类。
  • PreMain 提供了 Javaagent 的入口,JVM 首先尝试在代理类上调用 premain 方法。
  • MANIFEST.MF 是配置信息,主要是找到 Premain-Class Premain-Class: cn.bugstack.middleware.monitor.PreMain
3. 监控类入口

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ProfilingTransformer());
    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}

这个是 Javaagent 技术的固定入口方法类,同时还需要把这个类的路径配置到 MANIFEST.MF 中。

4. 字节码方法处理

cn.bugstack.middleware.monitor.probe.ProfilingTransformer

public class ProfilingTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (ProfilingFilter.isNotNeedInject(className)) {
                return classfileBuffer;
            }
            return getBytes(loader, className, classfileBuffer);
        } catch (Throwable e) {
            System.out.println(e.getMessage());
        }
        return classfileBuffer;
    }

    private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ProfilingClassAdapter(cw, className);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

}
5. 字节码方法解析

cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor

public class ProfilingMethodVisitor extends AdviceAdapter {

    private List<String> parameterTypeList = new ArrayList<>();
    private int parameterTypeCount = 0;     // 参数个数
    private int startTimeIdentifier;        // 启动时间标记
    private int parameterIdentifier;        // 入参内容标记
    private int methodId = -1;              // 方法全局唯一标记
    private int currentLocal = 0;           // 当前局部变量值
    private final boolean isStaticMethod;   // true;静态方法,false;非静态方法
    private final String className;

    protected ProfilingMethodVisitor(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {
        super(ASM5, mv, access, methodName, desc);
        this.className = className;
        // 判断是否为静态方法,非静态方法中局部变量第一个值是this,静态方法是第一个入参参数
        isStaticMethod = 0 != (access & ACC_STATIC);
        //(String var1,Object var2,String var3,int var4,long var5,int[] var6,Object[][] var7,Req var8)=="(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)V"
        Matcher matcher = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
        while (matcher.find()) {
            parameterTypeList.add(matcher.group(1));
        }
        parameterTypeCount = parameterTypeList.size();
        methodId = ProfilingAspect.generateMethodId(new MethodInfo(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
    }     

    //... 一些字节码插桩操作 
}
  • 当程序启动加载的时候,每个类的每一个方法都会被监控到。类的名称、方法的名称、方法入参出参的描述等,都可以在这里获取。
  • 为了可以在后续监控处理不至于每一次都去传参(方法信息)浪费消耗性能,一般这里都会给每个方法生产一个全局防重的 id ,通过这个 id 就可以查询到对应的方法。
  • 另外从这里可以看到的方法的入参和出参被描述成一段指定的码,(II)Ljava/lang/String; ,为了我们后续对参数进行解析,那么需要将这段字符串进行拆解。
6. 运行测试
6.1 配置 VM 参数 Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar

IDEA 运行时候配置到 VM options 中,jar包地址按照自己的路径进行配置。

6.2 测试结果
监控 - Begin By ASM
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:54(s)
监控 - End

Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
1. 先来个测试

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.makeClass("cn.bugstack.middleware.javassist.MathUtil");

        // 属性字段
        CtField ctField = new CtField(CtClass.doubleType, "π", ctClass);
        ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
        ctClass.addField(ctField, "3.14");

        // 方法:求圆面积
        CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
        calculateCircularArea.setModifiers(Modifier.PUBLIC);
        calculateCircularArea.setBody("{return π * $1 * $1;}");
        ctClass.addMethod(calculateCircularArea);

        // 方法;两数之和
        CtMethod sumOfTwoNumbers = new CtMethod(pool.get(Double.class.getName()), "sumOfTwoNumbers", new CtClass[]{CtClass.doubleType, CtClass.doubleType}, ctClass);
        sumOfTwoNumbers.setModifiers(Modifier.PUBLIC);
        sumOfTwoNumbers.setBody("{return Double.valueOf($1 + $2);}");
        ctClass.addMethod(sumOfTwoNumbers);
        // 输出类的内容
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
        Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
        System.out.println("圆面积:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("两数和:" + obj_02);
    }

}
  • 这是一个使用 Javassist 生成的求圆面积和抽象的类和方法并运行结果的过程,可以看到 Javassist 主要是 ClassPool、CtClass、CtField、CtMethod 等方法的使用。
  • 测试结果主要包括会生成一个指定路径下的类 cn.bugstack.middleware.javassist.MathUtil,同时还会在控制台输出结果。
生成的类
public class MathUtil {
  private static final double π = 3.14D;

  public double calculateCircularArea(double var1) {
      return 3.14D * var1 * var1;
  }

  public Double sumOfTwoNumbers(double var1, double var3) {
      return var1 + var3;
  }

  public MathUtil() {
  }
}
2. 监控设计工程结构
cn-bugstack-middleware-javassist
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   └── MethodDescription.java
    │   │       ├── probe
    │   │       │   ├── Monitor.java
    │   │       │   └── MyMonitorTransformer.java
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

整个使用 javassist 实现的监控框架来看,与 ASM 的结构非常相似,但大部分操作字节码的工作都交给了 javassist 框架来处理,所以整个代码结构看上去更简单了。

3. 监控方法插桩

cn.bugstack.middleware.monitor.probe.MyMonitorTransformer

public class MyMonitorTransformer implements ClassFileTransformer {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("cn.bugstack.middleware.test.interfaces.UserController");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            String currentClassName = className.replaceAll("/", ".");
            if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
                return null;
            }

            // 获取类
            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            String clazzName = ctClass.getName();

            // 获取方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
            String methodName = ctMethod.getName();

            // 方法信息:methodInfo.getDescriptor();
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            // 方法:入参信息
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            CtClass[] parameterTypes = ctMethod.getParameterTypes();

            boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
            int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
            List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
            List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
            StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化

            for (int i = 0; i < parameterSize; i++) {
                parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
                parameterTypeList.add(parameterTypes[i].getName());
                if (i + 1 == parameterSize) {
                    parameters.append("$").append(i + 1);
                } else {
                    parameters.append("$").append(i + 1).append(",");
                }
            }

            // 方法:出参信息
            CtClass returnType = ctMethod.getReturnType();
            String returnTypeName = returnType.getName();

            // 方法:生成方法唯一标识ID
            int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

            // 定义属性
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));

            // 方法前加强
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

            // 方法后加强
            ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换

            // 方法;添加TryCatch
            ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获

            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
  • 与 ASM 实现相比,整体的监控方法都是类似的,所以这里只展示下不同的地方。
  • 通过 Javassist 的操作,主要是实现一个 ClassFileTransformer 接口的 transform 方法,在这个方法中获取字节码并进行相应的处理。
  • 处理过程包括:获取类、获取方法、获取入参信息、获取出参信息、给方法生成唯一ID、之后开始进行方法的前后增强操作,这个增强也就是在方法块中添加监控代码。
  • 最后返回字节码信息 return ctClass.toBytecode(); 现在你新加入的字节码就已经可以被程序加载处理了。
4. 运行测试
4.1 配置 VM 参数 Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar

作者源码:https://github.com/fuzhengwei/MonitorDesign

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

推荐阅读更多精彩内容