java debug 体系-jdi

      JDI属于JPDA中最上层接口。定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。

0、工作方式

      首先,调试器(Debuuger)通过 Bootstrap 获取唯一的虚拟机管理器。
虚拟机管理器将在第一次被调用时初始化可用的链接器。一般地,调试器会默认地采用启动型链接器进行链接。
      然后,调试器调用链接器的 launch () 来启动目标程序,并完成调试器与目标虚拟机的链接。
      当链接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过链接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

1、模块划分

      通过上面的描述,我们可以将jdi分成3部分: 数据模块、连接模块、事件处理模块。

1.1 数据模块

      jdi的数据模块,主要就是Mirror机制。Mirror 接口是JDI最底层的接口,JDI中几乎所有其他接口都继承于它。Mirror 机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。

例如,在目标虚拟机上,已装载的类被映射成 ReferenceType 镜像,对象实例被映射成 ObjectReference 镜像,基本类型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中,在调试过程中所触发的事件被映射成 Event 镜像(如 StepEvent 等),调试器发出的事件请求被映射成 EventRequest 镜像(如 StepRequest 等),被调试的目标虚拟机则被映射成 VirtualMachine 镜像。但是,JDI 并不保证目标虚拟机上的每份信息和资源都只有唯一的镜像与之对应,这是由 JDI 的具体实现所决定的。例如,目标虚拟机上的某个事件有可能存在多个 Event 镜像与之对应,例如 BreakpointEvent 等。

      Mirror 实例或是由调试器创建,或是由目标虚拟机创建,调用 Mirror 实例 virtualMachine() 可以获取其虚拟机信息。该接口提供了一套方法,可以用来直接或间接地获取目标虚拟机上所有的数据和状态信息,也可以挂起、恢复、终止目标虚拟机。

1.2 连接模块

      连接是调试器与目标虚拟机之间交互的渠道,一次连接可以由调试器发起,也可以由被调试的目标虚拟机发起。一个调试器可以连接多个目标虚拟机,但一个目标虚拟机最多只能连接一个调试器。下面的例子中就讲了一种常见的连接方式: 由调试器启动目标虚拟机的连接方式。也可以在虚拟机处于运行状态时,采用attach的方式连接到目标虚拟机(我们平时用的Intellij 用的就是这种方式)。

1.3 事件处理模块

      主要在com.sun.jdi.event 和 com.sun.jdi.request 包中。

  • 事件类型: JDI中事件的接口叫Event . 它定义了18种具体的事件类型。


    事件类型

    事件集是事件发送的最小单位,并且事件集一旦被创建,则不可以被修改。

  • 事件请求:Event接口定义了request方法,该方法会返回由调试器Debugger发出的针对该事件的事件请求(EventRequest)。事件请求是由调试器向目标虚拟机发出的,目的是请求目标虚拟机在发生指定的事件后通知调试器。只有当调试器发出的请求与目标虚拟机上发生的事件匹配时,这些事件才会被分发到各个事件集,进而等待发送至调试器端。

      当然了,Debugger发送给Target VM的所有事件请求,不一定Target VM 都感兴趣。因此JDI提供了事件的过滤机制,来删选出最终真正要发送给Target VM的事件。

  • 对事件请求的管理: 在JDI中,事件请求的管理是通过EventRequestManager来完成的。它有许多createXXXRequest方法来创建不同类型的事件请求,也有许多deleteXXXRequest方法来删除不同类型的事件请求,还有xxxRequests方法来列出各种类型的事件请求。有一点需要注意的是,这里由EventRequestManager创建的createXXXRequest的事件都是非激活的,因此这些事件请求当发送给Target VM不会起任何作用,除非调用EventRequest的setEnable(true)使得该事件进入激活状态。
  • 事件队列:事件队列(EventQueue)的拥有者是目标虚拟机,EventQueue 将这些事件集以“先进先出”策略依次地发送到调试器端。EventQueue 负责管理来自目标虚拟机的事件,一个被调试的目标虚拟机上有且仅有一个 EventQueue实例。
  • Debugger 跟 targetVM之间的事件交互:
  • Debugger调用Target VM的 eventQueue() 和 eventRequestManager() 分别获取唯一的 EventQueue 实例和 EventRequestManager 实例.
  • Debugger通过 EventRequestManager 的 createXXXRequest() 创建需要的事件请求,并添加过滤器和设置挂起策略.
  • targetVM 上某个事件触发且匹配上eventRequest , 则将event放入对应的eventSet.
  • targetVM 上的EventQueue 管理这些eventSet, 按照FIFO原则发送给Debugger.
  • Debugger通过第一步获取到的EventQueue实例 获取来自Target VM的事件响应。

