技术最佳实践-javassist字节码编程

姓名:何热;学号:20021210616; 学院:电子工程学院
转自https://blog.csdn.net/qq_37442469/article/details/107240561
【嵌牛导读】javassist字节码编程

【嵌牛鼻子】Java javassist 动态编程
【嵌牛提问】Java中如何使用动态编程?什么原理?如何改进?

【嵌牛正文】

什么是动态编程

动态编程是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术在Java中有如下几种方式:

  • 动态编译

动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行

  • 调用JavaScript引擎

Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本。这个脚本API允许你为脚本语言提供Java支持。

  • 动态生成字节码

这种技术通过操作Java字节码的方式在JVM中生成新类或者对已经加载的类动态添加元素

动态编程解决什么问题

在静态语言中引入动态特性,主要是为了解决一些使用场景的痛点。其实完全使用静态编程也办得到,只是付出的代价比较高,没有动态编程来得优雅。例如依赖注入框架Spring使用了反射,而Dagger2 却使用了代码生成的方式(APT)例如 :

  • 在那些依赖关系需要动态确认的场景:
  • 需要在运行时动态插入代码的场景,比如动态代理的实现。
  • 通过配置文件来实现相关功能的场景

Java中如何使用

此处我们主要说一下通过动态生成字节码的方式,其他方式可以自行查找资料。
操作java字节码的工具有两个比较流行,一个是ASM,一个是Javassit。

  • ASM

直接操作字节码指令,执行效率高,使用者需要掌握Java类字节码文件格式及指令,对使用者的要求比较高。

  • Javassit

提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。

应用层面来讲一般使用建议优先选择Javassit,如果后续发现Javassit 成为了整个应用的效率瓶颈的话可以再考虑ASM.当然如果开发的是一个基础类库,或者基础平台,还是直接使用ASM吧,相信从事这方面工作的开发者能力应该比较高。

Javassit使用方法

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。

  • ClassPool

一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节

  • CtClass

表示一个类,这些CtClass对象可以从ClassPool获得

  • CtMethods

表示类中的方法

  • CtFields

表示类中的字段

动态生成一个类

下面的代码会生成一个实现了Cloneable接口的类GenerateClass

public class Main{
    public void generate(){
        ClassPool pool = ClassPool.getDefault();
        CtClass ct = pool.makeClass("top.ss007.GenerateClass");//创建类
        ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//让类实现Cloneable接口
        CtField f = new CtField(CtClass.intType, "id", ct);//获得一个类型为int,名称为id的字段
        f.setModifiers(AccessFlag.PUBLIC);//将字段设置为public
        ct.addField(f);//将字段设置到类上
        //添加构造函数
        CtConstructor constructor = CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}", ct);
        ct.addConstructor(constructor);
        //添加方法
        CtMethod helloM = CtNewMethod.make("public void hello(String des){ System.out.println(des);}", ct);
        ct.addMethod(helloM);
 
        ct.writeFile();//将生成的.class文件保存到磁盘
        ct.toClass();//获取class实例
 
        //下面的代码为验证代码
        Field[] fields = ct.toClass().getFields();
        System.out.println("属性名称:" + fields[0].getName() + "  属性类型:" + fields[0].getType());
 }
}

@Test
    @SneakyThrows
    public void test21() {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ct = classPool.makeClass("com.pigic.GenerateClass");//创建类
        ct.setInterfaces(new CtClass[]{classPool.get("java.lang.Cloneable")});
        CtMethod method = CtNewMethod.make("public void test22() {\n" +
                "        System.out.println(\"11111\");\n" +
                "        System.out.println(\"11111\");\n" +
                "        System.out.println(\"11111\");\n" +
                "    }", ct);
        ct.addMethod(method);
        ct.writeFile("src\\main\\resources");
        ct.toClass();
        ReflectUtil.invoke(ReflectUtil.newInstance("com.pigic.GenerateClass"),"test22");
    }
    public void test21() {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ct = classPool.makeClass("com.pigic.GenerateClass");//创建类
        ct.setInterfaces(new CtClass[]{classPool.get("java.lang.Cloneable")});
        //为cTclass对象添加一个属性start
        ct.addField(CtField.make("public Integer start = new Integer(15);", ct));
        ct.addMethod(CtMethod.make("public void setStart(Integer start){this.start = start;}", ct));
        ct.addMethod(CtMethod.make("public Integer getStart(){return this.start;}", ct));
        CtMethod method = CtNewMethod.make("public void test22() {\n" +
                "        System.out.println(\"11111\");\n" +
                "        System.out.println(getStart());\n" +
                "        System.out.println(\"11111\");\n" +
                "    }", ct);
        ct.addMethod(method);
        ct.writeFile("src\\main\\resources");
        ct.toClass();
        ReflectUtil.invoke(ReflectUtil.newInstance("com.pigic.GenerateClass"),"test22");
    }

