如何構(gòu)建編譯時(shí)注解解析框架

前言

在前面的文章中炒瘸,咱們學(xué)習(xí)了Java類加載千扔、Java反射链患、Java注解总寒,那現(xiàn)在咱們就可以利用所學(xué)搞點(diǎn)事情了踩身,所謂學(xué)以致用邓厕,方為正途重父。

如果想直接閱讀源碼挤牛,請(qǐng)點(diǎn)這里Github

鋪墊

在開(kāi)始搞事情前,咱們還需要了解以下幾個(gè)物件:

  • Annotation Processor: 注解處理器
  • JavaPoet:Java源碼文件生成者
  • javax.lang.model.element:用于解析程序中的元素呵恢,例如:包鞠值、類、方法渗钉、變量

Annotation Processor

注解處理器是在編譯時(shí)用來(lái)掃描和處理注解的工具彤恶。你可以注冊(cè)自己感興趣的注解,程式編譯時(shí)會(huì)將添加注解的元素鳄橘,交由注冊(cè)它的注解處理器來(lái)處理声离。

那咱們?nèi)绾螌?shí)現(xiàn)一個(gè)自己的注解處理器?

  1. 繼承AbstractProcessor
  2. 覆蓋getSupportedAnnotationTypes()
  3. 覆蓋getSupportedSourceVersion()
  4. 覆蓋process()

AbstractProcessor:抽象注釋處理器瘫怜,為大多數(shù)自定義注釋處理器的超類术徊。

getSupportedAnnotationTypes():這里注冊(cè)你感興趣的注解。它的返回一個(gè)字符串的Set鲸湃,包含注解類型的合法全稱赠涮。

getSupportedSourceVersion():指定使用的Java版本。通常這里返回SourceVersion.latestSupported()暗挑。

process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment):注解處理器的核心方法笋除,在這里進(jìn)行注解掃描、評(píng)估和處理炸裆,以及生成Java文件垃它。

生成Java文件,就交由JavaPoet來(lái)完成

JavaPoet

JavaPoet是一個(gè)用來(lái)生成 .java源文件的工具(由Square提供)烹看。

咱們來(lái)講一下JavaPoet里面常用的幾個(gè)類:

  • TypeSpec:表示一個(gè)類国拇、接口或者枚舉聲明
  • MethodSpec:表示一個(gè)構(gòu)造函數(shù)或方法聲明
  • FieldSpec:表示一個(gè)成員變量、字段聲明
  • JavaFile:生成java文件

下面通過(guò)一個(gè)實(shí)例來(lái)說(shuō)明具體使用方式:

private void generateHelloWorld() throws IOException {
        MethodSpec mainMethod = MethodSpec.methodBuilder("main")
                .addModifiers(new Modifier[]{Modifier.PUBLIC, Modifier.STATIC})
                .addParameter(String[].class, "args")
                .addStatement("System.out.println(\"Hello World\")")
                .build();

        FieldSpec androidVersion = FieldSpec.builder(String.class, "androidVer")
                .addModifiers(new Modifier[]{Modifier.PRIVATE})
                .initializer("$S", "Lollipop")
                .build();

        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(new Modifier[]{ Modifier.FINAL, Modifier.PUBLIC})
                .addMethod(mainMethod)
                .addField(androidVersion)
                .build();

        JavaFile javaFile = JavaFile.builder("com.hys.test", typeSpec).build();
        javaFile.writeTo(System.out);
    }

執(zhí)行函數(shù)听系,結(jié)果如下:

package com.hys.test;

import java.lang.String;

public class HelloWorld {
    private String androidVer = "Lollipop";

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

這里$S占位符贝奇,JavaPoet占位符如下:

  • $S:字符串類型占位符
  • $T:類型占位符
  • $N:名稱占位符(方法名或者變量名等)
  • $L:字面常量

這里只是投石問(wèn)路,關(guān)于JavaPoet更多API使用靠胜,請(qǐng)參見(jiàn)其文檔

javax.lang.model.element

Element
用于 Java 的模型元素的接口掉瞳。