一句话概括就是 EventRequest总是由Debugger发向Target VM ,而当请求与目标虚拟机上发生事件匹配,则事件会被归到EventSet中,EventSet会被Target VM的EventQueue所管理,并且按照FIFO原则发送到Debugger

2、一个例子

(1)首先来个测试

public class HelloWorld {
    public static void main(String[] args) {
        String str = "Hello world!";
        System.out.println(str);
    }
}

(2)JDI agent的例子

import java.util.List;
import java.util.Map;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
/**
 * Created by zhangpeng48 on 2018/7/16.
 */
public class MethodTrace {
    private static VirtualMachine vm;
    private static Process process;
    private static EventRequestManager eventRequestManager;
    private static EventQueue eventQueue;
    private static EventSet eventSet;
    private static boolean vmExit = false;
    //write your own testclass
    private static String className = "HelloWorld";

    public static void main(String[] args) throws Exception {
        System.out.println("begin....");
        launchDebugee();
        registerEvent();

        processDebuggeeVM();

        // Enter event loop
        eventLoop();

        destroyDebuggeeVM();

    }

    public static void launchDebugee() {
        LaunchingConnector launchingConnector = Bootstrap
                .virtualMachineManager().defaultConnector();

        // Get arguments of the launching connector
        Map<String, Connector.Argument> defaultArguments = launchingConnector
                .defaultArguments();
        Connector.Argument mainArg = defaultArguments.get("main");
        Connector.Argument suspendArg = defaultArguments.get("suspend");

        // Set class of main method
        mainArg.setValue(className);
        suspendArg.setValue("true");
        try {
            vm = launchingConnector.launch(defaultArguments);
        } catch (Exception e) {
            // ignore
        }
    }

    public static void processDebuggeeVM() {
        process = vm.process();
    }

    public static void destroyDebuggeeVM() {
        process.destroy();
    }

    public static void registerEvent() {
        // Register ClassPrepareRequest
        eventRequestManager = vm.eventRequestManager();
        MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();

        entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        entryReq.addClassFilter(className);
        entryReq.enable();

        MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
        exitReq.addClassFilter(className);
        exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        exitReq.enable();
    }

    private static void eventLoop() throws Exception {
        eventQueue = vm.eventQueue();
        while (true) {
            Thread.sleep(10000);
            if (vmExit == true) {
                System.out.println("vmexit");
                break;
            }
            eventSet = eventQueue.remove();
            EventIterator eventIterator = eventSet.eventIterator();
            while (eventIterator.hasNext()) {
                Event event = (Event) eventIterator.next();
                execute(event);
                if (!vmExit) {
                    eventSet.resume();
                }
            }
        }
    }

    private static void execute(Event event) throws Exception {
        if (event instanceof VMStartEvent) {
            System.out.println("VM started");
        } else if (event instanceof MethodEntryEvent) {
            Method method = ((MethodEntryEvent) event).method();
            System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
            System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
        } else if (event instanceof MethodExitEvent) {
            Method method = ((MethodExitEvent) event).method();
            System.out.printf("Exit -> method: %s\n",method.name());
        } else if (event instanceof VMDisconnectEvent) {
            vmExit = true;
        }
    }
}

(3)编译与运行
编译测试 HelloWorld :

javac HelloWorld.java

编译 JDI agent:

