一個(gè)例子帶你了解兩種自定義注解

有時(shí)候轉(zhuǎn)過頭看回一些基礎(chǔ)知識(shí),才發(fā)現(xiàn)原來當(dāng)時(shí)候自己覺得很難的東西都是從基礎(chǔ)知識(shí)衍生而來的,突然會(huì)有點(diǎn)豁然開朗的感覺次员。譬如說我們今天要講的知識(shí)點(diǎn)———注解。

初識(shí)王带,無處不在的注解

從Java1.5就開始引入淑蔚,在注解中,我們很容易就看到了Java的理念辫秧,"Write Once,Run Anywhere"束倍。平時(shí)開發(fā)的時(shí)候我們看到最多的注解莫過于是Java三種內(nèi)建注解之一的@Override被丧。

@Override——當(dāng)我們想要復(fù)寫父類中的方法時(shí)盟戏,我們需要使用該注解去告知編譯器我們想要復(fù)寫這個(gè)方法。這樣一來當(dāng)父類中的方法移除或者發(fā)生更改時(shí)編譯器將提示錯(cuò)誤信息甥桂。

其實(shí)在Android開發(fā)中柿究,我們?cè)诤芏嗟谌綆熘袝?huì)經(jīng)常看到注解黄选,下面我就列舉介紹一下在《Android高級(jí)進(jìn)階》中看到的一些關(guān)于運(yùn)用到注解的例子:

1.標(biāo)準(zhǔn)注解:

Java API中默認(rèn)定義的注解我們稱之為標(biāo)準(zhǔn)注解蝇摸,他們定義在java.lang,java.lang.annotation和javax.annotation包中,按照不同場(chǎng)景分為三類:

  1. 編譯時(shí)相關(guān)注解:編譯相關(guān)的注解是給編譯器使用办陷,如
    @Override貌夕、@Deprecated、SuppressWarnings民镜、@SafeVarags啡专、@Generated、@FunctionalInterface...
  2. 資源相關(guān)注解:一般用在JavaEE領(lǐng)域制圈,Android開發(fā)中應(yīng)該不會(huì)使用到们童,如@PostConstruct、@PreDestroy鲸鹦、@Resource慧库、@Resources...
  3. 元注解:用來定義和實(shí)現(xiàn)注解的注解,如@Target馋嗜、@Retention齐板、@Documented、@Inherited、@Repeatable

2.Support Annotation Library:

Support Annotation Library是從Android Support Library19.1開始引入的一個(gè)全新的函數(shù)包甘磨,它包含了一系列有用的元注解听皿,用來幫助開發(fā)者在編譯期間發(fā)現(xiàn)可能存在的Bug。

  1. Nullness注解:如@Nullable宽档、@NonNull
  2. 資源類型注解:如@AnimatorRes尉姨、@AttrRes、@LayoutRes...
  3. 類型定義注解:如@IntDef...
  4. 線程注解:如@UiThread吗冤、@MainThread又厉、@WorkerThread、@BinderThread...
  5. RGB顏色值注解:如@ColorRes
  6. 值范圍注解:如@Size椎瘟、@IntRange覆致、@FloatRange...
  7. 權(quán)限注解:如@RequirdsPermission
  8. 重寫函數(shù)注解:如@CallSuper
  9. 返回值注解:如@CheckResult
  10. @VisibleForTesting
  11. @Keep

3.一些著名的第三方庫:

如Butterknife、Dagger2肺蔚、DBFlow煌妈、Retrofit、JUnit

以上都是總結(jié)了大部分在《Android高級(jí)進(jìn)階》中出現(xiàn)的注解的地方宣羊,很多注解沒一個(gè)個(gè)解釋璧诵,有興趣的同學(xué)可以自己去搜索一下自己想知道的注解的具體用途。我們可以看到注解無論在java和Android中都是使用很廣泛的仇冯,而且慢慢變得必不可少之宿。下面我們就進(jìn)入我們的主題,分別用兩種方式去自定義注解苛坚。

那么我們先列出一個(gè)簡單的題目比被,然后用兩種不同的方式去實(shí)現(xiàn):