  • ExecutableElement:表示某個(gè)類或接口的方法、構(gòu)造方法或初始化程序(靜態(tài)或?qū)嵗├四ㄗ⑨岊愋驮?/li>
  • PackageElement:表示一個(gè)包程序元素
  • TypeElement:表示一個(gè)類或接口程序元素
  • TypeParameterElement:表示類陕习、接口、方法或構(gòu)造方法元素的形式類型參數(shù)
  • VariableElement:表示一個(gè)字段址愿、enum 常量该镣、方法或構(gòu)造方法參數(shù)、局部變量或異常參數(shù)

通過(guò)Element的getModifiers()獲得元素的修飾符

Modifier
表示程序元素(如類响谓、方法或字段)上的修飾符损合。
以下是常用修飾符:

  • ABSTRACT:修飾符 abstract
  • FINAL:修飾符 final
  • NATIVE:修飾符 native
  • PRIVATE:修飾符 private
  • PROTECTED:修飾符 protected
  • PUBLIC:修飾符 public
  • STATIC:修飾符 static
  • SYNCHRONIZED:修飾符 synchronized

通過(guò)Element的asType()獲得元素的類型

TypeMirror
表示 Java 編程語(yǔ)言中的類型省艳。這些類型包括基本類型、聲明類型(類和接口類型)嫁审、數(shù)組類型跋炕、類型變量和 null 類型。

通過(guò)TypeMirror的getKind()類型的種類

TypeKind
表示類型的種類律适。

以下是常用的類型:

  • ARRAY:數(shù)組類型
  • BOOLEAN:基本類型 boolean
  • BYTE:基本類型 byte
  • CHAR:基本類型 char
  • DECLARED:類或接口類型
  • DOUBLE:基本類型 double
  • ERROR:無(wú)法解析的類或接口類型辐烂。
  • EXECUTABLE:方法、構(gòu)造方法或初始化程序
  • FLOAT:基本類型 float
  • INT:基本類型 int
  • LONG:基本類型 long
  • NONE:在實(shí)際類型不適合的地方使用的偽類型
  • NULL:null 類型
  • PACKAGE:對(duì)應(yīng)于包元素的偽類型
  • SHORT:基本類型 short
  • TYPEVAR:類型變量
  • VOID:對(duì)應(yīng)于關(guān)鍵字 void 的偽類型

獲取元素的父元素
通過(guò)Element的getEnclosingElement返回元素的父元素捂贿。

獲取元素上的注解
通過(guò)Element的getAnnotation(Class<A> annotationType)獲得元素上的注解纠修。

了解了上述內(nèi)容,下面咱們開(kāi)始搞事情

創(chuàng)建注解處理器

1.Android Studio的File->New->New module厂僧,如下圖:


2.在彈出的Create New Module對(duì)話框中選擇Java Library扣草,命名為MockButterknife-complier,如下圖:

3.創(chuàng)建注解處理器類吁系,繼承AbstractProcessor德召,覆蓋getSupportedAnnotationTypes()、getSupportedSourceVersion()汽纤、process()三個(gè)方法上岗,如下圖:

4.注冊(cè)注解處理器,在項(xiàng)目下創(chuàng)建resources->META-INF->Services目錄蕴坪,在Services目錄下創(chuàng)建javax.annotation.processing.Processor文件肴掷,如下圖:

5.編輯javax.annotation.processing.Processor文件,添加注解處理器類背传,如下圖:

6.配置注解處理器呆瞻,添加JavaPoet,如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.squareup:javapoet:1.8.0'
}

7.創(chuàng)建自定義注解径玖,咱們?cè)谶@里創(chuàng)建兩個(gè)注解:

  • BindView注解
package com.hys.mockbutterknife.annotations;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
  • OnClick注解
package com.hys.mockbutterknife.annotations;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] value();

8.注冊(cè)自定義注解到注解處理器痴脾,在AnnotationProcessor添加如下代碼:

private Set<Class<? extends Annotation>> getSupportedAnnotations(){
        Set<Class<? extends Annotation>> supportedAnnotations = new LinkedHashSet<>();
        supportedAnnotations.add(BindView.class);
        supportedAnnotations.add(OnClick.class);
        return supportedAnnotations;
    }

在getSupportedAnnotationTypes()方法中調(diào)用getSupportedAnnotations(),即將自定義注解注冊(cè)到注解處理器梳星,代碼如下:

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new LinkedHashSet<>();

        Iterator ite = getSupportedAnnotations().iterator();
        while (ite.hasNext()){
            Class annotation = (Class<? extends Annotation>)ite.next();
            supportedAnnotationTypes.add(annotation.getCanonicalName());
        }

        return supportedAnnotationTypes;
    }

