生成Java源文件 (javawriter, javapoet, codemodel)

開發工具為Android Studio

一. 使用JavaWriter生成java源文件

  • (1) 介紹
    JavaWritersquare開源項目javapoet中的一個分支, JavaWriter的整個庫中有一個關鍵的類com.squareup.javawriter.JavaWriter(一共只有兩個類), 主要用來生成Java源文件, 使用鏈式調用依次構建Java源文件. JavaWriter生成Java源文件的方式比較原始。由于整個庫一共才兩個類, 因此沒有對Java源代碼進行建模, 如class、field、method等沒有對應的數據結構, 只有對應的方法來構建生成這些固定的概念。JavaWriter使用起來比較簡單,整個庫也非常小。官網介紹如下:

JavaWriter
is a utility class which aids in generating Java source files.
Source file generation can useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

  • (2) 使用步驟
    • 在Android Studio中創建一個Java library Module
    • 在上一步創建的module的構建腳本<module>/build.gradle中添加JavaWriter的依賴, 如下:
dependencies {
      compile 'com.squareup:javawriter:2.5.1'
}
  • 編寫java源文件生成代碼

  • (3) 使用案例

import com.squareup.javawriter.JavaWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.EnumSet;

import javax.lang.model.element.Modifier;

public class Demo1 {

    public static void main(String[] args) throws IOException {
        testJavaWriter();
    }

    /**
     * javawriter的github地址: https://github.com/square/javapoet/tree/javawriter_2
     * 使用下面語句引用該庫 (倉庫為jcenter):
     * compile 'com.squareup:javapoet:1.7.0'
     *
     * 使用JavaWriter生成java源文件
     * @throws IOException
     */
    private static void testJavaWriter() throws IOException {
        String packageName = "com.example.javawriter.generate";
        String className = "GenerateClass";
        File outFile = new File("java-demo/src/main/java/" + packageName.replaceAll("\\.", "/") + "/" + className + ".java");
        if(!outFile.getParentFile().exists()) {
            outFile.getParentFile().mkdirs();
        }
        if (!outFile.exists()) {
            outFile.createNewFile();
        }
        System.out.println(outFile.getAbsolutePath());
        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(outFile));
        JavaWriter jw = new JavaWriter(writer);
        jw.emitPackage(packageName)
                .beginType(packageName + "." + className, "class", EnumSet.of(Modifier.PUBLIC, Modifier.FINAL))
                .emitField("String", "firstName", EnumSet.of(Modifier.PRIVATE))
                .emitField("String", "lastName", EnumSet.of(Modifier.PRIVATE))
                .emitJavadoc("Return the person's full name")
                .beginMethod("String", "getName", EnumSet.of(Modifier.PUBLIC))
                .emitStatement("return firstName + \" - \" + lastName")
                .endMethod()
                .beginMethod("String", "getFirstName", EnumSet.of(Modifier.PUBLIC))
                .emitStatement("return firstName")
                .endMethod()
                .beginMethod("String", "getLastName", EnumSet.of(Modifier.PUBLIC))
                .emitStatement("return lastName") //注意不要使用分號結束return語句
                .endMethod()
                .endType()
                .close();
    }

}

運行程序, 生成源文件如下:

使用JavaWriter生成的Java源代碼.png

詳細用法可以參考官網用例https://github.com/square/javapoet/blob/javawriter_2/src/test/java/com/squareup/javawriter/JavaWriterTest.java

二. 使用javapoet生成Java源文件

  • (1) 介紹
    javapoet是大名鼎鼎的square公司開源的一個項目, github地址: https://github.com/square/javapoet. javapoet要比JavaWriter稍微復雜. 此庫定義了一系列的數據結構來表示Java源文件中某些固定的概念, 如class、interface、annoation、field、method等。javapoet對java源文件的結構進行了建模, 其模型如下:
2C698ACC-98EF-47A5-B829-4C5BDF03A3C6.png

javapoet官網對其介紹如下:

JavaPoet
JavaPoet is a Java API for generating .java source files.
Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

  • (2) 使用步驟
    • 在Android Studio中創建一個Java Library Module
    • 在上一步創建的Module的<module>/build.gradle構建腳本中添加javapoet庫的依賴, 如下:
dependencies {
        compile 'com.squareup:javapoet:1.7.0'
}
  • 編寫生成Java源文件的代碼

  • (3) 使用案例

import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.File;
import java.io.IOException;

import javax.lang.model.element.Modifier;

/**
 * @author stone
 * @date 16/9/12
 */
public class Demo2 {

    public static void main(String[] args) {
        testJavaPoet();
    }