題目:用注解實(shí)現(xiàn)兩數(shù)相加的運(yùn)算

一、運(yùn)行時(shí)自定義注解:

運(yùn)行時(shí)注解一般和反射機(jī)制配合使用泼舱,相比編譯時(shí)注解性能比較低等缀,但靈活性好,實(shí)現(xiàn)起來比較簡單娇昙,所以我們先來用這個(gè)去實(shí)現(xiàn)尺迂。

1. 我們先去創(chuàng)建文件和寫一個(gè)注解

/* 用來指明注解的訪問范圍
*  1.源碼級(jí)注解SOURCE,該類型的注解信息會(huì)留在.java源碼中涯贞,
*    源碼編譯后枪狂,注解信息會(huì)被丟棄,不會(huì)保留在編譯好的.class文件中宋渔;
*  2.編譯時(shí)注解CLASS州疾,注解信息會(huì)保留在.java源碼里和.class文件中,
*    在執(zhí)行的時(shí)候皇拣,會(huì)被Java虛擬機(jī)丟棄不回家再到虛擬機(jī)中严蓖;
*  3.運(yùn)行時(shí)注解RUNTIME薄嫡,java源碼里,.class文件中和Java虛擬機(jī)在運(yùn)行期也保留注解信息颗胡,
*    可通過反射讀取
*/
@Retention(RUNTIME)
//是一個(gè)ElementType類型的數(shù)組毫深,用來指定注解所使用的對(duì)象范圍
@Target(value = FIELD)
public @interface Add {
    float ele1() default 0f;
    float ele2() default 0f;
}

可以看到,因?yàn)槭沁\(yùn)行時(shí)注解毒姨,所以我們定義了@Retention是Runtime哑蔫,定義了ele1,ele2兩個(gè)看上去像函數(shù)的變量(在注解里這樣寫算是變量而不是方法或函數(shù))

2. 下面我們使用反射去告訴這個(gè)注解你應(yīng)該做什么

public class InjectorProcessor {
    public void process(final Object object) {
        
        Class class1 = object.getClass();
        //找到類里所有變量Field
        Field[] fields = class1.getDeclaredFields();
        //遍歷Field數(shù)組
        for(Field field:fields){
            //找到相應(yīng)的擁有Add注解的Field
            Add addMethod = field.getAnnotation(Add.class);
            if (addMethod != null){
                if(object instanceof Activity){
                    //獲取注解中ele1和ele2兩個(gè)數(shù)字弧呐,然后把他們相加
                    double d = addMethod.ele1() + addMethod.ele2();
                    try {
                        //把相加結(jié)果的值賦給該Field
                        field.setDouble(object,d);
                    }catch (Exception e){

                    }

                }
            }
        }

    }
}

就這樣闸迷,我們利用了反射,告訴了Add這個(gè)注解俘枫,在代碼里找到你的時(shí)候腥沽,你該做什么,把工作做好鸠蚪,你就有飯吃今阳。

3.使用


image

image

很快,我們就用第一種方式實(shí)現(xiàn)了給出的題目茅信;確實(shí)盾舌,在代碼量上這種方式比較簡單粗暴,但是這種方式并不常用汹押。

一矿筝、編譯時(shí)自定義注解:

有不常用的方式,肯定就有常用的方式棚贾,下面我們就來介紹這個(gè)常用的方式——注解處理器

著名的第三方框架ButterKnife也就是用這種方式去實(shí)現(xiàn)注解綁定控件的功能的。

注解處理器是(Annotation Processor)是javac的一個(gè)工具榆综,用來在編譯時(shí)掃描和編譯和處理注解(Annotation)妙痹。你可以自己定義注解和注解處理器去搞一些事情。一個(gè)注解處理器它以Java代碼或者(編譯過的字節(jié)碼)作為輸入鼻疮,生成文件(通常是java文件)怯伊。這些生成的java文件不能修改,并且會(huì)同其手動(dòng)編寫的java代碼一樣會(huì)被javac編譯判沟」⑶郏看到這里加上之前理解,應(yīng)該明白大概的過程了挪哄,就是把標(biāo)記了注解的類吧秕,變量等作為輸入內(nèi)容,經(jīng)過注解處理器處理迹炼,生成想要生成的java代碼砸彬。

