2024-12-29 基于spring的aop实现热插拔,实现类似链路追踪能力!

1、前序

本人使用过arthas工具,记得刚开始使用最让惊艳的是能够实现请求的链路追踪和耗时分析,之前也研究过arthas源码,但是没有深入研究它的链路追踪如何实现。skywalking也是公式在使用的一个工具,但是由于时间各种原因,一直没有去分析原理。列个TODO:skywalking实现原理。

无意看到一个文章:如何基于spring的aop实现热插拔,看了原理后就思考:这个东西我可以做一个类似arthas的链路追踪能力,需要的时候织入切面,不需要的时候可以移除,也不影响性能。有想法,那就干起来。

2、直接看效果

2.1、未织入切面之前

http://localhost:8080/tracer/run
image.png

平平无奇,通过traceId看是否能够查到请求过程的信息

http://localhost:8080/tracer/get/fa713bc1bc784e0fb3fc14465de11ba8
{
  "code": 200,
  "message": "success",
  "traceId": "e94f805f9d584a2cb3abf5caac72b71c",
  "result": {}
}

发现result中没有数据。我们期望result中是本次请求的链路过程和耗时统计

2.1、织入切面之后

2.1.1、执行织入

curl --location 'http://127.0.0.1:8080/advisor/add?interceptorClass=com.tracer.dynamic.inteceptor.TracerInterceptor&annotationClass=com.tracer.dynamic.annotation.TracerAnnotation'
2024-12-29 21:42:43.386  INFO 23752 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
替换成功
新对象的hashcode=589648775
当前被增强的bean的类名=class com.tracer.service.CourseService
新对象的名称=class com.tracer.service.CourseService$$EnhancerBySpringCGLIB$$c6659b67
需要被替换属性的类名称=com.tracer.service.TeacherService
替换成功
新对象的hashcode=29089716
当前被增强的bean的类名=class com.tracer.service.StudentService
新对象的名称=class com.tracer.service.StudentService$$EnhancerBySpringCGLIB$$349b0a75
需要被替换属性的类名称=com.tracer.controller.TracerController
替换成功
新对象的hashcode=212760089
当前被增强的bean的类名=class com.tracer.service.TeacherService
新对象的名称=class com.tracer.service.TeacherService$$EnhancerBySpringCGLIB$$c45eb380
需要被替换属性的类名称=com.tracer.service.StudentService
image.png

通过日志可以看出,有三个类被增强了。

2.1.2、执行业务,查看请求链路

再次请求接口

http://localhost:8080/tracer/fun
  "code": 200,
  "message": "success",
  "traceId": "297b7941823342c7b11a46116994a71e",
  "result": "student=zansan, teacher=zansan, CourseName=math"
}

通过traceId看是否能够查到请求过程的信息

http://localhost:8080/tracer/get/297b7941823342c7b11a46116994a71e
{
  "code": 200,
  "message": "success",
  "traceId": "a82542072bc646dd994e52746e0d6adb",
  "result": {
    "com.tracer.controller.TracerController#fun": 783,
    "com.tracer.service.StudentService#getStudentName": 801,
    "com.tracer.service.TeacherService#getTeacherName": 393,
    "com.tracer.service.CourseService#getCourseName": 58
  }
}

每个方法通过休眠模式业务耗时

fun sleep 774ms
StudentService sleep 798ms
TeacherService sleep 384ms
CourseService sleep 55ms

可以看到时间基本准确,存在几毫米的差距原因是,切面还有其他逻辑需要耗时。

3、核心实现

3.1、TracerInterceptor

package com.tracer.dynamic.inteceptor;

import com.tracer.tracer.StatisticContext;
import com.tracer.tracer.TraceContext;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Stack;

/**
 * 链路追踪,耗时统计
 */
