騷操作 | 不重啟 JVM附帽,替換掉已經(jīng)加載的類,偷天換日井誉?

來源公眾號:芋道源碼

  • Java對象行為

  • java.lang.instrument.Instrumentation

  • 直接操作字節(jié)碼

  • BTrace

  • Arthas

  • 三生萬物


在遙遠(yuǎn)的希艾斯星球爪哇國塞沃城中蕉扮,兩名年輕的程序員正在為一件事情苦惱,程序出問題了颗圣,一時看不出問題出在哪里喳钟,于是有了以下對話:

“Debug一下吧≡谄瘢”

“線上機(jī)器奔则,沒開Debug端口”挝纾”

“看日志应狱,看看請求值和返回值分別是什么?”

“那段代碼沒打印日志祠丝。”

“改代碼除嘹,加日志写半,重新發(fā)布一次∥竟荆”

“懷疑是線程池的問題叠蝇,重啟會破壞現(xiàn)場∧甓校”

長達(dá)幾十秒的沉默之后:“據(jù)說悔捶,排查問題的最高境界,就是只通過Review代碼來發(fā)現(xiàn)問題单芜⊥筛茫”

比幾十秒長幾十倍的沉默之后:“我輪詢了那段代碼一十七遍之后,終于得出一個結(jié)論洲鸠√玫”

“結(jié)論是馋缅?”

“我還沒到達(dá)只通過Review代碼就能發(fā)現(xiàn)問題的至高境界【畹恚”

Java對象行為

文章開頭的問題本質(zhì)上是動態(tài)改變內(nèi)存中已存在對象的行為問題萤悴。

所以,得先弄清楚JVM中和對象行為有關(guān)的地方在哪里,有沒有更改的可能性。

對象使用兩種東西來描述事物:行為和屬性狸吞。

舉個例子:

public class Person{

  private int age;
  private String name;

  public void speak(String str) {
    System.out.println(str);
 }

  public Person(int age, String name) {
    this.age = age;
    this.name = name;
  }
}

上面Person類中age和name是屬性扔傅,speak是行為。對象是類的實(shí)例圈匆,每個對象的屬性都屬于對象本身,但是每個對象的行為卻是公共的。舉個例子柳沙,比如我們現(xiàn)在基于Person類創(chuàng)建了兩個對象,personA和personB:

Person personA = new Person(18, "mayun");
personA.speak("我是馬韻");
Person personB = new Person(23, "lizhan");
personB.speak("我是李戰(zhàn)");

personA和personB有各自的姓名和年齡拌倍,但是有共同的行為:speak赂鲤。想象一下,如果我們是Java語言的設(shè)計(jì)者柱恤,我們會怎么存儲對象的行為和屬性呢数初?

“很簡單,屬性跟著對象走梗顺,每個對象都存一份泡孩。行為是公共的東西,抽離出來寺谤,單獨(dú)放到一個地方仑鸥。”

“咦变屁?抽離出公共的部分眼俊,跟代碼復(fù)用好像啊∷诠兀”

“大道至簡疮胖,很多東西本來都是殊途同歸∶瓢澹”

也就是說澎灸,第一步我們首先得找到存儲對象行為的這個公共的地方。一番搜索之后遮晚,我們發(fā)現(xiàn)這樣一段描述:

Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.

Java的對象行為(方法性昭、函數(shù))是存儲在方法區(qū)的。

“方法區(qū)中的數(shù)據(jù)從哪來鹏漆?”

“方法區(qū)中的數(shù)據(jù)是類加載時從class文件中提取出來的巩梢〈葱梗”

“class文件從哪來?”

“從Java或者其他符合JVM規(guī)范的源代碼中編譯而來括蝠【弦郑”

“源代碼從哪來?”

“廢話忌警,當(dāng)然是手寫搁拙!”

“倒著推,手寫沒問題法绵,編譯沒問題箕速,至于加載……有沒有辦法加載一個已經(jīng)加載過的類呢?如果有的話朋譬,我們就能修改字節(jié)碼中目標(biāo)方法所在的區(qū)域盐茎,然后重新加載這個類,這樣方法區(qū)中的對象行為(方法)就被改變了徙赢,而且不改變對象的屬性字柠,也不影響已經(jīng)存在對象的狀態(tài),那么就可以搞定這個問題了狡赐∫ひ担可是,這豈不是違背了JVM的類加載原理枕屉?畢竟我們不想改變ClassLoader常柄。”