9.上面咱們已經(jīng)注冊(cè)了自定義注解赞赖,接下來(lái)應(yīng)該處理這些注解(啰嗦,不處理冤灾,注冊(cè)它們做啥前域?!)

后面以BindView為例

查找添加注解的元素

Iterator ite = env.getElementsAnnotatedWith(BindView.class).iterator();

驗(yàn)證元素合法性

  • 驗(yàn)證元素是否可以訪問(wèn)
private boolean isInaccessible(Element element, String targetThing, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        //檢查元素的訪問(wèn)修飾符
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
            this.error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //檢查元素的父元素
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            this.error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //檢查父元素的訪問(wèn)修飾符
        if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
            this.error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        return false;
    }
  • 驗(yàn)證元素所在包的合法性
private boolean isInWrongPackage(Element element, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        String qualifiedName = enclosingElement.getQualifiedName().toString();
        //元素的父元素(即元素所在的類)不能在android的系統(tǒng)包中
        if (qualifiedName.startsWith("android.")) {
            this.error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        } 
        ////元素的父元素不能在java的資源包中
        else if (qualifiedName.startsWith("java.")) {
            this.error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        }

        return false;
    }
  • 驗(yàn)證元素類型的合法性
/*
* 遞歸驗(yàn)證
* 以TextView為例:isSubtypeOfType(typeMirror, "android.view.View")
*/
public static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
        // 類型相同
        if (isTypeEqual(typeMirror, otherType))
            return true;

        if (typeMirror.getKind() != TypeKind.DECLARED)
            return false;

        DeclaredType declaredType = (DeclaredType)typeMirror;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() > 0) {
            StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
            typeString.append('<');

            for(int i = 0; i < typeArguments.size(); ++i) {
                if (i > 0) {
                    typeString.append(',');
                }

                typeString.append('?');
            }

            typeString.append('>');
            if (typeString.toString().equals(otherType)) {
                return true;
            }
        }

        Element element = declaredType.asElement();
        if (!(element instanceof TypeElement)) {
            return false;
        } else {
            TypeElement typeElement = (TypeElement)element;
            // 獲取元素的父類
            TypeMirror superType = typeElement.getSuperclass();
            // 檢查父類的類型
            if (isSubtypeOfType(superType, otherType)) {
                return true;
            } else {                
                Iterator var7 = typeElement.getInterfaces().iterator();

                TypeMirror interfaceType;
                do {
                    if (!var7.hasNext()) {
                        return false;
                    }

                    interfaceType = (TypeMirror)var7.next();
                } while(!isSubtypeOfType(interfaceType, otherType));

                return true;
            }
        }

    }

生成Java源文件

  • 生成類
private TypeSpec createTypeSpec(){
        // 生成新類名韵吨,原類名+ _ViewBinding
        String className = this.encloseingElement.getSimpleName().toString() + "_ViewBinding";
        // 獲取父元素的類型全稱
        TypeName targetTypeName = TypeName.get(this.encloseingElement.asType());

        // 創(chuàng)建類構(gòu)建器
        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
                .addModifiers(new Modifier[]{Modifier.PUBLIC}) // 添加public修飾符
                .addField(targetTypeName, "target", new Modifier[]{Modifier.PRIVATE}); // 添加成員變量target

        classBuilder.addFields(createFieldForListener());
      
        if(isActivity()){
            classBuilder.addMethod(createConstructorForActivity());
        } else if(isView()){
            classBuilder.addMethod(createConstructorForView());
        } else if(isDialog()){
            classBuilder.addMethod(createConstructorForDialog());
        }

        // 默認(rèn)類構(gòu)造器
        classBuilder.addMethod(createBindConstructor());
        // 生成類
        return classBuilder.build();
    }
  • 生成JavaFile對(duì)象
public JavaFile brewJava() {
        String packageName = MoreElements.getPackage(this.encloseingElement).getQualifiedName().toString();
        return JavaFile.builder(packageName, createTypeSpec()).build();
    }
  • 生成Java源文件
...
JavaFile javaFile = bindSet.brewJava();

try{
      javaFile.writeTo(this.processingEnv.getFiler());
}catch (IOException ex){
     this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, ex.getMessage());
}
...

創(chuàng)建API

注解處理器搞好了匿垄,還需要給用戶提供API,用戶才能使用。

