34 - ASM之修改已有的方法

預(yù)期目標

假如有一個HelloWorld類痪伦,代碼如下:

public class HelloWorld {
    public void test() {
        System.out.println("this is a test method.");
    }
}

我們想實現(xiàn)的預(yù)期目標:對于test()方法,在“方法進入”時和“方法退出”時椭微,添加一條打印語句隐绵。

  • 第一種情況之众,在“方法進入”時,預(yù)期目標如下所示:
public class HelloWorld {
    public void test() {
        System.out.println("Method Enter...");
        System.out.println("this is a test method.");
    }
}
  • 第二種情況依许,在“方法退出”時棺禾,預(yù)期目標如下所示:
public class HelloWorld {
    public void test() {
        System.out.println("this is a test method.");
        System.out.println("Method Exit...");
    }
}

現(xiàn)在,我們有了明確的預(yù)期目標峭跳;接下來膘婶,就是將這個預(yù)期目標轉(zhuǎn)換成具體的ASM代碼。那么蛀醉,應(yīng)該怎么實現(xiàn)呢悬襟?從哪里著手呢?

實現(xiàn)思路

我們知道拯刁,現(xiàn)在的內(nèi)容是Class Transformation的操作脊岳,其中涉及到三個主要的類:ClassReader、ClassVisitor和ClassWriter垛玻。其中割捅,ClassReader負責讀取Class文件,ClassWriter負責生成Class文件帚桩,而具體的ClassVisitor負責進行Transformation的操作亿驾。換句話說,我們還是應(yīng)該從ClassVisitor類開始朗儒。

第一步颊乘,回顧一下ClassVisitor類當中主要的visitXxx()方法有哪些。在ClassVisitor類當中醉锄,有visit()乏悄、visitField()、visitMethod()和visitEnd()方法恳不;這些visitXxx()方法與.class文件里的不同部分之間是有對應(yīng)關(guān)系的檩小,如下圖:

visitXxx關(guān)系

根據(jù)我們的預(yù)期目標,現(xiàn)在想要修改的是“方法”的部分烟勋,那么就對應(yīng)著ClassVisitor類的visitMethod()方法规求。ClassVisitor.visitMethod()會返回一個MethodVisitor類的實例;而MethodVisitor類就是用來生成方法的“方法體”卵惦。

第二步阻肿,回顧一下MethodVisitor類當中定義了哪些visitXxx()方法。

methodVisitor方法

在MethodVisitor類當中沮尿,定義的visitXxx()方法比較多丛塌,但是我們可以將這些visitXxx()方法進行分組:

  • 第一組较解,visitCode()方法,標志著方法體(method body)的開始赴邻。
  • 第二組印衔,visitXxxInsn()方法,對應(yīng)方法體(method body)本身姥敛,這里包含多個方法奸焙。
  • 第三組,visitMaxs()方法彤敛,標志著方法體(method body)的結(jié)束与帆。
  • 第四組,visitEnd()方法臊泌,是最后調(diào)用的方法鲤桥。

另外,我們也回顧一下渠概,在MethodVisitor類中茶凳,visitXxx()方法的調(diào)用順序:

  • 第一步,調(diào)用visitCode()方法,調(diào)用一次。
  • 第二步掷邦,調(diào)用visitXxxInsn()方法,可以調(diào)用多次箱沦。
  • 第三步,調(diào)用visitMaxs()方法雇庙,調(diào)用一次谓形。
  • 第四步,調(diào)用visitEnd()方法疆前,調(diào)用一次寒跳。

到了這一步,我們基本上就知道了:需要修改的內(nèi)容就位于visitCode()和visitMaxs()方法之間竹椒,這是一個大概的范圍童太。

第三步,精確定位胸完。也就是說书释,在MethodVisitor類當中,要確定出要在哪一個visitXxx()方法里進行修改赊窥。

方法進入

如果我們想在“方法進入”時爆惧,添加一些打印語句,那么我們有兩個位置可以添加打印語句:

  • 第一個位置锨能,就是在visitCode()方法中检激。
  • 第二個位置肴捉,就是在第1個visitXxxInsn()方法中。

在這兩個位置當中叔收,我們推薦使用visitCode()方法。因為visitCode()方法總是位于方法體(method body)的前面傲隶,而第1個visitXxxInsn()方法是不穩(wěn)定的饺律。