创建属性

//为cTclass对象添加一个属性start
        CtField ctField = CtField.make("public Integer start = new Integer(15);", ct);

创建方法\

ct.addMethod(CtMethod.make("public void setStart(Integer start){this.start = start;}", ct));

属性上添加注解

public void test21() {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ct = classPool.makeClass("com.pigic.GenerateClass");//创建类
        ClassFile classFile = ct.getClassFile();
        ConstPool constPool = classFile.getConstPool();

        ct.setInterfaces(new CtClass[]{classPool.get("java.lang.Cloneable")});
        //为cTclass对象添加一个属性start
        CtField ctField = CtField.make("public Integer start = new Integer(15);", ct);
        FieldInfo fieldInfo = ctField.getFieldInfo();
        AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool,AnnotationsAttribute.visibleTag);
        Annotation autowired = new Annotation("org.springframework.beans.factory.annotation.Autowired",constPool);
        annotationsAttribute.addAnnotation(autowired);
        fieldInfo.addAttribute(annotationsAttribute);
        ct.addField(ctField);
        ct.addMethod(CtMethod.make("public void setStart(Integer start){this.start = start;}", ct));
        ct.addMethod(CtMethod.make("public Integer getStart(){return this.start;}", ct));
        CtMethod method = CtNewMethod.make("public void test22() {\n" +
                "        System.out.println(\"11111\");\n" +
                "        System.out.println(getStart());\n" +
                "        System.out.println(\"11111\");\n" +
                "    }", ct);
        ct.addMethod(method);
        ct.writeFile("src\\main\\resources");
        ct.toClass();
        ReflectUtil.invoke(ReflectUtil.newInstance("com.pigic.GenerateClass"),"test22");
    }

动态修改方法体

动态的修改一个方法的内容才是我们关注的重点,例如在AOP编程方面,我们就会用到这种技术,动态的在一个方法中插入代码。 例如我们有下面这样一个类

public class Point {
    private int x;
    private int y;
 
    public Point(){}
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    public void move(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
}

我们要动态的在内存中在move()方法体的前后插入一些代码

public void modifyMethod()
    {
        ClassPool pool=ClassPool.getDefault();
        try {
            CtClass ct=pool.getCtClass("top.ss007.Point");
            CtMethod m=ct.getDeclaredMethod("move");
           //$1,$2   表示获取方法的入参值
            m.insertBefore("{ System.out.print(\"dx:\"+$1); System.out.println(\"dy:\"+$2);}");
            m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}");
 
            ct.writeFile();
            //通过反射调用方法,查看结果
            Class pc=ct.toClass();
            Method move= pc.getMethod("move",new Class[]{int.class,int.class});
            Constructor<?> con=pc.getConstructor(new Class[]{int.class,int.class});
            move.invoke(con.newInstance(1,2),1,2);
        }
        ...
    }

使用反编译工具查看修改后的move方法结果:

    System.out.print("dx:" + dx);System.out.println("dy:" + dy);
    this.x += dx;
    this.y += dy;
    Object localObject = null;//方法返回值
    System.out.println(this.x);System.out.println(this.y);
  }

重写方法

@Test
    @SneakyThrows
    public void test23() {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get("com.alibaba.fastjson.JSON");
        CtMethod ctMethod = ctClass.getDeclaredMethod("parseObject",classPool.get(new String[]{"java.lang.String"}));
        ctMethod.setBody("{\n" +
                "        System.out.println(com.pigic.hzeropigic.domain.dto.PIGIC.PIGIC);\n" +
                "        System.out.println(\"111\");\n" +
                "        System.out.println($1);\n" +
                "        cn.pigicutils.core.lang.Console.log(new Object[]{\"222\"});\n" +
                "        return new com.alibaba.fastjson.JSONObject();\n" +
                "    }");
        ctClass.writeFile("src\\main\\resources");
        ctClass.toClass();
        JSONObject jsonObject = JSON.parseObject("{xx:11,aa:15}");
    }

