Java常用命令

javac命令

       说到常用命令,首先就不得不提javac命令,它是java的编译命令,开发人员编写的java源码,经过javac的命令处理后,会生成对应的字节码文件,文件名与源码名称一致,并且以class后缀结尾。它的使用格式为:

javac [ options ][ sourcefiles ] [ classes ][ @argfiles ]

【options】: 命令行参数

【sourcefiles】:源码文件,可以一次编译多个源文件

【classes】:这个参数主要是用于指定编译的文件路径,比如可以指定 package.类名.class

【@argfiles】:该参数主要用于多个类编译的时候,可以在一个文件中列举出来需要编译的文件,但是这些文件中不允许使用-J参数。

javac的编译主要有两种方式:

  • 如果编译的文件比较少,可以直接在命令上挨个将其列出,然后一起编译

  • 如果需要编译的文件太多,可以将需要编译的文件写在一个文件中,通过空格或者换行区分,然后在编译的时候,就用【@文件名】的形式进行编译,也就是上面说的第四个参数

       需要明确的是:待编译的源码文件必须以“.java”为后缀,编译后的字节码文件必须是以“.class”为后缀,而且两个的名字必须一致,例如:源码文件“HelloWorld.java”,编译后的文件必然是“HelloWorld.class”。如果类中指定了package包名,那么源码和编译后的二进制文件必须在与包名一致的文件夹目录下,例如:包名为"com.test",如果此时java文件需要放在E:\workspace目录下,那HelloWorld.java放置的位置就是:"E:\workspace\com\test\HelloWorld.java",相应的class文件也是如此。

       如果类中存在内部类,则在编译的时候除去public类会产生一个class文件外,每一个内部类都会单独生成一个class文件,文件名称是“外部类名称内部类名称.class”。如:HelloWorld.java中如果有一个内部类InnerClass,javac编译之后,会产生两个文件:HelloWorld.class、HelloWorldInnerClass.class两个文件。

       默认情况下,编译产生的class文件会与对应源码java文件所处目录一样,可以通过-d参数进行指定编译后的二进制文件产生的位置。

       具体参数太多,这里不一一详细介绍,javac的options选项有标准的和非标准的参数,所谓标准就是在以后仍然会使用的一些参数,但是也有一些不是标准的参数,非标准的参数一般都以-X开头。下面仅以非标准的Xlint参数展示一些使用效果:

-Xlint参数

-Xlint:name

       首先这里的“name”是有固定的选项的,不能随便填:cast、classfile、deprecation、dep-anan、divzero、empty、fallthrough、finally、options、overrides、path、processing、rawtypes、serial、static、try、unchecked、varargs。这些参数各有个的用途,这里随机找几个参数来看看效果:

cast

String s = (String)"HelloWorld";

使用了这个参数,可以在编译的时候检查代码中是否存在无必要的或者是冗余的转型。在编译的时候可以看到如下结果:

Xlint_name_cast.png

可以看到它会报警告,指明有一个冗余的转换。

deprecation

这个参数主要是警告类中是否有过时的方法在使用。

java.util.Date now = new java.util.Date();
System.out.println(now.getDay());
Xlint_name_deprecation.png

可以看到,这里使用了Date类中已经过时的方法getDay,编译的时候会抛出警告。其他的这里不再做更多讨论。

关于argfile

单个文件的情况

这种情况下,使用命令:

root@ubuntu:/usr/local/workspace/demo1# javac @argfile

这种情况下,只需要将需要编译的文件逐个在argfile中填写即可,现在主要来看看多种文件指定的情况。

多文件参数

现在我有Test01.java、Test02.java、Test03.java三个源码文件,同时还需要有多个options共同作用最终编译出想要的效果,请看下面的例子:

我们先创建一个options文件,它里面的内容有三行,代表三个指令参数:

-d clazz
-g
-sourcepath /usr/local/workspace/demo1

创建一个classes文件,里面的内容如下:

Test01.java
Test02.java
Test03.java

然后在options所属目录下创建一个clazz文件夹用于存放编译后的class文件

那么在使用javac的时候就可以像如下方式使用:

root@ubuntu:/usr/local/workspace/demo1# javac @options @classes