我們可以看到所有注解都會(huì)在編譯的時(shí)候就把代碼生成颠毙,而且高效、避免在運(yùn)行期大量使用反射砂碉,不會(huì)對(duì)性能造成損耗蛀蜜。
下面我們就看看怎么去實(shí)現(xiàn)一個(gè)注解處理器:

1. 建立工程:

  1. 首先創(chuàng)建一個(gè)project;
  2. 創(chuàng)建lib_annotations增蹭,
    這是一個(gè)純java的module滴某,不包含任何android代碼,只用于存放注解滋迈。
  3. 創(chuàng)建lib_compiler壮池,
    這同樣是一個(gè)純java的module。該module依賴于步驟2創(chuàng)建的module_annotation,處理注解的代碼都在這里杀怠,該moduule最終不會(huì)被打包進(jìn)apk椰憋,所以你可以在這里導(dǎo)入任何你想要的任意大小依賴庫。
  4. 創(chuàng)建lib_api,
    對(duì)該module不做要求赔退,可以是android library或者java library或者其他的橙依。該module用于調(diào)用步驟3生成的輔助類方法。
image

image

為什么我們要新建這么多module呢硕旗,原因很簡單窗骑,因?yàn)橛行煸诰幾g時(shí)起作用,有些在運(yùn)行時(shí)起作用漆枚,把他們放在同一個(gè)module下會(huì)報(bào)錯(cuò)创译,所以我們秉著各司其職的理念把他們都分開了。

2.在module的lib_annotations創(chuàng)建Add注解

image

跟第一種方法不同墙基,我們?cè)贎Retention選擇的是CLASS软族,雖然選擇RUNTIME也是可以的,但是為了顯示區(qū)別残制,我們還是作了修改立砸。

3.寫注解處理器

在寫注解處理器之前我們必須在lib_compiler中引入兩個(gè)庫輔助我們成就大業(yè):

image
  1. auto-service:
    AutoService會(huì)自動(dòng)在META-INF文件夾下生成Processor配置信息文件,該文件里就是實(shí)現(xiàn)該服務(wù)接口的具體實(shí)現(xiàn)類初茶。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候颗祝,
    就能通過該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化恼布,完成模塊的注入螺戳。
    基于這樣一個(gè)約定就能很好的找到服務(wù)接口的實(shí)現(xiàn)類,而不需要再代碼里制定折汞,方便快捷倔幼。
  2. javapoet:JavaPoet是square推出的開源java代碼生成框架,提供Java Api生成.java源文件字支。這個(gè)框架功能非常有用凤藏,我們可以很方便的使用它根據(jù)注解奸忽、數(shù)據(jù)庫模式、協(xié)議格式等來對(duì)應(yīng)生成代碼揖庄。通過這種自動(dòng)化生成代碼的方式栗菜,可以讓我們用更加簡潔優(yōu)雅的方式要替代繁瑣冗雜的重復(fù)工作。

我們先在lib_compiler中創(chuàng)建一個(gè)基類

image
public class AnnotatedClass {
    
    public Element mClassElement;
    /**
     * 元素相關(guān)的輔助類
     */
    public Elements mElementUtils;

    public TypeMirror elementType;

    public Name elementName;
    
    //加法的兩個(gè)值
    private float value1;
    private float value2;


    public AnnotatedClass(Element classElement) {

        this.mClassElement = classElement;
        this.elementType = classElement.asType();
        this.elementName = classElement.getSimpleName();

        value1 = mClassElement.getAnnotation(Add.class).ele1();
        value2 = mClassElement.getAnnotation(Add.class).ele2();
    }

    Name getElementName() {
        return elementName;
    }

    TypeMirror getElementType(){
        return elementType;
    }

    Float getTotal(){
        return (value1 + value2);
    }