public class TracerInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        long beginTs = System.currentTimeMillis();

        Map<String, String> map = TraceContext.tradeIdThreadLocal.get();
        String tracerId = map.get("tracerId");

        Method method = invocation.getMethod();
        String methodName = method.getName();

        // 如果是toString()方法或者equals方法跳过
        if(methodName.equals("toString") || methodName.equals("equals")){
            return invocation.proceed();
        }

        String className = method.getDeclaringClass().getName();

        String currentPath = className + "#" + methodName;

        Stack<String> path = (Stack<String>) TraceContext.pathThreadLocal.get();
        if (path == null) {
            path = new Stack<>();
            TraceContext.pathThreadLocal.set(path);
        }
        path.push(currentPath); // 当前请求路径入栈

        Object result = invocation.proceed();

        TraceContext.TraceNode preNode = (TraceContext.TraceNode) TraceContext.tradeNodeThreadLocal.get();

        TraceContext.TraceNode currentNode = new TraceContext.TraceNode(currentPath, System.currentTimeMillis() - beginTs, tracerId);
        if (preNode == null) {
            currentNode.next = null;
        } else {
            currentNode.next = preNode;
        }
        TraceContext.tradeNodeThreadLocal.set(currentNode);

        path = (Stack<String>) TraceContext.pathThreadLocal.get();
        path.pop(); // 当前请求路径出栈
        if (path.isEmpty()) {
            TraceContext.TraceNode traceNode = (TraceContext.TraceNode) TraceContext.tradeNodeThreadLocal.get();
            StatisticContext.traceNodeHashMap.put(tracerId, traceNode);
            TraceContext.tradeNodeThreadLocal.remove();
        }
        return result;
    }
}

实现请求链路追踪的逻辑都在这里。

3.2、AdvisorController

package com.tracer.controller;

import cn.hutool.core.text.CharSequenceUtil;
import com.tracer.dynamic.DynamicProxy;
import com.tracer.dynamic.OperateEventEnum;
import com.tracer.dynamic.dynamicAdvisor;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/advisor")
@Slf4j
public class AdvisorController {
    @Resource
    private DynamicProxy dynamicProxy;
    private static final Map<String, dynamicAdvisor> advisorMap = new HashMap<>();