public void visitCode() {
    // 首先,處理自己的代碼邏輯
    // TODO: 添加“方法進入”時的代碼

    // 其次跺株,調(diào)用父類的方法實現(xiàn)
    super.visitCode();
}

方法退出

如果我們在“方法退出”時想添加的代碼复濒,是否可以添加到visitMaxs()方法內(nèi)呢?這樣做是不行的乒省。因為在執(zhí)行visitMaxs()方法之前巧颈,方法體(method body)已經(jīng)執(zhí)行過了:在方法體(method body)當中,里面會包含return語句袖扛;如果return語句一執(zhí)行砸泛,后面的任何語句都不會再執(zhí)行了;換句話說蛆封,如果在visitMaxs()方法內(nèi)添加的打印輸出語句唇礁,由于前面方法體(method body)中已經(jīng)執(zhí)行了return語句,后面的任何語句就執(zhí)行不到了惨篱。

那么盏筐,到底是應(yīng)該在哪里添加代碼呢?為了回答這個問題砸讳,我們需要知道“方法退出”有哪幾種情況琢融。方法的退出,有兩種情況簿寂,一種是正常退出(執(zhí)行return語句)漾抬,另一種是異常退出(執(zhí)行throw語句);接下來陶耍,就是將這兩種退出情況應(yīng)用到ASM的代碼層面奋蔚。

在MethodVisitor類當中,無論是執(zhí)行return語句烈钞,還是執(zhí)行throw語句泊碑,都是通過visitInsn(opcode)方法來實現(xiàn)的。所以毯欣,如果我們想在“方法退出”時馒过,添加一些語句,那么這些語句放到visitInsn(opcode)方法中就可以了酗钞。

public void visitInsn(int opcode) {
    // 首先腹忽,處理自己的代碼邏輯
    if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
        // TODO: 添加“方法退出”時的代碼
    }

    // 其次来累,調(diào)用父類的方法實現(xiàn)
    super.visitInsn(opcode);
}

推薦做法:在編寫ASM代碼的時候,如果寫了一個類窘奏,它繼承自ClassVisitor嘹锁,那么就命名成XxxVisitor;如果寫了一個類着裹,它繼承自MethodVisitor领猾,那么就命名成XxxAdapter。通過類的名字骇扇,我就可以區(qū)分出哪些類是繼承自ClassVisitor摔竿,哪些類是繼承自MethodVisitor。

示例一:方法進入

編碼實現(xiàn):

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MethodEnterVisitor extends ClassVisitor {
    public MethodEnterVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null && !"<init>".equals(name)) {
            mv = new MethodEnterAdapter(api, mv);
        }
        return mv;
    }

    private static class MethodEnterAdapter extends MethodVisitor {
        public MethodEnterAdapter(int api, MethodVisitor methodVisitor) {
            super(api, methodVisitor);
        }

        @Override
        public void visitCode() {
            // 首先少孝,處理自己的代碼邏輯
            super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            super.visitLdcInsn("Method Enter...");
            super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 其次继低,調(diào)用父類的方法實現(xiàn)
            super.visitCode();
        }
    }
}

在上面MethodEnterAdapter類的visitCode()方法中,主要是做兩件事情:

  • 首先稍走,處理自己的代碼邏輯袁翁。
  • 其次,調(diào)用父類的方法實現(xiàn)钱磅。

在處理自己的代碼邏輯中梦裂,有3行代碼。這3條語句的作用就是添加System.out.println("Method Enter...");語句:

super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Enter...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

注意盖淡,上面的代碼中使用了super關(guān)鍵字年柠。

事實上,在MethodVisitor類當中褪迟,定義了一個protected MethodVisitor mv;字段冗恨。我們也可以使用mv這個字段,代碼也可以這樣寫:

mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Method Enter...");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

但是這樣寫味赃,可能會遇到mv為null的情況掀抹,這樣就會出現(xiàn)NullPointerException異常。

如果使用super心俗,就會避免NullPointerException異常的情況傲武。因為使用super的情況下,就是調(diào)用父類定義的方法城榛,在本例中其實就是調(diào)用MethodVisitor類里定義的方法揪利。在MethodVisitor類里的visitXxx()方法中,會先對mv進行是否為null的判斷狠持,所以就不會出現(xiàn)NullPointerException的情況疟位。

public abstract class MethodVisitor {
    protected MethodVisitor mv;