    /**
     * 庫: https://github.com/square/javapoet/
     * 使用下面語句引用javapoet (倉庫為jcenter):
     * compile 'com.squareup:javawriter:2.5.1'
     *
     * 使用javapoet生成java源文件的步驟 (1,2,3步驟可以交換):
     * 1. 構建成員變量
     * 2. 構建構造方法
     * 3. 構建方法(static/concrete)
     * 4. 構建類型(enum/annotation/interface/class)
     * 5. 構建java源文件
     * 6. 輸出java源文件到文件系統
     */
    private static void testJavaPoet() {
        String packageName = "com.stone.demo.javawriter";
        String className = "HelloWorld";

        //1. 生成一個字段
        FieldSpec fieldSpec = FieldSpec.builder(String.class, "var", Modifier.PUBLIC).build();

        //2. 生成一個方法 (方式一: 面向代碼, 更為底層的構建方式)
        MethodSpec mainMethod = MethodSpec.methodBuilder("main")  //設置方法名稱
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)   //添加修飾符
                .addParameter(String[].class, "args")             //添加參數
                .returns(TypeName.VOID)                           //添加返回值
                .addStatement("$T.out.println($S)", System.class, "Hello world !")  //添加代碼語句 (結束語句的分號不需要, 注意與CodeBlock的區別)
                .build();

        //2. 生成一個方法 (方式二: 對方法建模, 結構化的構建)
//        ParameterSpec parameterSpec = ParameterSpec.builder(String[].class, "args").build();  //構建參數模型
//        CodeBlock codeBlock = CodeBlock.of("$T.out.println($S);", System.class, "Hello world"); //構建代碼塊 (語句結束的分號不能少)
//        MethodSpec methodSpec = MethodSpec.methodBuilder("main")    //設置方法名稱
//                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)     //添加修飾符
//                .returns(TypeName.VOID)                             //添加返回值
//                .addParameter(parameterSpec)                        //添加方法參數
//                .addCode(codeBlock)                                 //添加代碼塊
//                .build();


        //3. 生成類型(enum/class/annotation/interface)
        TypeSpec hellworld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC)
                .addField(fieldSpec)
                .addMethod(mainMethod)