关于javac命令使用还有很多,此处篇幅有限,详细使用情况,可以参考 Oracle的javac文档

javap命令

       javap命令是反编译命令,可以将class字节码文件反编译并输出,这个命令在进行源码学习的时候很有用处,它有一些可选参数,不同的参数,执行后的结果有所不同。

格式:javap [ options ] classes

       这里的options就是可选参数,后面具体会说,classes指的就是字节码所在地址,它可以是一个具体的文件名称全路径(D:\workspace\src\Test01.class),也可以是一个URL定位(file:///D:/workspace/src/Test01.class)。下面就简单抽几个options参数来查看以下具体效果。

不指定options

这里来看一下默认不指定options参数得到的效果展示,下面是我的源代码:

public class Test04 {
 public String str1 = "Hello";

 protected int i = 9;

 private String name = "Still";

 public void m1() {
 System.out.println("m1 Method...");
 }

 protected void m2() {
 System.out.println("m2 Method...");
 }

 private void m3() {
 System.out.println("m3 Method...");
 }
}

将上面的代码经过javac编译之后,再通过javap反编译,可以看到如下效果:


javap默认无参数.png

       这里可以看到,它只是将我们源码中定义的public和protected修饰的字段和方法展示了出来。而且字段只展示类型,没有具体的赋值,方法只是给了一个方法定义,没有方法体。

-help(或--help或-?) 参数

输出的是该指定的使用方法以及各个参数的含义。如果需要了解具体参数的含义可以使用该参数来获取各个参数对应的含义。


javap_help.png

-c 参数

       输出拆解后的代码,它展示的是偏向于指令的内容,与源码会有很大不同,具体它的各个指令的含义,这里不做深入探究,可以在网上搜索相关指令资料。执行效果如下:

javap_c参数.png

       这里只是截取了一部分,并不是全部,可以看到它几乎把源码中的语句拆分成了一个一个的指令操作。
注:对于没有指定参数的情况下,这里有一个默认参数,就是上面help指令执行结果中的-protected参数,对应的有public,只输出public修饰的字段和方法;private参数则会输出所有public、protected和private修饰的参数。
其它的参数具体可结合help指令详细了解,这里就不再过多赘述。

jps命令

       jps(Java Virtual Machine Process Status Tool)它是JDK1.5开始提供的一个工具,用于查看当前系统中所有运行的Java程序以及它的pid,另外,jps指令执行的时候,会在java.io.tmpdir指定的目录下生成一个hsperfdata_{userName} 文件夹,它执行的结果其实就是把该文件夹下的目录展示一遍,然后具体的参数就是通过读取参数所指文件目录内容来获取,

我们可以编写一个程序来获取当前系统环境中,java.io.tmpdir指向的目录是哪里:

public class Test05 {
 public static void main(String[] args) {
 System.out.println(System.getProperty("java.io.tmpdir"));
 }
}

得到目录后可以查看对应目录下的文件结构,执行后可以在控制台看见(hsperfdata_root目录):

root@ubuntu:/usr/local/workspace/demo1# java Test05
/tmp
root@ubuntu:/usr/local/workspace/demo1# ll /tmp
total 60
drwxrwxrwt 13 root root 4096 Jul 10 19:17 ./
drwxr-xr-x 25 root root 4096 Jun 16 04:11 ../
drwxrwxrwt  2 root root 4096 Jul 10 18:06 .font-unix/
drwxr-xr-x  2 root root 4096 Jul 10 19:20 hsperfdata_root/  ------>这个就是Jps对应生成的文件目录
drwxrwxrwt  2 root root 4096 Jul 10 18:06 .ICE-unix/
drwx------  3 root root 4096 Jul 10 18:06 systemd-private-e857832c543743dba97ddc04a0bc32e4-colord.service-JmYbHL/
drwx------  3 root root 4096 Jul 10 18:06 systemd-private-e857832c543743dba97ddc04a0bc32e4-rtkit-daemon.service-T8XEtS/
drwx------  3 root root 4096 Jul 10 18:06 systemd-private-e857832c543743dba97ddc04a0bc32e4-systemd-timesyncd.service-EIPLVz/
drwxrwxrwt  2 root root 4096 Jul 10 18:06 .Test-unix/
-rw-r--r--  1 root root 1600 Jul 10 18:06 vgauthsvclog.txt.0
drwxrwxrwt  2 root root 4096 Jul 10 18:06 VMwareDnD/
drwx------  2 root root 4096 Jul 10 18:07 vmware-root/
-r--r--r--  1 root root   11 Jul 10 18:07 .X0-lock
drwxrwxrwt  2 root root 4096 Jul 10 18:07 .X11-unix/
drwxrwxrwt  2 root root 4096 Jul 10 18:06 .XIM-unix/

首先来看一下它的帮助指令:

root@ubuntu:/usr/local/workspace/demo1# jps -help
usage: jps [-help]
 jps [-q] [-mlvV] [<hostid>]
​
Definitions:
 <hostid>:      <hostname>[:<port>]

       可以看到,它的帮助比较简单,这里注意,[-mlvV]这里的参数含义是:-m、-l、-v、-V。不同的参数效果不同,为了测试它的命令行效果,首先编写一个测试代码,内容很简单,就是一个死循环。

public class JpsDemo {
 public static void main(String[] args) {
 System.out.println("进入死循环。。。");
 while(true) {
 }
 }
}

编译并执行后,这时另起一个命令窗口,尝试运行jps命令:

root@ubuntu:~# jps
3098 Jps
3086 JpsDemo

       可以看到,目前系统内就两个Java进程,一个是Jps本身,另一个就是刚刚运行的JpsDemo。-q参数是只显示pid,不显示程序名:

root@ubuntu:~# jps -q
3110
3086

       这里可以看到,后面的名称不见了,另外:每次运行jps,它自身的pid都有可能不同,不是一个固定值,但是一直运行的程序pid暂时不会变,除非重新运行。

       -m参数不仅显示pid和程序名称,还会显示程序运行时,传入main方法的参数,这里在启动JpsDemo的时候,考虑使用参数传入:

root@ubuntu:/usr/local/workspace/demo1# java JpsDemo Still Work
进入死循环中。。。

这时候再执行jps -m,可以看到如下结果:

root@ubuntu:~# jps -m
3147 Jps -m
3135 JpsDemo Still Work

可以看到Still Work已经被打印出来了。

-l参数可以输出main方法所在类的完整路径或者所在jar包的完整路径名:

root@ubuntu:~# jps -l
3159 jdk.jcmd/sun.tools.jps.Jps
3135 JpsDemo

这里JpsDemo之所以没有路径,是因为我在编写的时候,没有指定package,所以这里展示的为空。

-v参数可以输出Java程序运行时,给JVM设置的参数。例如这里给JpsDemo传入JVM参数:

root@ubuntu:/usr/local/workspace/demo1# java -Dfile.encoding=UTF-8 JpsDemo
进入死循环中。。。

然后运行-v参数可以看到:

root@ubuntu:~# jps -v
3254 Jps -Denv.class.path=.:/usr/local/java/jdk-10.0.1//lib:/usr/local/java/jdk-10.0.1///lib:.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib -Dapplication.home=/usr/local/java/jdk-10.0.1 -Xms8m -Djdk.module.main=jdk.jcmd
3242 JpsDemo -Dfile.encoding=UTF-8

       前面说过,jps命令执行的结果实际就是tmp文件目录下,hsperfdata_{userName}目录的中的内容,我这里的{userName}是root,所以我的文件夹下是hsperfdata_root,那么相应的,如果在执行jps过程中,出现了任何导致该文件目录异常的情况,都会造成jps失效,例如:

  • 磁盘已满

  • 无法继续写入

  • 临时文件丢失

  • java.io.tmpdir被人为设定了,不是默认值,而jps只能从默认的tmp目录中读取。

jstack命令

命令使用

在了解jstack命令之前,先了解以下线程的几个运行状态概念,先看下面这张图:

thread.png
  • 进入区(Entry Set):表示线程在通过synchronized锁请求获取对应锁资源,如果获取,可以进入拥有者区域,否则就在进入区中等待。

  • 拥有者(The Owner):成功竞争到对象锁

  • 等待区(Wait Set):线程通过调用对象的wait方法释放锁资源,在等待区中等待被唤醒

同一时刻只能有一个线程进入Monitor区域,这里的Monitor是每个对象都有的,并且仅有一个,可以看成是类或对象的锁。

下面看jstack命令,首先使用help参数查看命令信息:

root@ubuntu:~# jstack -help
Usage:
 jstack [-l] <pid>
 (to connect to running process)
​
Options:
 -l  long listing. Prints additional information about locks
 -h or -help to print this help message

我这里使用的是java10版本,与java8会有所不同,java8执行的界面会有一些其他-F、-m之类的参数。

Usage:
 jstack [-l] <pid>
 (to connect to running process)
 jstack -F [-m] [-l] <pid>
 (to connect to a hung process)
 jstack [-m] [-l] <executable> <core>
 (to connect to a core file)
 jstack [-m] [-l] [server_id@]<remote server IP or hostname>
 (to connect to a remote debug server)
​
Options:
 -F  to force a thread dump. Use when jstack <pid> does not respond (process
is hung)
 -m  to print both java and native frames (mixed mode)
 -l  long listing. Prints additional information about locks
 -h or -help to print this help message</pre>

       这里暂时忽略因为版本不同而导致的参数不同的问题。下面简单看一下默认情况下的参数执行效果,这里先写一段测试代码,与前面JpsDemo一样,也是一个死循环,然后通过jps命令找到对应的pid,查看相关的运行情况如下:

...
"main" #1 prio=5 os_prio=0 tid=0x00007fddd8010800 nid=0xdb3 runnable  [0x00007fdde0b9e000]
 java.lang.Thread.State: RUNNABLE
 at JstackDemo.main(JstackDemo.java:4)
...

结果中的内容有很多,这里截取其中最主要的一段,这段可以看到有一个线程处于RUNNABLE状态,它是在JstackDemo.java源码中的第4行。这是针对一条线程情况下,下面来看看如果有两个线程是什么效果,测试代码如下:

public class JstackDemo2 {
 public static void main(String[] args) {
 Thread t1 = new Thread(new Runnable(){
 @Override
 public void run() {
 System.out.println("t1线程启动,进入死循环...");
 while(true) {}
 }
 }, "t1");
 t1.start();
 }
}
...
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f822408a000 nid=0xe22 waiting on condition  [0x00007f8228fab000]
 java.lang.Thread.State: RUNNABLE
 at java.lang.ref.Reference.waitForReferencePendingList(java.base@10.0.1/Native Method)
 at java.lang.ref.Reference.processPendingReferences(java.base@10.0.1/Reference.java:174)
 at java.lang.ref.Reference.access$000(java.base@10.0.1/Reference.java:44)
 at java.lang.ref.Reference$ReferenceHandler.run(java.base@10.0.1/Reference.java:138)
"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f822408c000 nid=0xe23 in Object.wait()  [0x00007f8228eaa000]
 java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(java.base@10.0.1/Native Method)
 - waiting on <0x00000000f0e09480> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(java.base@10.0.1/ReferenceQueue.java:151)
 - waiting to re-lock in wait() <0x00000000f0e09480> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(java.base@10.0.1/ReferenceQueue.java:172)
 at java.lang.ref.Finalizer$FinalizerThread.run(java.base@10.0.1/Finalizer.java:216)
 ...
"t1" #10 prio=5 os_prio=0 tid=0x00007f8224140000 nid=0xe2b runnable  [0x00007f820940d000]
 java.lang.Thread.State: RUNNABLE
 at JstackDemo2$1.run(JstackDemo2.java:7)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
...

       这里可以看到,t1的线程目前是RUNNABLE状态,而且是运行在JstackDemo.java文件中的第7行。而且还有一个Finalizer,它是WATITING状态,已经将锁资源释放,这里就相当于是一个过程,先执行了主线程,然后主线程释放资源,然后进入t1线程,t1锁住资源,进入RUNNABLE状态。

死锁

下面来看一下死锁状态下jstack会打印出什么内容,先编写一个死锁程序:

public class DeathLock {
    public static void main(String[] args) {
        Thread t1 = new Thread(new DeathLockClass(true), "t1");
        Thread t2 = new Thread(new DeathLockClass(false), "t2");
        t1.start();
        t2.start();
    }
}
class DeathLockClass implements Runnable{
    public boolean flag = false;
    public DeathLockClass(boolean flag) {
         this.flag = flag;
    }
    @Override
    public void run() {
        if(flag) {
            while(true) {
                synchronized (Lock.lock1) {
                    System.out.println("lock1 entered:" + Thread.currentThread().getName());
                    synchronized (Lock.lock2) {
                        System.out.println("lock2 entered:" + Thread.currentThread().getName());
                    }
                }
            }
        } else {
            while(true) {
                synchronized (Lock.lock2) {
                   System.out.println("lock2 entered:" + Thread.currentThread().getName());
                   synchronized (Lock.lock1) {
                       System.out.println("lock1 entered:" + Thread.currentThread().getName());
                   }
               }
           }
       }
    }
   }
class Lock {
 static Object lock1 = new Object();
 static Object lock2 = new Object();
}

执行后,可以看到:

root@ubuntu:/usr/local/workspace/demo1# java DeathLock
lock1 entered:t1
lock2 entered:t2

       然后就会进入死锁状态,t1等待获取lock2的锁资源,但是lock2的锁资源在t2那里,同时t2等待获取的lock1锁资源在t1那里,僵持不下,形成死锁,下面看看在这种情况下的jstack命令效果:

...
Found one Java-level deadlock:
=============================
"t1":
 waiting to lock monitor 0x00007f9dd8008200 (object 0x00000000f0ee7e20, a java.lang.Object),
 which is held by "t2"
"t2":
 waiting to lock monitor 0x00007f9dd8006600 (object 0x00000000f0ee7e10, a java.lang.Object),
 which is held by "t1"
​
Java stack information for the threads listed above:
===================================================
"t1":
 at DeathLockClass.run(DeathLock.java:23)
 - waiting to lock <0x00000000f0ee7e20> (a java.lang.Object)
 - locked <0x00000000f0ee7e10> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
"t2":
 at DeathLockClass.run(DeathLock.java:32)
 - waiting to lock <0x00000000f0ee7e10> (a java.lang.Object)
 - locked <0x00000000f0ee7e20> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
Found 1 deadlock.

其它部分不看,单单只是这一部分,就可以看到,它明确表明了有死锁存在,这里说的很明白,就不再赘述。

jmap命令

       jmap(Memory Map for Java)用于生成堆转储快照。jmap的有些功能在windows平台下是受限的,除了-dump和-histo,其余的都只能在Linux/Solaris系统。

root@ubuntu:~# jmap -help
Usage:
 jmap -clstats <pid>
 to connect to running process and print class loader statistics
 jmap -finalizerinfo <pid>
 to connect to running process and print information on objects awaiting finalization
 jmap -histo[:live] <pid>
 to connect to running process and print histogram of java object heap
 if the "live" suboption is specified, only count live objects
 jmap -dump:<dump-options> <pid>
 to connect to running process and dump java heap
​
 dump-options:
 live         dump only live objects; if not specified,
 all objects in the heap are dumped.
 format=b     binary format
 file=<file>  dump heap to <file>
​
 Example: jmap -dump:live,format=b,file=heap.bin <pid>

       这里因为是java10,所以可能与其他版本有些不同。参照上面最后一行的例子,可以输出一个dump文件,对于它的使用一般是搭配jhat生成堆转储快照。不过一般不到万不得已,基本不会使用jhat进行堆转储分析,一是因为一般不会直接在部署程序的服务器上直接分析dump,二是jhat的分析功能相对简陋,可以有其它更好的代替工具。另外需要知道的一点:jmap -histo:live命令会触发一次GC操作,然后再输出结果。jhat内置一个HTTP/HTML服务器,通过jhat指令,可以临时启动一个简单服务器:

root@ubuntu:/usr/local/workspace/demo1# jhat heap.bin
Reading from heap.bin...
Dump file created Wed Jul 11 22:23:36 PDT 2018
Snapshot read, resolving...
Resolving 14747 objects...
Chasing references, expect 2 dots..
Eliminating duplicate references..
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.</pre>

可以看到,服务起在本地的7000的端口上:

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

推荐阅读更多精彩内容