类附上注解

// 类附上注解
        AnnotationsAttribute classAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        Annotation controller = new Annotation("org.springframework.stereotype.Controller",constpool);
        Annotation requestMapping = new Annotation("org.springframework.web.bind.annotation.RequestMapping.RequestMapping",constpool);
        String visitPath = "/api/department";
        requestMapping.addMemberValue("value",new StringMemberValue(visitPath,constpool));
        classAttr.addAnnotation(controller);
        classAttr.addAnnotation(requestMapping);
        ccFile.addAttribute(classAttr);

方法上附上注解

AnnotationsAttribute methodAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        //Annotation annotation3 = new Annotation("org.springframework.web.bind.annotation.RequestMapping.RequestMapping",constpool);
        requestMapping.addMemberValue("value",new StringMemberValue("/register",constpool));

        Annotation responseBody = new Annotation("org.springframework.web.bind.annotation.RequestMapping.ResponseBody",constpool);
        methodAttr.addAnnotation(requestMapping);
        methodAttr.addAnnotation(responseBody);
        MethodInfo info = method.getMethodInfo();
        info.addAttribute(methodAttr);
        clazz.addMethod(method);
        clazz.writeFile();

综合使用

public void makeclass(String className,String methodName, CONSTANTS.INVOKETYPE invoketype,String interfaceCode) throws NotFoundException, CannotCompileException, IOException {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass(className);
        ClassFile ccFile = clazz.getClassFile();
        ConstPool constpool = ccFile.getConstPool();

        CtClass executor = pool.get("com.javassist.test.Executor");
        CtClass requst = pool.get("javax.servlet.http.HttpServletRequest");
        CtClass response = pool.get("javax.servlet.http.HttpServletResponse");

        String fieldName = invoketype.getValue());
        // 增加字段
        CtField field = new CtField(executor,fieldName,clazz);
        field.setModifiers(Modifier.PUBLIC);
        FieldInfo fieldInfo = field.getFieldInfo();

        // 属性附上注解
        AnnotationsAttribute fieldAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        Annotation autowired = new Annotation("org.springframework.beans.factory.annotation.Autowired",constpool);
        fieldAttr.addAnnotation(autowired);
        fieldInfo.addAttribute(fieldAttr);
        clazz.addField(field);

        // 增加方法,javassist可以直接将字符串set到方法体中,所以使用时非常方便
        CtMethod method = new CtMethod(new CtClassType(CtClass.javaLangObject,pool),methodName,new CtClass[]{requst,response},clazz);
        method.setModifiers(java.lang.reflect.Modifier.PUBLIC);
        StringBuffer methodBody = new StringBuffer();
        methodBody.append("{return "+fieldName+".execute(\""+interfaceCode+"\",(com.javassist.test.RequestVo)$1.getAttribute(\"request\"));}");
        method.setBody(methodBody.toString());



        // 类附上注解
        AnnotationsAttribute classAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        Annotation controller = new Annotation("org.springframework.stereotype.Controller",constpool);
        Annotation requestMapping = new Annotation("org.springframework.web.bind.annotation.RequestMapping.RequestMapping",constpool);
        String visitPath = "/api/department";
        requestMapping.addMemberValue("value",new StringMemberValue(visitPath,constpool));
        classAttr.addAnnotation(controller);
        classAttr.addAnnotation(requestMapping);
        ccFile.addAttribute(classAttr);

        //方法附上注解
        AnnotationsAttribute methodAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        //Annotation annotation3 = new Annotation("org.springframework.web.bind.annotation.RequestMapping.RequestMapping",constpool);
        requestMapping.addMemberValue("value",new StringMemberValue("/register",constpool));

        Annotation responseBody = new Annotation("org.springframework.web.bind.annotation.RequestMapping.ResponseBody",constpool);
        methodAttr.addAnnotation(requestMapping);
        methodAttr.addAnnotation(responseBody);
        MethodInfo info = method.getMethodInfo();
        info.addAttribute(methodAttr);
        clazz.addMethod(method);
        clazz.writeFile();

    }