//                .addMethod(methodSpec)
                .build();
        
        //4. 構建Java源文件
        JavaFile javaFile = JavaFile.builder(packageName, hellworld).build();

        //5. 輸出java源文件到文件系統
        try {
            //輸出到控制臺
//            javaFile.writeTo(System.out);

            //生成java源文件到AndroidStudio的當前Module中
            generateToCurrentAndroidStudioModule(javaFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成到當前module的源文件目錄下
     *
     * @param javaFile
     * @throws IOException
     */
    private static void generateToCurrentAndroidStudioModule(JavaFile javaFile) throws IOException {
        String targetDirectory = "java-demo/src/main/java"; //輸出到和用例程序相同的源碼目錄下
        File dir = new File(targetDirectory);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        javaFile.writeTo(dir); //JavaFile.write(), 參數為源碼生成目錄(源碼的classpath目錄)
    }


}

運行程序, 生成如下源文件:

使用javapoet生成Java源文件.png

詳細用法, 參考官網用例:
https://github.com/square/javapoet/tree/master/src/test/java/com/squareup/javapoet

三. 使用codemodel生成Java源文件

  • (1) 介紹
    官網如是說:

CodeModel project
CodeModel is a Java library for code generators; it provides a way to generate Java programs in a way much nicer than PrintStream.println(). This project is a spin-off from the JAXB RI for its schema compiler to generate Java source files.
------ From here: https://codemodel.java.net/

IBM Developers是醬紙介紹的:

CodeModel 是用于生成 Java 代碼的 Java 庫,它提供了一種通過 Java 程序來生成 Java 程序的方法。
CodeModel 項目是 JAXB 的子項目。JAXB(Java Architecture for XML Binding)是一項可以根據 XML Schema 產生 Java 類的技術,它提供了將 XML 實例文檔反向生成 Java 對象樹的方法,并能將 Java 對象樹的內容重新寫到 XML 實例文檔。JAXB 是 JDK 的組成部分。JAXB RI(Reference Implementation)即 schema compiler 能夠將 XML 的 schema 文件映射為相應的 Java 元素。
------ From here: http://www.ibm.com/developerworks/cn/java/j-lo-codemodel/

我覺的:
它就是一個生成Java源代碼的庫 ! (哈! (⌒^⌒)b) !
但是我還是想多說幾句, codemodel和javapoet差不多, 都對java源文件進行了建模, 都有相關的數據結構來表述源文件中固定的概念, 這樣用戶使用起來會更加方便, 只是增加了復雜度和理解上的困難. 其實只要我們按coding的順序(先聲明包...再import依賴包...再聲明class...然后生命成員變量...再然后聲明方法......)來構建也是挺好理解的.
下面貼幾張圖:

codemodel官網.png

對此圖有兩點說明, 右下角顯示:
a. codemodel版權屬于oracle公司
b. 此庫已經很久沒有更新了

點擊左邊導航欄的Download按鈕跳到codemodel的maven倉庫, 這里可以下載codemodel的jar包和源碼. 這里再貼一圖:

codemodel下載.png

此圖說明codemodel從2011年開始就不再更新了 (自從sun被oracle收購之后, oracle對很多java業務就不再關心. 因為某些雞肋的業務不賺錢啊...呵呵...), 有好心的開發者fork了codemodel源碼并進行了維護升級. 如下:
https://github.com/UnquietCode/JCodeModel
https://github.com/phax/jcodemodel

喂喂, 樓主, 這是寫偵探小說么?! 不喜勿噴哈 ヾ _?

  • (2) 使用步驟
    • 在Android Studio中創建一個Java Library Module
    • 在上一步創建的Module的<module>/build.gradle構建腳本中添加codemodel的依賴庫, 如下:

dependencies {
compile 'com.sun.codemodel:codemodel:2.6'
}

  * 編寫生成Java源文件的代碼

* (3) 使用案例

package com.example.javawriter;

// 注意不要引用了錯誤的包, 有internal的包是jdk內部使用的包
// import com.sun.codemodel.internal.ClassType;
// import com.sun.codemodel.internal.JDefinedClass
// import com.sun.codemodel.internal.JBlock
// ....

// 下面的包是獨立出來的codemodel庫的包, 這個包是沒有internal的
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JVar;

import java.io.File;
import java.io.IOException;

public class Demo3 {

public static void main(String[] args) throws IOException, JClassAlreadyExistsException {
    testCodeModel();
}

/**
 * 使用codemodel生成Java源文件:
 * 在構建腳本中添加codelmodel的依賴庫 (倉庫為jcenter):
 * compile 'com.sun.codemodel:codemodel:2.6'
 *
 * @throws JClassAlreadyExistsException
 * @throws IOException
 */
private static void testCodeModel() throws JClassAlreadyExistsException, IOException {
    final String className = "com.stone.generate.Person";

    /************ 生成一個Java源文件模型 ************/
    JCodeModel model = new JCodeModel();

    /************ 為模型添加一個頂級類型 (添加一個類) ************/
    JDefinedClass klass = model._class(JMod.PUBLIC, className, ClassType.CLASS);

    /************ 添加一個靜態成員變量 ************/
    int modifier = JMod.PRIVATE + JMod.STATIC + JMod.FINAL;
    JFieldVar jStaticFieldVar = klass.field(modifier, String.class, "TAG");
    //jStaticFieldVar.assign(JExpr.lit(klass.fullName()));  //error, 不能對未初始化成員變量進行賦值, 要先進行初始化;
    jStaticFieldVar.init(JExpr.lit(klass.fullName()));

    /************ 添加一個成員變量 ************/
    //原始類型變量(int, byte,char ....)才可以使用JType.parse(model, "int"), Object類型直接使用Object.class
    //JFieldVar jFieldVar = klass.field(JMod.PRIVATE, JType.parse(model, "String"), "name"); //java.lang.IllegalArgumentException: Not a primitive type: String
    JFieldVar jFieldVar = klass.field(JMod.PRIVATE, String.class , "name");
    jFieldVar.annotate(MyAnnotation.class); //給字段添加一個注解

    /************ 添加一個構造方法 ************/
    JMethod constructor = klass.constructor(JMod.PRIVATE);
    constructor.param(String.class, "name");  //為構造方法添加一個參數
    JBlock constructorBlock = constructor.body();
    constructorBlock.assign(JExpr.refthis("name"), constructor.params().get(0));        //初始化成員變量
    constructorBlock.directStatement("System.out.println(\"Constructor invoked !\");"); //直接定義語句


    /************ 添加一個成員方法 ************/
    JMethod jMethod = klass.method(JMod.PUBLIC, Void.TYPE, "setName");  //參數依次為: 修飾符, 返回類型, 方法名
    //jMethod.param(JType.parse(model, "String"), "name"); //java.lang.IllegalArgumentException: Not a primitive type: String
    jMethod.param(String.class, "name");
    JBlock methodBlock = jMethod.body();                                //構建方法體
    methodBlock.assign(JExpr.refthis("name"), jMethod.params().get(0)); //在方法體中生成一句賦值語句 (為成員變量賦值)

    //在方法塊中定義兩個局部變量
    //JVar var = jBlock.decl(model.INT, "age");                     //先聲明變量, 再進行初始化 (兩步完成)
    //var.init(JExpr.lit(23));
    JVar var = methodBlock.decl(model.INT, "age", JExpr.lit(100));  //聲明變量, 同時進行初始化 (一步到位)
    JVar isAgeGreatThan_25 = methodBlock.decl(model.BOOLEAN, "isAgeGreatThan_25", var.gt(JExpr.lit(25)));


    //構造一個if...else...語句塊
    JBlock if_else_block =  new JBlock();
    JConditional jConditional = if_else_block._if(isAgeGreatThan_25);
    jConditional._then().directStatement("System.out.println(\"Age great than 25\");"); //語句結束時不要忘了分號
    jConditional._else().directStatement("System.out.println(\"Age less than 25\");");

    //將if...else...語句塊添加到方法語句塊中
    methodBlock.add(if_else_block);


    /************ 構建生成一個Java源文件 ************/
    model.build(new File("java-demo/src/main/java"));
}

}

上面用到的自定義注解如下: 

package com.example.javawriter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
String value() default "Read the fucking source code !";
}