    public void visitCode() {
        if (mv != null) {
            mv.visitCode();
        }
    }

    public void visitInsn(final int opcode) {
        if (mv != null) {
            mv.visitInsn(opcode);
        }
    }

    public void visitIntInsn(final int opcode, final int operand) {
        if (mv != null) {
            mv.visitIntInsn(opcode, operand);
        }
    }

    public void visitVarInsn(final int opcode, final int var) {
        if (mv != null) {
            mv.visitVarInsn(opcode, var);
        }
    }

    public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor) {
        if (mv != null) {
            mv.visitFieldInsn(opcode, owner, name, descriptor);
        }
    }

    // ......

    public void visitMaxs(final int maxStack, final int maxLocals) {
        if (mv != null) {
            mv.visitMaxs(maxStack, maxLocals);
        }
    }

    public void visitEnd() {
        if (mv != null) {
            mv.visitEnd();
        }
    }
}

進行轉(zhuǎn)換:

import lsieun.utils.FileUtils;
import org.objectweb.asm.*;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)構(gòu)建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)構(gòu)建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new MethodEnterVisitor(api, cw);

        //(4)結(jié)合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

結(jié)果驗證:

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Method m = clazz.getDeclaredMethod("test");

        Object instance = clazz.newInstance();
        m.invoke(instance);
    }
}

特殊情況:<init>()方法

在.class文件中,<init>()方法喘垂,就表示類當中的構(gòu)造方法甜刻。
我們在“方法進入”時绍撞,有一個對于<init>的判斷:

if (mv != null && !"<init>".equals(name)) {
    // ......
}

Java requires that if you call this() or super() in a constructor, it must be the first statement.

public class HelloWorld {
    public HelloWorld() {
        System.out.println("Method Enter...");
        super(); // 報錯:Call to 'super()' must be first statement in constructor body
    }
}

去掉對于<init>()方法的判斷,會發(fā)現(xiàn)它好像也是可以正常執(zhí)行的得院。
但是傻铣,如果我們換一下添加的語句,就會出錯了:

super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitVarInsn(Opcodes.ALOAD, 0);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

示例二:方法退出

代碼實現(xiàn):

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MethodExitVisitor extends ClassVisitor {
    public MethodExitVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null && !"<init>".equals(name)) {
            mv = new MethodExitAdapter(api, mv);
        }
        return mv;
    }

    private static class MethodExitAdapter extends MethodVisitor {
        public MethodExitAdapter(int api, MethodVisitor methodVisitor) {
            super(api, methodVisitor);
        }

        @Override
        public void visitInsn(int opcode) {
            // 首先尿招,處理自己的代碼邏輯
            if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                super.visitLdcInsn("Method Exit...");
                super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }

            // 其次矾柜,調(diào)用父類的方法實現(xiàn)
            super.visitInsn(opcode);
        }
    }
}

進行轉(zhuǎn)換:

import lsieun.utils.FileUtils;
import org.objectweb.asm.*;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)構(gòu)建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)構(gòu)建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new MethodExitVisitor(api, cw);

        //(4)結(jié)合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

結(jié)果驗證:

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Method m = clazz.getDeclaredMethod("test");

        Object instance = clazz.newInstance();
        m.invoke(instance);
    }
}

輸出結(jié)果:

this is a test method.
Method Exit...

示例三:方法進入和方法退出

第一種方式

第一種方式,就是將多個ClassVisitor類串聯(lián)起來就谜。

import lsieun.utils.FileUtils;
import org.objectweb.asm.*;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)構(gòu)建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)構(gòu)建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv1 = new MethodEnterVisitor(api, cw);
        ClassVisitor cv2 = new MethodExitVisitor(api, cv1);
        ClassVisitor cv = cv2;

        //(4)結(jié)合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}
第二種方式

第二種方式,就是將所有的代碼都放到一個ClassVisitor類里面里覆。