咱們創(chuàng)建一個(gè)新的Module椿疗,Android Studio的File->New->New module漏峰,選擇Android Library,命名為Mockbutterknife-source变丧。

這個(gè)Module主要使用反射技術(shù)芽狗,動(dòng)態(tài)的創(chuàng)建并調(diào)用上文中生成的類(下文中稱為綁定類)。

  • 編寫(xiě)API接口(其中之一)
    @UiThread
    public static void bind(Activity target) {
        View sourceView = target.getWindow().getDecorView();
        createBinding(target, sourceView);
    }
  • 動(dòng)態(tài)創(chuàng)建綁定類痒蓬,調(diào)用其構(gòu)造器方法
private static void createBinding(Object target, View source) {
        Class<?> targetClass = target.getClass();
        // 查找targetClass名稱+_ViewBinding的class文件,加載并返回構(gòu)造器
        Constructor constructor = findBindConstructorForClass(targetClass);

        if (constructor == null) {
            return ;
        }

        try {
            constructor.newInstance(target, source);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }

在APP中使用

  • 配置APP滴劲,在build.gradle中添加如下內(nèi)容:
dependencies {
    ...
    annotationProcessor project(':MockButterknife-complier') 
    implementation project(path: ':MockButterknife-complier')
    implementation project(path: ':Mockbutterknife-source')
  • 為Activity添加自定義注解
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_click)
    TextView tvClick;
    @BindView(R.id.tv_dont_click)
    TextView tvDontClcik;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MockButterKnife.bind(this);

        initData();
    }

   ...

    @OnClick(value = {R.id.tv_click, R.id.tv_dont_click})
    public void onClick(View view){

        if(view.getId() == R.id.tv_click)
            new AboutDialog().show(this.getSupportFragmentManager());
        else if(view.getId() == R.id.tv_dont_click)
            Toast.makeText(this, getString(R.string.main_toast), Toast.LENGTH_SHORT).show();
    }
}
  • 生成的class文件
package com.hys.annotationprocessortest;

import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  private MainActivity target;

  private View view2131165309;

  private View view2131165310;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;
    this.target.tvClick = (TextView)source.findViewById(2131165309);
    this.target.tvDontClcik = (TextView)source.findViewById(2131165310);
    this.view2131165309 = source.findViewById(2131165309);
    this.view2131165309.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           target.onClick(v);
       }
    });
    this.view2131165310 = source.findViewById(2131165310);
    this.view2131165310.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           target.onClick(v);
       }
    });
  }
}

好了攻晒,關(guān)于如何構(gòu)建編譯時(shí)注解解析框架,就先講到這班挖,上述項(xiàng)目的具體代碼在Github鲁捏,感謝你耐心的閱讀。


我是青嵐之峰萧芙,如果讀完后覺(jué)的有所收獲给梅,歡迎點(diǎn)贊加關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市双揪,隨后出現(xiàn)的幾起案子动羽,更是在濱河造成了極大的恐慌,老刑警劉巖渔期,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件运吓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡疯趟,警方通過(guò)查閱死者的電腦和手機(jī)拘哨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)信峻,“玉大人倦青,你說(shuō)我怎么就攤上這事№镂瑁” “怎么了产镐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)矾策。 經(jīng)常有香客問(wèn)我磷账,道長(zhǎng),這世上最難降的妖魔是什么贾虽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任逃糟,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绰咽。我一直安慰自己菇肃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布取募。 她就那樣靜靜地躺著琐谤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玩敏。 梳的紋絲不亂的頭發(fā)上斗忌,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音旺聚,去河邊找鬼织阳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛砰粹,可吹牛的內(nèi)容都是我干的唧躲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼碱璃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼弄痹!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嵌器,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肛真,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后嘴秸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毁欣,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年岳掐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凭疮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡串述,死狀恐怖执解,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纲酗,我是刑警寧澤衰腌,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站觅赊,受9級(jí)特大地震影響右蕊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吮螺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一饶囚、第九天 我趴在偏房一處隱蔽的房頂上張望帕翻。 院中可真熱鬧,春花似錦萝风、人聲如沸嘀掸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)睬塌。三九已至,卻和暖如春歇万,著一層夾襖步出監(jiān)牢的瞬間揩晴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工贪磺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留文狱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓缘挽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親呻粹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壕曼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355