“少年搀擂,可以去看看java.lang.instrument.Instrumentation西潘。”

java.lang.instrument.Instrumentation

看完文檔之后哨颂,我們發(fā)現(xiàn)這么兩個接口:redefineClasses和retransformClasses秸架。一個是重新定義class,一個是修改class咆蒿。這兩個大同小異,看redefineClasses的說明:

This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.

都是替換已經(jīng)存在的class文件蚂子,redefineClasses是自己提供字節(jié)碼文件替換掉已存在的class文件沃测,retransformClasses是在已存在的字節(jié)碼文件上修改后再替換之。

當(dāng)然食茎,運(yùn)行時直接替換類很不安全蒂破。比如新的class文件引用了一個不存在的類,或者把某個類的一個field給刪除了等等别渔,這些情況都會引發(fā)異常附迷。所以如文檔中所言惧互,instrument存在諸多的限制:

The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.

我們能做的基本上也就是簡單修改方法內(nèi)的一些行為,這對于我們開頭的問題喇伯,打印一段日志來說喊儡,已經(jīng)足夠了。當(dāng)然稻据,我們除了通過retransform來打印日志艾猜,還能做很多其他非常有用的事情,這個下文會進(jìn)行介紹捻悯。

那怎么得到我們需要的class文件呢匆赃?一個最簡單的方法,是把修改后的Java文件重新編譯一遍得到class文件今缚,然后調(diào)用redefineClasses替換算柳。但是對于沒有(或者拿不到,或者不方便修改)源碼的文件我們應(yīng)該怎么辦呢姓言?其實(shí)對于JVM來說瞬项,不管是Java也好,Scala也好事期,任何一種符合JVM規(guī)范的語言的源代碼滥壕,都可以編譯成class文件。JVM的操作對象是class文件兽泣,而不是源碼绎橘。所以,從這種意義上來講唠倦,我們可以說“JVM跟語言無關(guān)”称鳞。既然如此,不管有沒有源碼稠鼻,其實(shí)我們只需要修改class文件就行了冈止。

直接操作字節(jié)碼

Java是軟件開發(fā)人員能讀懂的語言,class字節(jié)碼是JVM能讀懂的語言候齿,class字節(jié)碼最終會被JVM解釋成機(jī)器能讀懂的語言熙暴。無論哪種語言,都是人創(chuàng)造的慌盯。所以周霉,理論上(實(shí)際上也確實(shí)如此)人能讀懂上述任何一種語言,既然能讀懂亚皂,自然能修改俱箱。只要我們愿意,我們完全可以跳過Java編譯器灭必,直接寫字節(jié)碼文件狞谱,只不過這并不符合時代的發(fā)展罷了乃摹,畢竟高級語言設(shè)計(jì)之始就是為我們?nèi)祟愃?wù),其開發(fā)效率也比機(jī)器語言高很多跟衅。

對于人類來說孵睬,字節(jié)碼文件的可讀性遠(yuǎn)遠(yuǎn)沒有Java代碼高。盡管如此与斤,還是有一些杰出的程序員們創(chuàng)造出了可以用來直接編輯字節(jié)碼的框架肪康,提供接口可以讓我們方便地操作字節(jié)碼文件,進(jìn)行注入修改類的方法撩穿,動態(tài)創(chuàng)造一個新的類等等操作磷支。其中最著名的框架應(yīng)該就是ASM了,cglib食寡、Spring等框架中對于字節(jié)碼的操作就建立在ASM之上雾狈。

我們都知道,Spring的AOP是基于動態(tài)代理實(shí)現(xiàn)的抵皱,Spring會在運(yùn)行時動態(tài)創(chuàng)建代理類善榛,代理類中引用被代理類,在被代理的方法執(zhí)行前后進(jìn)行一些神秘的操作呻畸。那么移盆,Spring是怎么在運(yùn)行時創(chuàng)建代理類的呢?動態(tài)代理的美妙之處伤为,就在于我們不必手動為每個需要被代理的類寫代理類代碼咒循,Spring在運(yùn)行時會根據(jù)需要動態(tài)地創(chuàng)造出一個類。這里創(chuàng)造的過程并非通過字符串寫Java文件绞愚,然后編譯成class文件叙甸,然后加載。Spring會直接“創(chuàng)造”一個class文件位衩,然后加載裆蒸,創(chuàng)造class文件的工具,就是ASM了糖驴。