    /**
     * 包名
     */
    public String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }
    /**
     * 類名
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
    }
}

然后我們的主角就要出場(chǎng)了——注解處理器
我們創(chuàng)建一個(gè)文件蹄梢,然后繼承AbstractProcessor

@AutoService(Processor.class)
public class AddProcessor extends AbstractProcessor{

    private static final String ADD_SUFFIX = "_Add";
    private static final String TARGET_STATEMENT_FORMAT = "target.%1$s = %2$s";
    private static final String CONST_PARAM_TARGET_NAME = "target";

    private static final char CHAR_DOT = '.';

    private Messager messager;
    private Types typesUtil;
    private Elements elementsUtil;
    private Filer filer;
     /** 
     * 解析的目標(biāo)注解集合疙筹,一個(gè)類里可以包含多個(gè)注解,所以是Map<String, List<AnnotatedClass>>
     */  
    Map<String, List<AnnotatedClass>> annotatedElementMap = new LinkedHashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnv.getMessager();
        typesUtil = processingEnv.getTypeUtils();
        elementsUtil = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(Add.class.getCanonicalName());
        return annotataions;
    }


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        因?yàn)樵摲椒赡軙?huì)執(zhí)行多次禁炒,所以每次進(jìn)來必須clear
        annotatedElementMap.clear();
        //1.遍歷每個(gè)有Add注解的Element而咆,
        //2.然后把它加入Map里面,一個(gè)類里可以包含多個(gè)注解,所以是Map<String, List<AnnotatedClass>>幕袱,
        //3.賦予它工作任務(wù)暴备,告訴他你該做什么,
        //4.然后生成Java文件
        for (Element element : roundEnv.getElementsAnnotatedWith(Add.class)) {
            //判斷被注解的類型是否符合要求
            if (element.getKind() != ElementKind.FIELD) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Only FIELD can be annotated with @%s");
            }
            TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
            String fullClassName = encloseElement.getQualifiedName().toString();
            AnnotatedClass annotatedClass = new AnnotatedClass(element);
            //把類名和該類里面的所有關(guān)于Add注解的注解放到Map里面
            if(annotatedElementMap.get(fullClassName) == null){
                annotatedElementMap.put(fullClassName, new ArrayList<AnnotatedClass>());
            }
            annotatedElementMap.get(fullClassName).add(annotatedClass);

        }
        //因?yàn)樵摲椒〞?huì)執(zhí)行多次,所以size=0時(shí)返回true結(jié)束
        if (annotatedElementMap.size() == 0) {
            return true;
        }

        //用javapoet生成類文件
        try {
            for (Map.Entry<String, List<AnnotatedClass>> entry : annotatedElementMap.entrySet()) {
                MethodSpec constructor = createConstructor(entry.getValue());
                TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
                JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
                javaFile.writeTo(filer);
            }

        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR, "Error on creating java file");
        }

        return true;
    }


    //以下是javapoet創(chuàng)建各種方法的實(shí)現(xiàn)方式
    private MethodSpec createConstructor(List<AnnotatedClass> randomElements) {
        AnnotatedClass firstElement = randomElements.get(0);
        MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(firstElement.mClassElement.getEnclosingElement().asType()), CONST_PARAM_TARGET_NAME);
        for (int i = 0; i < randomElements.size(); i++) {
            addStatement(builder, randomElements.get(i));
        }
        return builder.build();
    }

    private void addStatement(MethodSpec.Builder builder, AnnotatedClass randomElement) {
        builder.addStatement(String.format(
                TARGET_STATEMENT_FORMAT,
                randomElement.getElementName().toString(),
                randomElement.getTotal())
        );
    }

    private TypeSpec createClass(String className, MethodSpec constructor) {
        return TypeSpec.classBuilder(className + ADD_SUFFIX)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(constructor)
                .build();
    }

    private String getPackage(String qualifier) {
        return qualifier.substring(0, qualifier.lastIndexOf(CHAR_DOT));
    }

    private String getClassName(String qualifier) {
        return qualifier.substring(qualifier.lastIndexOf(CHAR_DOT) + 1);
    }

}