運行程序, 生成如下Java源文件:  

![codemodel生成的Java源文件.png](http://upload-images.jianshu.io/upload_images/1642441-497df81a186a5f3d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


 

四. 三者的異同
1. 三個庫都是用來生成java源文件的
2. JavaWriter就是一個工具類, 使用起來簡單容易理解, 但是要手動拼接源文件中的語句, 因此容易出出現拼寫錯誤. 
3. javapoet和codemodel都對java源文件進行了建模. 結構化java源文件后, 用戶使用時必須使用library提供的數據結構, 代碼的拼接生成由庫處理, 因此不會產生拼寫錯誤, 使用起來也比較方便. 
4. 個人感覺javapoet比codemodel使用起來更加方便, 抽象出來的概念也更少, 更加容易理解. codemodel屬于重量級的庫, 它幾乎對java源文件中的所有概念都進行了抽象, 如: 類、字段、方法、變量、語句塊、循環......等等 , 因此使用起來非常繁瑣, 理解起來也更加困難.
5. javapoet和codemodel生成java源文件的步驟是相反的: javapoet是先構建字段、方法、構造器、語句 ...... 等等, 然后添加到一個類(TypeSpec)中, 也就是說, javapoet是先構建細節, 然后再組織整體骨架, 是先分后總的邏輯; codemodel恰恰相反, codemodel構建java源文件非常類似于構建一顆DOM樹, 先構建根節點(JCodeModel), 然后再構建其他分支節點(JDefinedClass、JFieldVar、JMethod ......), 最后再構建比分支節點更細節的支節點 (JBlock、JExpr、JConditional ......)。

五. 使用場景
這三個庫主要就是用來生成Java源文件. 那么什么時候需要生成java源文件呢? 使用注解或解析注解的地方需要(生成那些需要我們重復勞動的代碼 --- 減少我們的負擔!). 那么什么地方會使用注解和解析呢? 使用注解的地方非常多, 如大部分的ORM框架([Realm](https://realm.io/)、[OrmLite](http://ormlite.com/)、[DBFlow](http://www.appance.com/dbflow/)、[ActiveAndroid](http://www.activeandroid.com/)、[greenDAO](http://greenrobot.org/greendao/)... 等)、依賴注入框架([Dagger](http://square.github.io/dagger/)、[ButterKnife](http://jakewharton.github.io/butterknife/)、 [guice](https://github.com/google/guice)、[RoboGuice](https://github.com/roboguice/roboguice) ... 等)、編譯時檢查框架(support-annotations、[jcip](https://github.com/jcip/jcip.github.com) ...等)以及很多其他優秀框架([Retrofit](http://square.github.io/retrofit/)、[retrolambda](https://github.com/orfjackal/retrolambda), [PermissionsDispatcher](http://hotchemi.github.io/PermissionsDispatcher/), [RxPermissions](https://github.com/tbruyelle/RxPermissions), [EventBus](http://greenrobot.org/eventbus/) ... 等) ......


references 
[用 Java 生成 Java - CodeModel 介紹](http://www.ibm.com/developerworks/cn/java/j-lo-codemodel/)
[codemodel](https://codemodel.java.net/)
[javapoet](https://github.com/square/javapoet)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,830評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,765評論 25 708
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,737評論 18 399
  • 關鍵字:TextInputLayout、TextInputEditText、材料設計項目地址:AboutMater...
    Arnold_J閱讀 563評論 0 2
  • 看到了第十一集,只能說感同身受。回憶如潮水涌來:高考結束時告別的目光、吃醋時趴桌上生無可戀的表情、體檢前溫柔的別怕...
    米花小子閱讀 251評論 0 0