到這里僚祷,我們知道了用ASM框架直接操作class文件,在類中加一段打印日志的代碼贮缕,然后retransform就可以了久妆。

BTrace

截止到目前,我們都是停留在理論描述的層面跷睦。那么如何進(jìn)行實(shí)現(xiàn)呢?先來看幾個問題:

  1. 在我們的工程中肋演,誰來做這個尋找字節(jié)碼抑诸,修改字節(jié)碼烂琴,然后retransform的動作呢?我們并非先知蜕乡,不可能知道未來有沒有可能遇到文章開頭的這種問題奸绷。考慮到性價比层玲,我們也不可能在每個工程中都開發(fā)一段專門做這些修改字節(jié)碼号醉、重新加載字節(jié)碼的代碼。

  2. 如果JVM不在本地辛块,在遠(yuǎn)程呢畔派?

  3. 如果連ASM都不會用呢?能不能更通用一些润绵,更“傻瓜”一些线椰。

幸運(yùn)的是,因?yàn)橛蠦Trace的存在尘盼,我們不必自己寫一套這樣的工具了憨愉。什么是BTrace呢?BTrace已經(jīng)開源卿捎,項(xiàng)目描述極其簡短:

A safe, dynamic tracing tool for the Java platform.

BTrace是基于Java語言的一個安全的配紫、可提供動態(tài)追蹤服務(wù)的工具。BTrace基于ASM午阵、Java Attach API躺孝、Instrument開發(fā),為用戶提供了很多注解趟庄。依靠這些注解括细,我們可以編寫B(tài)Trace腳本(簡單的Java代碼)達(dá)到我們想要的效果,而不必深陷于ASM對字節(jié)碼的操作中不可自拔戚啥。

看BTrace官方提供的一個簡單例子:攔截所有java.io包中所有類中以read開頭的方法奋单,打印類名、方法名和參數(shù)名猫十。當(dāng)程序IO負(fù)載比較高的時候览濒,就可以從輸出的信息中看到是哪些類所引起,是不是很方便拖云?

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;

/**
 * This sample demonstrates regular expression
 * probe matching and getting input arguments
 * as an array - so that any overload variant
 * can be traced in "one place". This example
 * traces any "readXX" method on any class in
 * java.io package. Probed class, method and arg
 * array is printed in the action.
 */
@BTrace public class ArgArray {
    @OnMethod(
        clazz="/java\\.io\\..*/",
        method="/read.*/"
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
        println(pcn);
        println(pmn);
        printArray(args);
    }
}

再來看另一個例子:每隔2秒打印截止到當(dāng)前創(chuàng)建過的線程數(shù)贷笛。

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.Export;

/**
 * This sample creates a jvmstat counter and
 * increments it everytime Thread.start() is
 * called. This thread count may be accessed
 * from outside the process. The @Export annotated
 * fields are mapped to jvmstat counters. The counter
 * name is "btrace." + <className> + "." + <fieldName>
 */
@BTrace public class ThreadCounter {

    // create a jvmstat counter using @Export
    @Export private static long count;

    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    )
    public static void onnewThread(@Self Thread t) {
        // updating counter is easy. Just assign to
        // the static field!
        count++;
    }

    @OnTimer(2000)
    public static void ontimer() {
        // we can access counter as "count" as well
        // as from jvmstat counter directly.
        println(count);
        // or equivalently ...
        println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count"));
    }
}

看了上面的用法是不是有所啟發(fā)?忍不住冒出來許多想法宙项。比如查看HashMap什么時候會觸發(fā)rehash乏苦,以及此時容器中有多少元素等等。

有了BTrace,文章開頭的問題可以得到完美的解決汇荐。至于BTrace具體有哪些功能洞就,腳本怎么寫,這些Git上BTrace工程中有大量的說明和舉例掀淘,網(wǎng)上介紹BTrace用法的文章更是恒河沙數(shù)旬蟋,這里就不再贅述了。