    /**
     * 基于注解:自定义注解或者spring的注解都可以
     * http://127.0.0.1:8080/advisor/add?interceptorClass=com.tracer.dynamic.inteceptor.TracerInterceptor&annotationClass=com.tracer.dynamic.annotation.TracerAnnotation
     * http://127.0.0.1:8080/advisor/add?interceptorClass=com.tracer.dynamic.inteceptor.TracerInterceptor&annotationClass=org.springframework.stereotype.Component
     *
     * 基于表达式:execution(* com.tracer.service.*.*()) // service包下所有类的无参数方法
     *
     * http://127.0.0.1:8080/advisor/add?interceptorClass=com.tracer.dynamic.inteceptor.TracerInterceptor&expression=execution(* com.tracer.service.*.*())
     *
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/add")
    public String add(String interceptorClass, String expression, String annotationClass) throws Exception {
        if (CharSequenceUtil.isAllBlank(expression, annotationClass) || CharSequenceUtil.isBlank(interceptorClass)) {
            return "the parameter is abnormal";
        }
        if (advisorMap.containsKey(interceptorClass + annotationClass) || advisorMap.containsKey(interceptorClass + expression)) {
            return "advisor already exists";
        }
        MethodInterceptor methodInterceptor = (MethodInterceptor) Class.forName(interceptorClass).getDeclaredConstructor().newInstance();
        dynamicAdvisor dynamicAdvisor;
        // 以注解为主,有注解就用注解
        if (CharSequenceUtil.isNotBlank(annotationClass)) {
            Class<? extends Annotation> aClass = (Class<? extends Annotation>) Class.forName(annotationClass);
            dynamicAdvisor = new dynamicAdvisor(aClass, methodInterceptor);
            advisorMap.put(interceptorClass + annotationClass, dynamicAdvisor);
        } else {
            dynamicAdvisor = new dynamicAdvisor(expression, methodInterceptor);
            advisorMap.put(interceptorClass + expression, dynamicAdvisor);
        }
        dynamicProxy.operateAdvisor(dynamicAdvisor, OperateEventEnum.ADD);
        return "advisor add success";
    }


    @GetMapping(value = "/delete")
    public String delete(String interceptorClass, String expression, String annotationClass) {
        if (CharSequenceUtil.isAllBlank(expression, annotationClass) || CharSequenceUtil.isBlank(interceptorClass)) {
            throw new IllegalArgumentException("参数异常");
        }

        if (!advisorMap.containsKey(interceptorClass + annotationClass) && !advisorMap.containsKey(interceptorClass + expression)) {
            return "advisor not exists";
        }

        // 以注解为主,有注解就用注解
        StringBuilder advisorKey = new StringBuilder(interceptorClass);
        if (CharSequenceUtil.isNotBlank(annotationClass)) {
            advisorKey.append(annotationClass);
        } else {
            advisorKey.append(expression);
        }
        dynamicAdvisor dynamicAdvisor = advisorMap.get(advisorKey.toString());
        dynamicProxy.operateAdvisor(dynamicAdvisor, OperateEventEnum.DELETE);
        advisorMap.remove(advisorKey.toString());
        return "advisor delete success";
    }

}


3.3、DynamicAdvisor

package com.tracer.dynamic;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;

import java.lang.annotation.Annotation;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

public class DynamicAdvisor extends AbstractPointcutAdvisor {
    private final Advice advice; // interceptor
    private final Pointcut pointcut;

    public dynamicAdvisor(Class<? extends Annotation>  annotationClass, MethodInterceptor interceptor) {
        this.advice = interceptor;
        this.pointcut = buildPointcut(annotationClass);
    }

    public dynamicAdvisor(String expression, MethodInterceptor interceptor) {
        this.advice = interceptor;
        this.pointcut = buildPointcut(expression);
    }

    /**
     * 直接复制的@Async构建pointcut的代码
     * @param annotationType  interface com.tracer.dynamic.annotation.XdxAnnotation
     * @return
     */
    private Pointcut buildPointcut(Class<? extends Annotation> annotationType) {
        Set<Class<? extends Annotation>> annotationTypes = new LinkedHashSet<>(2);
        annotationTypes.add(annotationType);
        ComposablePointcut result = null;
        AnnotationMatchingPointcut mpc;
        for(Iterator var3 = annotationTypes.iterator(); var3.hasNext(); result = result.union(mpc)) {
            Class<? extends Annotation> asyncAnnotationType = (Class)var3.next();
            Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
            mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
            if (result == null) {
                result = new ComposablePointcut(cpc);
            } else {
                result.union(cpc);
            }
        }

        return result != null ? result : Pointcut.TRUE;
    }

    private Pointcut buildPointcut(String expression) {
        AspectJExpressionPointcut tmpPointcut = new AspectJExpressionPointcut();
        tmpPointcut.setExpression(expression);
        return  tmpPointcut;
    }

    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }
    @Override
    public Advice getAdvice() {
        return advice;
    }
}

这块代码能否实现基于注解或者表达式的切点构建。

3.4、DynamicProxy

package com.tracer.dynamic;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.*;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Field;

@Configuration
public class DynamicProxy extends ProxyProcessorSupport implements BeanFactoryAware {
    @Autowired
    private ConfigurableApplicationContext context;
    private static DefaultListableBeanFactory beanFactory;