#注意 classpath 多个引用时,在linux环境使用“:”分割。
javac -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace.java    

运行:

java -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace HelloWorld

结果是这个样子的~

结果

4)demo下载地址
链接: https://pan.baidu.com/s/1bKVKexvWKCh6F0hG4nkRDQ 密码: etk3

3、核心api的分析

源码地址:
hotspot实现: hotspot的实现
JDK自带实现: openjdk 的实现

本文主要是对JDK自带的实现进行分析~

3.1 launchDebugee

launchDebugee

首先获取一个获取虚拟机管理器virtualmachineManager, 然后获取默认的连接器

public List<LaunchingConnector> launchingConnectors() {
        ArrayList var1 = new ArrayList(this.connectors.size());
        Iterator var2 = this.connectors.iterator();

        while(var2.hasNext()) {
            Connector var3 = (Connector)var2.next();
            if(var3 instanceof LaunchingConnector) {
                var1.add((LaunchingConnector)var3);
            }
        }

        return Collections.unmodifiableList(var1);
    }

然后为连接器设置参数 主要是设置main(mainArg.setValue 设置该main对应哪个类), 跟suspend (suspend=y/n 是否在调试客户端建立连接之后启动 VM 。如果是y,则方便调试vm启动过程中的一些步骤。本文设置为true.)
最后 launch ( 启动目标程序,连接调试器(Debuuger)与目标虚拟机(VirtualMachine)) 。主要是创建了一个socket 以及一个VM . 代码如下。

public VirtualMachine launch(Map<String, ? extends Argument> var1) throws IOException, IllegalConnectorArgumentsException, VMStartException {
        // 获取各种参数
            String var3 = this.argument("home", var1).value();
        String var4 = this.argument("options", var1).value();
        String var5 = this.argument("main", var1).value();
        boolean var6 = ((BooleanArgumentImpl)this.argument("suspend", var1)).booleanValue();
        String var7 = this.argument("quote", var1).value();
        String var8 = this.argument("vmexec", var1).value();
        String var9 = null;
        if(var7.length() > 1) {
            throw new IllegalConnectorArgumentsException("Invalid length", "quote");
        } else if(var4.indexOf("-Djava.compiler=") != -1 && var4.toLowerCase().indexOf("-djava.compiler=none") == -1) {
            throw new IllegalConnectorArgumentsException("Cannot debug with a JIT compiler", "options");
        } else {
            // 进入主题
            ListenKey var10;
            String var13;
            if(!this.usingSharedMemory) {
              // 本文分析的是SunCommandLineLauncher ,本默认走这个分支。  下面函数的作用是绑定了一个socket 到 SocketTransportService 
                var10 = this.transportService().startListening();
            } else {
                Random var11 = new Random();
                int var12 = 0;

                while(true) {
                    try {
                        var13 = "javadebug" + String.valueOf(var11.nextInt(100000));
                        var10 = this.transportService().startListening(var13);
                        break;
                    } catch (IOException var18) {
                        ++var12;
                        if(var12 > 5) {
                            throw var18;
                        }
                    }
                }
            }
                        // 获取了socket的地址。
            String var19 = var10.address();
                        // 创建了一个VM.
            VirtualMachine var2;
            try {
                if(var3.length() > 0) {
                    var9 = var3 + File.separator + "bin" + File.separator + var8;
                } else {
                    var9 = var8;
                }

                if(hasWhitespace(var9)) {
                    var9 = var7 + var9 + var7;
                }
                                // 组装参数: transport , address, suspend
                String var20 = "transport=" + this.transport().name() + ",address=" + var19 + ",suspend=" + (var6?'y':'n');
                if(hasWhitespace(var20)) {
                    var20 = var7 + var20 + var7;
                }
                                // 继续组装参数 组装之后的结构类似: "java ${jrePath} -Xdebug -Xrunjdwp:transport=dt_socket,address=local:40023,suspend=y"
                var13 = var9 + ' ' + var4 + ' ' + "-Xdebug " + "-Xrunjdwp:" + var20 + ' ' + var5;
                // 核心。 launcher. VM的创建  下面重点分析一下。
                var2 = this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService());
            } finally {
                this.transportService().stopListening(var10);
            }

            return var2;
        }
    }