編碼實現(xiàn):

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MethodAroundVisitor extends ClassVisitor {
    public MethodAroundVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null && !"<init>".equals(name)) {
            boolean isAbstractMethod = (access & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT;
            boolean isNativeMethod = (access & Opcodes.ACC_NATIVE) == Opcodes.ACC_NATIVE;
            if (!isAbstractMethod && !isNativeMethod) {
                mv = new MethodAroundAdapter(api, mv);
            }
        }
        return mv;
    }

    private static class MethodAroundAdapter extends MethodVisitor {
        public MethodAroundAdapter(int api, MethodVisitor methodVisitor) {
            super(api, methodVisitor);
        }

        @Override
        public void visitCode() {
            // 首先丧荐,處理自己的代碼邏輯
            super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            super.visitLdcInsn("Method Enter...");
            super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 其次,調(diào)用父類的方法實現(xiàn)
            super.visitCode();
        }

        @Override
        public void visitInsn(int opcode) {
            // 首先喧枷,處理自己的代碼邏輯
            if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                super.visitLdcInsn("Method Exit...");
                super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }

            // 其次虹统,調(diào)用父類的方法實現(xiàn)
            super.visitInsn(opcode);
        }
    }
}

進行轉(zhuǎn)換:

import lsieun.utils.FileUtils;
import org.objectweb.asm.*;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)構(gòu)建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)構(gòu)建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new MethodAroundVisitor(api, cw);

        //(4)結(jié)合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

總結(jié)

本文主要是對“方法進入”和“方法退出”添加代碼進行介紹,內(nèi)容總結(jié)如下:

  • 第一點隧甚,在“方法進入”時和“方法退出”時添加代碼车荔,應(yīng)該如何實現(xiàn)?
    • 在“方法進入”時添加代碼戚扳,是在visitCode()方法當中完成;
    • 在“方法退出”添加代碼時珠增,是在visitInsn(opcode)方法中,判斷opcode為return或throw的情況下完成。
  • 第二點梦皮,在“方法進入”時和“方法退出”時添加代碼,有一些特殊的情況退子,需要小心處理:
    • 接口荐虐,是否需要處理福扬?接口當中的抽象方法沒有方法體,但也可能有帶有方法體的default方法汽烦。
    • 帶有特殊修飾符的方法:
      • 抽象方法,是否需要處理?不只是接口當中有抽象方法煮岁,抽象類里也可能有抽象方法。抽象方法,是沒有方法體的。
      • native方法腌且,是否需要處理?native方法是沒有方法體的精续。
    • 名字特殊的方法顷级,例如,構(gòu)造方法(<init>())和靜態(tài)初始化方法(<clinit>())翔冀,是否需要處理?

另外跌捆,在編寫代碼的時候姆钉,我們遵循一個“規(guī)則”:如果是ClassVisitor的子類,就取名為XxxVisitor類;如果是MethodVisitor的子類思恐,就取名為XxxAdapter類婚温。

本文的介紹方式側(cè)重于讓大家理解“工作原理”荆秦,而后續(xù)介紹的AdviceAdapter則側(cè)重于“應(yīng)用”,AdviceAdapter的實現(xiàn)也是基于visitCode()和visitInsn(opcode)方法實現(xiàn)的坪圾,在理解上有一個步步遞進的關(guān)系漾月。

本文也是基礎(chǔ)方式,可以應(yīng)對各種場景烛芬,大家需進行掌握。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赘娄,一起剝皮案震驚了整個濱河市仆潮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遣臼,老刑警劉巖性置,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異暑诸,居然都是意外死亡蚌讼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門个榕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篡石,“玉大人,你說我怎么就攤上這事西采』巳” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胖眷。 經(jīng)常有香客問我武通,道長,這世上最難降的妖魔是什么珊搀? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任冶忱,我火速辦了婚禮,結(jié)果婚禮上境析,老公的妹妹穿的比我還像新娘囚枪。我一直安慰自己,他們只是感情好劳淆,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布链沼。 她就那樣靜靜地躺著,像睡著了一般沛鸵。 火紅的嫁衣襯著肌膚如雪括勺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天曲掰,我揣著相機與錄音疾捍,去河邊找鬼。 笑死栏妖,一個胖子當著我的面吹牛拾氓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播底哥,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼房官!你這毒婦竟也來了趾徽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤翰守,失蹤者是張志新(化名)和其女友劉穎孵奶,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜡峰,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡了袁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了湿颅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片载绿。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖油航,靈堂內(nèi)的尸體忽然破棺而出崭庸,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布怕享,位于F島的核電站执赡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏函筋。R本人自食惡果不足惜沙合,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望跌帐。 院中可真熱鬧首懈,春花似錦、人聲如沸含末。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佣盒。三九已至挎袜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肥惭,已是汗流浹背盯仪。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜜葱,地道東北人全景。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像牵囤,于是被迫代替她去往敵國和親爸黄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

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