    public void operateAdvisor(DynamicAdvisor advisor, OperateEventEnum operateEventEnum) {
        // 循环每一个bean
        for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
            Object bean = beanFactory.getBean(beanDefinitionName);
            // 判断当前bean是否匹配:注解或者表达式
            if (!isEligible(bean, advisor)) {
                continue;
            }

            // 判断当前bean是不是已经是代理对象了,是就直接进行 Advisor 操作
            if (bean instanceof Advised) {
                Advised advised = (Advised) bean;
                if (operateEventEnum == OperateEventEnum.DELETE) {
                    advised.removeAdvisor(advisor);
                } else if (operateEventEnum == OperateEventEnum.ADD) {
                    advised.addAdvisor(advisor);
                }

                setField(bean, advised);
                continue;
            }
            // 生成 Advisor 的代理对象
            ProxyFactory proxyFactory = new ProxyFactory();
            proxyFactory.addAdvisor(advisor);
            proxyFactory.setTarget(bean);
            ClassLoader classLoader = this.getProxyClassLoader();
            Object proxy = proxyFactory.getProxy(classLoader);


            // 遍历beanFactory所有的单例bean,找到bean的所有属性,如果属性和beanDefinitionName一样,就替换成代理对象
            setField(bean, proxy);

            // 删除已有的 Bean 定义
//            beanFactory.removeBeanDefinition(beanDefinitionName);
//            // 使用 BeanDefinitionRegistry 或 ConfigurableListableBeanFactory 动态替换 Bean
//            RootBeanDefinition beanDefinition = new RootBeanDefinition(proxy.getClass());
//            beanFactory.registerBeanDefinition(beanDefinitionName, beanDefinition);

            // 如果已经存在,则先销毁
            if (beanFactory.containsSingleton(beanDefinitionName)) {
                unregisterSingleton(beanDefinitionName);
            }
            registerSingleton(beanDefinitionName, proxy, beanDefinitionName);

        }
    }

    private static void setField(Object bean, Object newObject) {
        // 遍历beanFactory所有的单例bean,找到bean的所有属性,如果属性和beanDefinitionName一样,就替换成代理对象
//        Iterator<String> beanNamesIterator = beanFactory.getDependenciesForBean();
        String[] names = beanFactory.getBeanDefinitionNames();
        for (String name : names) {
            Object b = beanFactory.getBean(name);
            if (b == bean) {
                continue;
            }

            // 原始对象
            Object target=b;

            Field[] fields = null;
            // 如果b是代理对象,就获取原始对象的属性
            if (AopUtils.isAopProxy(b)) {
                try {
                    // 获取原始对象
                    target = getTargetObject(b);
                    fields = target.getClass().getDeclaredFields();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                fields = b.getClass().getDeclaredFields();
            }

            for (Field field : fields) {
                if (field.getType() == bean.getClass()) {
                    try {
                        field.setAccessible(true);
                        field.set(target, newObject);
                        field.setAccessible(false);
                        System.out.println("替换成功");
                        System.out.println("新对象的hashcode="+newObject.hashCode());
                        System.out.println("当前被增强的bean的类名="+bean.getClass());
                        System.out.println("新对象的名称="+newObject.getClass());
                        System.out.println("需要被替换属性的类名称="+target.getClass().getName());
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    // 获取代理对象的原始对象
    private static Object getTargetObject(Object proxy) throws Exception {
        if (AopUtils.isJdkDynamicProxy(proxy)) {
            // 对于 JDK 动态代理,通过反射获取代理对象的 'h' 属性
            try{
                Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
                h.setAccessible(true);
                AopProxy aopProxy = (AopProxy) h.get(proxy);
                Field advised = aopProxy.getClass().getDeclaredField("advised");
                advised.setAccessible(true);
                Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
                return target;
            } catch(Exception e){
                e.printStackTrace();
            }
        } else {
            // 对于 CGLIB 代理,通过反射获取目标对象
            try{
                Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
                h.setAccessible(true);
                Object dynamicAdvisedInterceptor = h.get(proxy);
                Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
                advised.setAccessible(true);
                Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
                return target;
            } catch(Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 注销Bean
     *
     * @param beanName 名称
     */
    public static void unregisterSingleton(String beanName) {
        if (beanFactory instanceof DefaultListableBeanFactory) {
            // 首先确保销毁该bean的实例(如果该bean实例是一个单例的话)
            if (beanFactory.containsSingleton(beanName)) {
                beanFactory.destroySingleton(beanName);
            }
            // 然后从容器的bean定义注册表中移除该bean定义
            if (beanFactory.containsBeanDefinition(beanName)) {
                beanFactory.removeBeanDefinition(beanName);
            }
        }
    }

    public static void registerSingleton(String beanName, Object proxy, String beanDefinitionName) {
        if (beanFactory instanceof DefaultListableBeanFactory) {
            RootBeanDefinition beanDefinition = new RootBeanDefinition(proxy.getClass());
            beanFactory.registerBeanDefinition(beanDefinitionName, beanDefinition);
            beanFactory.registerSingleton(beanName, proxy);
        }
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        DynamicProxy.beanFactory = (DefaultListableBeanFactory) beanFactory;
    }

    /**
     * 复制的 @Async 的匹配逻辑
     */
    private boolean isEligible(Object bean, Advisor advisor) {
        return AopUtils.canApply(advisor, bean.getClass());
    }
}

这块代码实现:遍历每个bean,对匹配的方法的类,或者指定注解的类进行切面织入,并且如果bean已经是代理对象,也可以增强,bean如果不是代理对象,那就构建代理对象实现增强。如果一个bean被增强了,要扫描所有的bean,找到这个bean被引用的地方,是实现对象替换(否者@Autowired的属性不会被替换)。

3.5、TraceContext

package com.tracer.tracer;

import java.util.Map;
import java.util.Stack;

public class TraceContext {
    public static ThreadLocal tradeNodeThreadLocal = new ThreadLocal<TraceNode>();
    public static ThreadLocal<Map<String,String>> tradeIdThreadLocal = new ThreadLocal<>();
    // 请求路径
    public static ThreadLocal pathThreadLocal = new ThreadLocal<Stack<String>>();

    public static class TraceNode {
        public TraceNode next;
        public String path;
        public String traceId;
        public long time;

        public TraceNode(String path, long time, String traceId) {
            this.path = path;
            this.time = time;
            this.traceId = traceId;
        }
    }
}

3.6、TracerController

package com.tracer.controller;


import com.tracer.service.StudentService;
import com.tracer.tracer.StatisticContext;
import com.tracer.result.R;
import com.tracer.dynamic.annotation.TracerAnnotation;
import com.tracer.tracer.TraceContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Random;

@RestController
@RequestMapping("/tracer")
@Slf4j
public class TracerController {

    /**
     * http://localhost:8080/tracer/get/0407ab3dbd8d484690f2079c0ef72aae
     *
     * @param tracerId
     * @return
     */
    @RequestMapping("/get/{tracerId}")
    public R traceIdTest(@PathVariable("tracerId") String tracerId) {
        TraceContext.TraceNode node = (TraceContext.TraceNode) StatisticContext.traceNodeHashMap.get(tracerId);
        HashMap<String, Long> path = new LinkedHashMap<>();

        // 遍历node,获取所有的className和time
        TraceContext.TraceNode preNode = null;
        while (node != null) {
            path.put(node.path, node.time);
            if (preNode != null) {
               // 上一个节点的耗时时间减去当前节点的耗时时间为上一个节点本身的耗时时间
                path.put(preNode.path, preNode.time - node.time);
            }
            preNode = node;

            node = node.next;
        }
        return R.restResult(path, 200, "success");
    }

    @Autowired
    private StudentService studentService;

    /**
     * 用来测试效果的fun
     *
     * @return
     */
    @GetMapping(value = "/fun")
    @TracerAnnotation
    public R fun() throws InterruptedException {
        // 业务
        int t = new Random().nextInt(1000);
        System.out.println("fun sleep " + t + "ms");
        Thread.sleep(t);
//        StudentService s = ApplicationContextUtil.getBean(StudentService.class);
        String studentName = studentService.getStudentName();
        return R.restResult(studentName, 200, "success");
    }
}


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

推荐阅读更多精彩内容