this.transportService().startListening(); 绑定了一个socket 到 SocketTransportService 。 分析如下:

ListenKey startListening(String var1, int var2) throws IOException {
        InetSocketAddress var3;
        if(var1 == null) {
            // 创建InetSocketAddress 里面包含了ip 跟 port , 此处ip为 0.0.0.0 (Returns the InetAddress representing anyLocalAddress)
            var3 = new InetSocketAddress(var2);
        } else {
            var3 = new InetSocketAddress(var1, var2);
        }
                // 创建socket 
        ServerSocket var4 = new ServerSocket();
            // socket 的bind
        var4.bind(var3);
        return new SocketTransportService.SocketListenKey(var4);
    }

this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService()); 的实现

protected VirtualMachine launch(String[] var1, String var2, ListenKey var3, TransportService var4) throws IOException, VMStartException {
        AbstractLauncher.Helper var5 = new AbstractLauncher.Helper(var1, var2, var3, var4);
        var5.launchAndAccept(); // socket 执行 accept ,connect
        VirtualMachineManager var6 = Bootstrap.virtualMachineManager(); 
        return var6.createVirtualMachine(var5.connection(), var5.process());// 建立一个virtualMachineManager 
    }

createVirtualMachine 中最核心的就是 创建了一个 VirtualMachineImpl。

VirtualMachineImpl(VirtualMachineManager var1, Connection var2, Process var3, int var4) {
        super((VirtualMachine)null);
        this.vm = this;
        this.vmManager = (VirtualMachineManagerImpl)var1;
        this.process = var3;
        this.sequenceNumber = var4;
        this.threadGroupForJDI = new ThreadGroup(this.vmManager.mainGroupForJDI(), "JDI [" + this.hashCode() + "]");
            // 创建一个TargetVM, 这个过程新建了一个线程,并且设置为后台常驻。
        this.target = new TargetVM(this, var2);
        
        EventQueueImpl var5 = new EventQueueImpl(this, this.target);
            // 新增一个 event处理器 线程。 
        new InternalEventHandler(this, var5);
        this.eventQueue = new EventQueueImpl(this, this.target);
            // new一个事件请求管理器
        this.eventRequestManager = new EventRequestManagerImpl(this);
            // 起飞。。。
        this.target.start();

        IDSizes var6;
        try {
            var6 = IDSizes.process(this.vm);
        } catch (JDWPException var8) {
            throw var8.toJDIException();
        }
        // 下面的内容可能跟mirror中提到的各种镜像有关。。。
        this.sizeofFieldRef = var6.fieldIDSize; // 属性相关的镜像
        this.sizeofMethodRef = var6.methodIDSize;  // 方法相关的镜像
        this.sizeofObjectRef = var6.objectIDSize; // 对象实例 相关的镜像
        this.sizeofClassRef = var6.referenceTypeIDSize;  // 类相关的镜像
        this.sizeofFrameRef = var6.frameIDSize;   //   StackFrame 镜像
            
        this.internalEventRequestManager = new EventRequestManagerImpl(this);
        //  createClassPrepareRequest  相关的event
            ClassPrepareRequest var7 = this.internalEventRequestManager.createClassPrepareRequest();
        var7.setSuspendPolicy(0);
        var7.enable();
            //  createClassUnloadRequest  相关的event
        ClassUnloadRequest var9 = this.internalEventRequestManager.createClassUnloadRequest();
        var9.setSuspendPolicy(0);
        var9.enable();
        this.notifyInitCompletion();
    }

当连接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过连接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

再补充个知识点: 如果采用jdwp agent, 则建立完连接之后,第一件事情就是handshake 看一下代码。