案例:使用 javassist 修改字节码实现 eureka-client 监听服务启动

  • 查看eureka里源码,有个com.netflix.discovery.shared.Application类,addInstance方法在服务上线或更新,removeInstance方法在服务下线时调用,因此修改这俩方法,实现监听服务上、下线。
  • 由于spring-boot自己实现的类加载机制,以spring-boot的jar形式运行javassist会扫描不到包,要通过insertClassPath添加扫描路径。
  • 通过setBody修改方法体,分别添加me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaAddInstance;
  • 通过toClass覆盖原有类后,通过类加载器重新加载。
public void init() {
        try {
            ClassPool classPool = new ClassPool(true);
            //添加com.netflix.discovery包的扫描路径
            ClassClassPath classPath = new ClassClassPath(Applications.class);
            classPool.insertClassPath(classPath);
            //获取要修改Application类
            CtClass ctClass = classPool.get(APPLICATION_PATH);
            //获取addInstance方法
            CtMethod addInstanceMethod = ctClass.getDeclaredMethod("addInstance");
            //修改addInstance方法
            addInstanceMethod.setBody("{instancesMap.put($1.getId(), $1);"
                    + "synchronized (instances) {me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaAddInstance($1);" +
                    "instances.remove($1);instances.add($1);isDirty = true;}}");
            //获取removeInstance方法
            CtMethod removeInstanceMethod = ctClass.getDeclaredMethod("removeInstance");
            //修改removeInstance方法
            removeInstanceMethod.setBody("{me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaRemoveInstance($1);this.removeInstance($1, true);}");
            //覆盖原有的Application类
            ctClass.toClass();
            //使用类加载器重新加载Application类
            classPool.getClassLoader().loadClass(APPLICATION_PATH);
            Class.forName(APPLICATION_PATH);
        } catch (Exception e) {
            throw new EurekaEventException(e);
        }
    }
  • 放入main函数,在spring boot启动前执行或者使用spring boot的事件,在spring bean初始化之前执行。(确保在eureka第一次执行之前执行即可)
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {

    public static void main(String[] args) {
        //先执行修改字节码代码
        EurekaEventHandler.getInstance().init();
        new SpringApplicationBuilder(EurekaClientApplication.class).web(true).run(args);
    }
}

使用JDK中Observable和Observer实现观察者,订阅者模式

  • 发送事件使用java.util.Observable的setChanged和notifyObservers
public class EurekaEventObservable extends Observable {
    public void sendEvent(EurekaEventPayload payload) {
        setChanged();
        notifyObservers(payload);
    }
}
  • 接收事件使用使用java.util.Observer的update
public abstract class AbstractEurekaEventObserver implements Observer, EurekaEventService {
      @Override
        public void update(Observable o, Object arg) {
            if (arg instanceof EurekaEventPayload) {
                EurekaEventPayload payload = (EurekaEventPayload) arg;
                if (InstanceInfo.InstanceStatus.UP.name().equals(payload.getStatus())) {
                    LOGGER.info("Receive UP event, payload: {}", payload);
                } else {
                    LOGGER.info("Receive DOWN event, payload: {}", payload);
                }
                putPayloadInCache(payload);
                consumerEventWithAutoRetry(payload);
            }
        }
}

使用RxJava实现自动重试。

接收到服务启动去执行一些操作,如果执行失败有异常则自动重试指定次数,每个一段事件重试一次,执行成功则不再执行

private void consumerEventWithAutoRetry(final EurekaEventPayload payload) {
    rx.Observable.just(payload)
            .map(t -> {
                // 此处为接收到服务启动去执行的一些操作
                consumerEvent(payload);
                return payload;
            }).retryWhen(x -> x.zipWith(rx.Observable.range(1, retryTime),
            (t, retryCount) -> {
               //异常处理
                if (retryCount >= retryTime) {
                    if (t instanceof RemoteAccessException || t instanceof RestClientException) {
                        LOGGER.warn("error.eurekaEventObserver.fetchError, payload {}", payload, t);
                    } else {
                        LOGGER.warn("error.eurekaEventObserver.consumerError, payload {}", payload, t);
                    }
                }
                return retryCount;
            }).flatMap(y -> rx.Observable.timer(retryInterval, TimeUnit.SECONDS)))
            .subscribeOn(Schedulers.io())
            .subscribe((EurekaEventPayload payload1) -> {
            });
}

添加手动重试失败接口

自动重试失败,可以手动重试,添加手动重试接口

@RestController
@RequestMapping(value = "/v1/eureka/events")
public class EurekaEventEndpoint {