我們可以看到注解處理器總共有四個(gè)方法,他們分別的作用是:

  1. init() 可選
    在該方法中可以獲取到processingEnvironment對(duì)象次泽,借由該對(duì)象可以獲取到生成代碼的文件對(duì)象, debug輸出對(duì)象,以及一些相關(guān)工具類
  1. getSupportedSourceVersion()
    返回所支持的java版本障癌,一般返回當(dāng)前所支持的最新java版本即可
  1. getSupportedAnnotationTypes()
    你所需要處理的所有注解,該方法的返回值會(huì)被process()方法所接收
  1. process() 必須實(shí)現(xiàn)
    掃描所有被注解的元素辩尊,并作處理涛浙,最后生成文件。該方法的返回值為boolean類型摄欲,若返回true,則代表本次處理的注解已經(jīng)都被處理轿亮,不希望下一個(gè)注解處理器繼續(xù)處理,否則下一個(gè)注解處理器會(huì)繼續(xù)處理蒿涎。

4.使用

好了哀托,打了這么多代碼,我們先看下編譯時(shí)生成的代碼和文件是怎么樣的劳秋,就會(huì)使用了:


image

我們可以看到,我們?cè)谧⒔馓幚砥骼锩鎸懥四敲炊啻a胖齐,就是為了生成Build目錄下的.class文件玻淑,是自動(dòng)生成的。

看到了生成的AnnotationActivity_Add的文件呀伙,我們下面就去寫一個(gè)注入方法补履,把我們想要結(jié)果拿出來展示:


image

我們看到Util里面我們實(shí)現(xiàn)了想要的東西,把AnnotationActivity_Add的結(jié)果找出來再賦值給相應(yīng)的變量剿另。


image

image

總結(jié)

我們成功的用兩種不同的注解方式實(shí)現(xiàn)了兩數(shù)相加的運(yùn)算箫锤,1.運(yùn)用的是反射贬蛙,2.運(yùn)用的是注解處理器。雖然看上去注解處理器的方式比較繁瑣谚攒,但是使用比較普遍阳准,而且有很多好處,這里就不一一述說馏臭。如果有興趣學(xué)習(xí)的同學(xué)可以下載源碼去學(xué)習(xí)一下野蝇,互相交流,共同學(xué)習(xí)括儒。Demo源碼下載鏈接

參考文章:

使用Android注解處理器绕沈,解放勞動(dòng)生產(chǎn)力

JavaPoet - 優(yōu)雅地生成代碼

我的掘金

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帮寻,隨后出現(xiàn)的幾起案子乍狐,更是在濱河造成了極大的恐慌,老刑警劉巖固逗,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浅蚪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡抒蚜,警方通過查閱死者的電腦和手機(jī)掘鄙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗡髓,“玉大人操漠,你說我怎么就攤上這事《稣猓” “怎么了浊伙?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長长捧。 經(jīng)常有香客問我嚣鄙,道長,這世上最難降的妖魔是什么串结? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任哑子,我火速辦了婚禮,結(jié)果婚禮上肌割,老公的妹妹穿的比我還像新娘卧蜓。我一直安慰自己,他們只是感情好把敞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布弥奸。 她就那樣靜靜地躺著,像睡著了一般奋早。 火紅的嫁衣襯著肌膚如雪盛霎。 梳的紋絲不亂的頭發(fā)上赠橙,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音愤炸,去河邊找鬼。 笑死摇幻,一個(gè)胖子當(dāng)著我的面吹牛横侦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绰姻,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼枉侧,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了狂芋?” 一聲冷哼從身側(cè)響起榨馁,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帜矾,沒想到半個(gè)月后翼虫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屡萤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年珍剑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片死陆。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡招拙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出措译,到底是詐尸還是另有隱情别凤,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布领虹,位于F島的核電站规哪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏塌衰。R本人自食惡果不足惜诉稍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望最疆。 院中可真熱鬧均唉,春花似錦、人聲如沸肚菠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚊逢。三九已至层扶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間烙荷,已是汗流浹背镜会。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留终抽,地道東北人戳表。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像昼伴,于是被迫代替她去往敵國和親匾旭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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