void handshake(Socket s, long timeout) throws IOException {
        s.setSoTimeout((int)timeout);
 
        byte[] hello = "JDWP-Handshake".getBytes("UTF-8");
        s.getOutputStream().write(hello);
 
        byte[] b = new byte[hello.length];
        int received = 0;
        while (received < hello.length) {
            int n;
            try {
                n = s.getInputStream().read(b, received, hello.length-received);
            } catch (SocketTimeoutException x) {
                throw new IOException("handshake timeout");
            }
            if (n < 0) {
                s.close();
                throw new IOException("handshake failed - connection prematurally closed");
            }
            received += n;
        }
        for (int i=0; i<hello.length; i++) {
            if (b[i] != hello[i]) {
                throw new IOException("handshake failed - unrecognized message from target VM");
            }
        }
 
        // disable read timeout
        s.setSoTimeout(0);

发送的"JDWP-Handshake"就是JDWP协议里面规定的。在连接建立之后,发送数据包之前,debugger跟debuggee必须要有一个handshake的过程,handshake分为两步,

    1. debugger发送14个字节,也就是JDWP-Handshake,给debuggee;
    1. debuggee发送同样的14个字节回应;

JDWP协议的细节将在另外一篇文章介绍。

3.2 registerEvent

注册了2个event.

 public void registerEvent() {
        // Register ClassPrepareRequest  这个方法继承了mirrorImpl, mirror机制也是我们后面分析的重点。
        eventRequestManager = vm.eventRequestManager();
        // 创建一个方法进入的event
            MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();
        entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        entryReq.addClassFilter(className);
        entryReq.enable();

            // 创建一个方法退出的event
        MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
        exitReq.addClassFilter(className);
        exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        exitReq.enable();
    }

3.3 processDebuggeeVM

vm.process(). vm 跑起来。

3.4 eventLoop

事件循环,具体处理在execute.

private void eventLoop() throws Exception {
        eventQueue = vm.eventQueue();
        while (true) {
            Thread.sleep(10000);
            if (vmExit == true) {
                System.out.println("vmexit");
                break;
            }
            eventSet = eventQueue.remove();
            EventIterator eventIterator = eventSet.eventIterator();
            while (eventIterator.hasNext()) {
                Event event = (Event) eventIterator.next();
                execute(event);
                if (!vmExit) {
                    eventSet.resume();
                }
            }
        }
    }

execute: 目前只是简单地输出了各种event的日志。

private void execute(Event event) throws Exception {
        if (event instanceof VMStartEvent) {
            System.out.println("VM started");
        } else if (event instanceof MethodEntryEvent) {
            Method method = ((MethodEntryEvent) event).method();
            System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
            System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
        } else if (event instanceof MethodExitEvent) {
            Method method = ((MethodExitEvent) event).method();
            System.out.printf("Exit -> method: %s\n",method.name());
        } else if (event instanceof VMDisconnectEvent) {
            vmExit = true;
        }
    }

3.5 destroyDebuggeeVM

process.destroy();

参考文献

1、JPDA 架构研究20 - JDI的事件请求和处理模块
2、https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html?ca=drs-
3、https://yq.aliyun.com/articles/56?hmsr=toutiao.io&spm=5176.100240.searchblog.18&utm_medium=toutiao.io&utm_source=toutiao.io

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,657评论 18 139
  • 文章来源:美团点评技术团队 作为移动开发者,最头疼的莫过于遇到产品上线以后出现了bug,但是本地开发环境又无法复现...
    程序农猿阅读 1,613评论 3 2
  • 我是007-6111号梁海霞,能与大家一起见证成长是一件乐事,更是一件幸事。 目前,本人在一家民企担任行政管理工作...
    海蓝蓝_4f9e阅读 101评论 0 0
  • 2017.6.28(星期三) 1.重新拾起简书,每周更新文章,并下定决心每周至少更新一篇文章,风雨无阻。 2.买了...
    锅包鱼儿阅读 222评论 0 1
  • 无论任何时候当我回首,你还在,人未老,时光已过,岁月静好! 往事是尘封在记忆中的梦,而你是我唯一鲜明的记忆,那绿叶...
    玲玲d阅读 847评论 0 0