    private EurekaEventService eurekaEventService;

    public EurekaEventEndpoint(EurekaEventService eurekaEventService) {
        this.eurekaEventService = eurekaEventService;
    }

    @Permission(permissionLogin = true)
    @ApiOperation(value = "获取未消费的事件列表")
    @GetMapping
    public List<EurekaEventPayload> list(@RequestParam(value = "service", required = false) String service) {
        return eurekaEventService.unfinishedEvents(service);
    }

    @Permission(permissionLogin = true)
    @ApiOperation(value = "手动重试未消费成功的事件")
    @PostMapping("retry")
    public List<EurekaEventPayload> retry(@RequestParam(value = "id", required = false) String id,
                                          @RequestParam(value = "service", required = false) String service) {
        return eurekaEventService.retryEvents(id, service);
    }

}

案例:修改方法,统计方法的执行时间

@Test
    public void updateGetUserInfoMethod() throws Exception {
        ClassPool pool = new ClassPool();
        pool.appendSystemPath();
        // 定义类
        CtClass userServiceClass = pool.get("com.ty.javaagent.UserServiceImpl");
        // 需要修改的方法
        CtMethod method = userServiceClass.getDeclaredMethod("getUserInfo");
        // 修改原有的方法
        method.setName("getUserInfo$agent");
        // 创建新的方法,复制原来的方法
        CtMethod newMethod = CtNewMethod.copy(method, "getUserInfo", userServiceClass, null);
        // 注入的代码
        StringBuffer body = new StringBuffer();
 
        body.append("{\nlong start = System.currentTimeMillis();\n");
        // 调用原有代码,类似于method();($$)表示所有的参数
        body.append("getUserInfo$agent($$);\n");
        body.append("System.out.println(\" take \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
 
        body.append("}");
        newMethod.setBody(body.toString());
        // 增加新方法
        userServiceClass.addMethod(newMethod);
 
        UserServiceImpl userServiceImpl = (UserServiceImpl) userServiceClass.toClass().newInstance();
        userServiceImpl.getUserInfo();
    }

javaAgent编程

一、写一个Agent

1、编写一个Java类,并包含如下两个方法中的任一个:

public static void premain(String agentArgs, Instrumentation inst); //【1】
public static void premain(String agentArgs); //【2】
其中,【1】和【2】同时存在时,【1】会优先被执行,而【2】则会被忽略。
具体使用如下代码:

import java.lang.instrument.Instrumentation;
public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is an agent.");
        System.out.println("args:" + agentArgs + "\n");
    }
}

2、jar打包
在代码的resources目录下添加META-INF/MANIFEST.MF文件。其目的是指定Premain-Class的类。

Manifest-Version: 1.0
Premain-Class: com.zl.unit1.MyAgent
Can-Redefine-Classes: true

3、在pom.xml中配置打包的相关配置。

    <packaging>jar</packaging>
    <build>
        <finalName>my-agent</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <archive>
                        <index>true</index>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                        <manifest>
                            <addDefaultImplementationEntries/>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
    </plugins>
    </build>

最后,执行mvn clean package,就能生成一个my-agent.jar。

二、运行Agent

新建一个测试类。如下:

public class AgentTest {
    public static void main(String[] args) {
        System.out.println("this is main");
    }
}

命令行运行:java -javaagent: 文件位置 [=参数]
运行结果如下:我这里重复加载了两次Agent,但是传入的参数不同。

this is an agent.
args:first

this is an agent.
args:second

this is main

三、调试Agent

1、使用 Spring Boot 创建一个简单的 Web 项目。AgentDemo用户创建Agent类,agent-test用于外部启动执行Agent类


image.png

友情提示 :这里一定要注意下。创建的 Web 项目,使用 IntelliJ IDEA 的菜单 File / New / Module 或 File / New / Module from Existing Sources ,保证 Web 项目和 Agent 项目平级。这样,才可以使用 IntelliJ IDEA 调试 Agent 。

2、设置线程模式下的断点


image.png

3、执行
运行 Web 项目的 Application 的 #main(args) 方法,并增加 JVM 启动参数,-javaagent:F:\IdeaWorkSpace\AgentDemo\target\my-agent.jar。如下图 :


image.png

————————————————
版权声明:本文为CSDN博主「潘顾昌」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37442469/article/details/107240561

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

推荐阅读更多精彩内容