我們明白了原理革娄,又有好用的工具支持倾贰,剩下的就是發(fā)揮我們的創(chuàng)造力了,只需在合適的場景下合理地進(jìn)行使用即可拦惋。

既然BTrace能解決上面我們提到的所有問題匆浙,那么BTrace的架構(gòu)是怎樣的呢?

BTrace主要有下面幾個模塊:

  1. BTrace腳本:利用BTrace定義的注解架忌,我們可以很方便地根據(jù)需要進(jìn)行腳本的開發(fā)吞彤。

  2. Compiler:將BTrace腳本編譯成BTrace class文件。

  3. Client:將class文件發(fā)送到Agent叹放。

  4. Agent:基于Java的Attach API饰恕,Agent可以動態(tài)附著到一個運(yùn)行的JVM上,然后開啟一個BTrace Server井仰,接收client發(fā)過來的BTrace腳本埋嵌;解析腳本,然后根據(jù)腳本中的規(guī)則找到要修改的類俱恶;修改字節(jié)碼后雹嗦,調(diào)用Java Instrument的retransform接口,完成對對象行為的修改并使之生效合是。

整個BTrace的架構(gòu)大致如下:

btrace工作流程

BTrace最終借Instrument實(shí)現(xiàn)class的替換了罪。如上文所說,出于安全考慮聪全,Instrument在使用上存在諸多的限制泊藕,BTrace也不例外。BTrace對JVM來說是“只讀的”难礼,因此BTrace腳本的限制如下:

  1. 不允許創(chuàng)建對象

  2. 不允許創(chuàng)建數(shù)組

  3. 不允許拋異常

  4. 不允許catch異常

  5. 不允許隨意調(diào)用其他對象或者類的方法娃圆,只允許調(diào)用com.sun.btrace.BTraceUtils中提供的靜態(tài)方法(一些數(shù)據(jù)處理和信息輸出工具)

  6. 不允許改變類的屬性

  7. 不允許有成員變量和方法,只允許存在static public void方法

  8. 不允許有內(nèi)部類蛾茉、嵌套類

  9. 不允許有同步方法和同步塊

  10. 不允許有循環(huán)

  11. 不允許隨意繼承其他類(當(dāng)然讼呢,java.lang.Object除外)

  12. 不允許實(shí)現(xiàn)接口

  13. 不允許使用assert

  14. 不允許使用Class對象

如此多的限制,其實(shí)可以理解谦炬。BTrace要做的是悦屏,雖然修改了字節(jié)碼,但是除了輸出需要的信息外,對整個程序的正常運(yùn)行并沒有影響础爬。

Arthas

BTrace腳本在使用上有一定的學(xué)習(xí)成本散劫,如果能把一些常用的功能封裝起來,對外直接提供簡單的命令即可操作的話幕帆,那就再好不過了。阿里的工程師們早已想到這一點(diǎn)赖条,就在去年失乾,阿里巴巴開源了自己的Java診斷工具——Arthas

Arthas提供簡單的命令行操作,功能強(qiáng)大纬乍。究其背后的技術(shù)原理碱茁,和本文中提到的大致無二。Arthas的文檔很全面仿贬,想詳細(xì)了解的話可以戳這里纽竣。

本文旨在說明Java動態(tài)追蹤技術(shù)的來龍去脈,掌握技術(shù)背后的原理之后茧泪,只要愿意蜓氨,各位讀者也可以開發(fā)出自己的“冰封王座”出來。

三生萬物

現(xiàn)在队伟,讓我們試著站在更高的地方“俯瞰”這些問題穴吹。

Java的Instrument給運(yùn)行時的動態(tài)追蹤留下了希望,Attach API則給運(yùn)行時動態(tài)追蹤提供了“出入口”嗜侮,ASM則大大方便了“人類”操作Java字節(jié)碼的操作港令。

基于Instrument和Attach API前輩們創(chuàng)造出了諸如JProfiler、Jvisualvm锈颗、BTrace這樣的工具顷霹。以ASM為基礎(chǔ)發(fā)展出了cglib、動態(tài)代理击吱,繼而是應(yīng)用廣泛的Spring AOP淋淀。

Java是靜態(tài)語言,運(yùn)行時不允許改變數(shù)據(jù)結(jié)構(gòu)姨拥。然而绅喉,Java 5引入Instrument,Java 6引入Attach API之后叫乌,事情開始變得不一樣了柴罐。雖然存在諸多限制,然而憨奸,在前輩們的努力下革屠,僅僅是利用預(yù)留的近似于“只讀”的這一點(diǎn)點(diǎn)狹小的空間,仍然創(chuàng)造出了各種大放異彩的技術(shù),極大地提高了軟件開發(fā)人員定位問題的效率似芝。

計(jì)算機(jī)應(yīng)該是人類有史以來最偉大的發(fā)明之一那婉,從電磁感應(yīng)磁生電,到高低電壓模擬0和1的比特党瓮,再到二進(jìn)制表示出幾種基本類型详炬,再到基本類型表示出無窮的對象,最后無窮的對象組合交互模擬現(xiàn)實(shí)生活乃至整個宇宙寞奸。

兩千五百年前呛谜,《道德經(jīng)》有言:“道生一,一生二枪萄,二生三隐岛,三生萬物〈煞”

兩千五百年后聚凹,計(jì)算機(jī)的發(fā)展過程也大抵如此吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末齐帚,一起剝皮案震驚了整個濱河市妒牙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌童谒,老刑警劉巖单旁,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饥伊,居然都是意外死亡象浑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門琅豆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來愉豺,“玉大人,你說我怎么就攤上這事茫因◎嚼梗” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵冻押,是天一觀的道長驰贷。 經(jīng)常有香客問我,道長洛巢,這世上最難降的妖魔是什么括袒? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮稿茉,結(jié)果婚禮上锹锰,老公的妹妹穿的比我還像新娘芥炭。我一直安慰自己,他們只是感情好恃慧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布园蝠。 她就那樣靜靜地躺著,像睡著了一般痢士。 火紅的嫁衣襯著肌膚如雪彪薛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天怠蹂,我揣著相機(jī)與錄音陪汽,去河邊找鬼。 笑死褥蚯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的况增。 我是一名探鬼主播赞庶,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼澳骤!你這毒婦竟也來了歧强?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤为肮,失蹤者是張志新(化名)和其女友劉穎摊册,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颊艳,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茅特,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棋枕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片白修。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖重斑,靈堂內(nèi)的尸體忽然破棺而出兵睛,到底是詐尸還是另有隱情,我是刑警寧澤窥浪,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布祖很,位于F島的核電站,受9級特大地震影響漾脂,放射性物質(zhì)發(fā)生泄漏假颇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一符相、第九天 我趴在偏房一處隱蔽的房頂上張望拆融。 院中可真熱鬧蠢琳,春花似錦、人聲如沸镜豹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趟脂。三九已至泰讽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昔期,已是汗流浹背已卸。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硼一,地道東北人累澡。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像般贼,于是被迫代替她去往敵國和親愧哟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

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

  • 引子 在遙遠(yuǎn)的希艾斯星球爪哇國塞沃城中,兩名年輕的程序員正在為一件事情苦惱腮介,程序出問題了肥矢,一時看不出問題出在哪里,...
    java成功之路閱讀 463評論 0 0
  • Java動態(tài)追蹤技術(shù)探究 在Java虛擬機(jī)中叠洗,字符串常量到底存放在哪 一次生產(chǎn) CPU 100% 排查優(yōu)化實(shí)踐 聊...
    passiontim閱讀 4,073評論 0 38
  • 引言 什么是 ASM 甘改? ASM 是一個 Java 字節(jié)碼操控框架。它能被用來動態(tài)生成類或者增強(qiáng)既有類的功能灭抑。AS...
    Chauncey_Chen閱讀 1,484評論 0 6
  • 小二班團(tuán)隊(duì)成員: 班主任:劉慧敏(小敏老師)一個90后姑娘楼誓,未婚,未婚的好處呢就是有更多的時間精力放在工作上名挥。畢業(yè)...
    youhimi閱讀 528評論 0 1
  • 月亮在天上 螞蟻回家了 調(diào)皮的星星還沒睡 我在你的夢鄉(xiāng)里 你在我的心底里
    種樹養(yǎng)樹閱讀 276評論 0 1